Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
48ebb51
seo(landing): Organization/WebSite entity JSON-LD, AI-crawler robots …
BSalaeddin Sep 12, 2026
47f9657
seo(landing): serve the IndexNow key file
BSalaeddin Sep 12, 2026
33f5f2f
seo(landing): query-driven titles, H1s and descriptions for the frame…
BSalaeddin Sep 12, 2026
7e41670
perf(landing): paint the hero copy from the server HTML and defer the…
BSalaeddin Sep 12, 2026
2359b87
docs(landing): claim-integrity pass — truthful wording for parity, pr…
BSalaeddin Sep 12, 2026
7b37913
perf(landing): load the live demo, StackBlitz editor, feature scenes …
BSalaeddin Sep 12, 2026
3c7b466
ci(landing): add a warn-only nightly mobile Lighthouse performance ra…
BSalaeddin Sep 12, 2026
5e46887
ci(docs): accept the robots/sitemap metadata routes as redirect targe…
BSalaeddin Sep 12, 2026
b6db509
ci(minio): pull the pinned MinIO images from quay.io now that Docker …
BSalaeddin Sep 12, 2026
e05145d
Merge pull request #421 from DevinoSolutions/ci/minio-images-quay
AminDhouib Sep 16, 2026
decdff4
Merge pull request #416 from DevinoSolutions/seo/indexnow-key
AminDhouib Sep 16, 2026
08e055b
Merge pull request #418 from DevinoSolutions/seo/claim-integrity
AminDhouib Sep 16, 2026
5588fd4
Merge pull request #417 from DevinoSolutions/seo/money-page-copy
AminDhouib Sep 16, 2026
df65483
Merge branch 'dev' into seo/entity-and-hygiene
AminDhouib Sep 16, 2026
409c9c7
Merge dev into seo/mobile-performance
AminDhouib Sep 16, 2026
4333895
Merge pull request #415 from DevinoSolutions/seo/entity-and-hygiene
AminDhouib Sep 16, 2026
7d91678
Merge pull request #419 from DevinoSolutions/seo/mobile-performance
AminDhouib Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ jobs:
run: pnpm --filter @useupup/e2e-test test:e2e:drive-sandbox

Lighthouse:
name: Lighthouse (landing SEO + Best Practices)
name: Lighthouse (landing SEO + Best Practices + mobile perf ratchet)
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
Expand All @@ -384,17 +384,27 @@ jobs:
# lhci boots `next start` itself and asserts per lighthouserc.cjs:
# SEO=100 everywhere; Best Practices 100 on docs pages, ratcheted
# on home/framework pages (third-party embed cookies). A11y is NOT
# asserted here — the axe ratchet above owns it; perf is excluded
# (noisy on shared runners; size-limit owns bundle weight).
# asserted here — the axe ratchet above owns it. Performance is not
# asserted in THIS config either; it has its own mobile run below.
- name: Lighthouse audit + assertions (SEO / Best Practices)
run: pnpm --filter @useupup/landing run lighthouse

# Mobile performance, on its own port and its own config, asserted
# at WARN level only: the score is genuinely noisy on a shared
# runner, so this is a visible ratchet in the log and the uploaded
# report rather than a gate. (`continue-on-error` is banned by the
# test-quality guard and unnecessary — warn assertions never fail.)
- name: Lighthouse mobile performance ratchet (warn-only)
run: pnpm --filter @useupup/landing run lighthouse:mobile

- name: Upload Lighthouse reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: nightly-lighthouse-reports
path: apps/landing/.lighthouseci-reports/
path: |
apps/landing/.lighthouseci-reports/
apps/landing/.lighthouseci-mobile-reports/
if-no-files-found: warn
retention-days: 14

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ apps/e2e-test/screenshots/
# filesystem report output uploaded as a CI artifact
.lighthouseci/
apps/landing/.lighthouseci-reports/
apps/landing/.lighthouseci-mobile-reports/
11 changes: 9 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,8 +617,15 @@ DrivePlugin`. All three popup providers now persist a token-expiry key and refre
SEO=100 on every audited page and Best Practices 100 on docs pages /
ratcheted ~0.74 on home+framework pages whose StackBlitz embed + ads tag set
third-party cookies; a11y is deliberately NOT asserted — the axe ratchet
owns it, Lighthouse's a11y audits are axe-core anyway — and perf is excluded
as CI-runner noise; size-limit owns bundle weight), static `build:storybook`
owns it, Lighthouse's a11y audits are axe-core anyway; perf is not asserted
in THIS config. Performance is no longer skipped outright: a second step in
the same job runs `pnpm --filter @useupup/landing run lighthouse:mobile`
(config `apps/landing/lighthouserc.mobile.cjs`: mobile emulation, its own
port 4464 so both configs run back to back, median of 5) and asserts
`categories:performance` at **warn** level — a visible ratchet in the log and
the uploaded report, never a red night, since the score really is noisy on a
shared runner and `continue-on-error` is banned by the test-quality guard;
size-limit still owns bundle weight), static `build:storybook`
for all six frameworks, `smoke:packages`, the mastra LLM evals (only when the
`OPENROUTER_API_KEY` Actions secret exists — absent, the job goes green with
a loud skip notice, never silently), and the **Drive-Sandbox** job — the live
Expand Down
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@
<h3 align="center">One file uploader. Every framework.</h3>

<p align="center">
A headless upload engine with native, byte-identical UI packages for
A headless upload engine with native, DOM-identical UI packages for
<b>React</b>, <b>Vue</b>, <b>Svelte</b>, <b>Angular</b>, <b>Vanilla&nbsp;JS</b>, and <b>Preact</b> —
with optional server-mode uploads, cloud drives, camera, screen capture, and link imports.
</p>

<!-- npm badges: add after first @useupup publish -->
<p align="center">
<a href="https://github.com/DevinoSolutions/upup/actions/workflows/main.yml"><img src="https://github.com/DevinoSolutions/upup/actions/workflows/main.yml/badge.svg" alt="CI" /></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
Expand All @@ -31,15 +30,15 @@
**upup** is a free, MIT-licensed file uploader built as one headless
[`@useupup/core`](packages/core) engine with a matching native UI for every major
framework. React is the visual canon; the Vue, Svelte, Angular, Vanilla, and
Preact ports render the **same DOM** with the **same props**, verified
byte-for-byte by a cross-framework parity harness. Upload straight from the
browser to any S3-compatible storage (Client Mode), or route through your own
backend with an HMAC-signed trust model (Server Mode via
[`@useupup/server`](packages/server)).
Preact ports render the **same DOM** with the **same props**, verified by a
cross-framework parity harness (structure, classes, accessibility attributes,
text). Upload straight from the browser to any S3-compatible storage (Client
Mode), or route through your own backend with an HMAC-signed trust model
(Server Mode via [`@useupup/server`](packages/server)).

## Install

Pick the package for your framework — the component API and rendered DOM are identical across all of them:
Pick the package for your framework — the component API and the rendered DOM contract are identical across all of them:

| Package | Install | Get started |
| ------------------ | ------------------------ | ------------------------------------------------------------------ |
Expand Down Expand Up @@ -113,7 +112,7 @@ Express, Fastify, and Hono handlers ship as subpath exports
## Features

- **Headless core.** `@useupup/core` is a zero-framework-dependency engine: file state, an upload pipeline (compression, HEIC→JPEG, EXIF stripping, checksums, thumbnails, optional Web Worker offload), cloud-drive plugins, i18n, and theming. Build your own UI on it, or use a native package.
- **Native UI for six frameworks.** React, Vue, Svelte, Angular, Vanilla JS, and Preact — same DOM, same Tailwind classes, enforced byte-for-byte by a parity harness.
- **Native UI for six frameworks.** React, Vue, Svelte, Angular, Vanilla JS, and Preact — same DOM structure, same `upup-` classes, enforced by a cross-framework parity harness.
- **Client or Server mode.** Direct browser → storage presigned uploads, or a server-proxied [`@useupup/server`](packages/server) with an HMAC-signed trust model (signed length, key/uploadId binding, mandatory secrets).
- **S3-compatible storage.** AWS S3, Cloudflare R2, MinIO, DigitalOcean Spaces, Backblaze B2, Wasabi — any S3-compatible endpoint.
- **Cloud drives.** Import from Google Drive, OneDrive, Dropbox, and Box, in client or server mode.
Expand All @@ -123,7 +122,10 @@ Express, Fastify, and Hono handlers ship as subpath exports
- **i18n & theming.** ICU-based localization with 9 bundled locales and RTL support, plus a slot-level theming system that targets every rendered element.
- **TypeScript-first.** Full type definitions out of the box.

## Battle-tested in production
## Used in production

Both apps below run the upup v3.1 engine under its previous `@upupjs/*` scope —
the same codebase, published before the rename to `@useupup/*`.

- **uNotes** — AI doc uploads for past exams → [unotes.net](https://unotes.net)
- **Shorty** — media uploads for transcripts → [aishorty.com](https://aishorty.com)
Expand Down
95 changes: 95 additions & 0 deletions apps/e2e-test/landing/docs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,99 @@ test.describe('docs', () => {
await expect(vueTabAgain).toHaveAttribute('aria-selected', 'true')
}).toPass({ timeout: 15_000 })
})

// ── SEO surfaces ──────────────────────────────────────────────────────
// The dev server this project boots leaves NEXT_PUBLIC_BASE_URL unset, so
// next.config.mjs computes SITE_BASE = https://useupup.com and takes the
// PRODUCTION branch of headers()/redirects(). That is what lets these
// assertions exercise the real prod rules without a second webServer.
const PRODUCTION_ORIGIN = 'https://useupup.com'

test('docs page declares its canonical URL and its markdown twin as an alternate', async ({
page,
request,
}) => {
await page.goto('/docs/getting-started/')
await expect(page.locator('link[rel=canonical]')).toHaveAttribute(
'href',
`${PRODUCTION_ORIGIN}/docs/getting-started/`,
)
const alternate = page.locator(
'link[rel=alternate][type="text/markdown"]',
)
await expect(alternate).toHaveAttribute(
'href',
`${PRODUCTION_ORIGIN}/docs-md/getting-started/`,
)
// Fetch the PATH against this server — following the absolute href
// would test production, not the build under test.
const href = await alternate.getAttribute('href')
const twin = await request.get(new URL(href ?? '').pathname)
expect(twin.status()).toBe(200)
expect(twin.headers()['content-type']).toContain('text/markdown')
// The twin must declare the HTML page as its original, or it is a
// duplicate of every docs page under a second URL.
expect(twin.headers()['link']).toContain('rel="canonical"')
expect(twin.headers()['link']).toContain(
`${PRODUCTION_ORIGIN}/docs/getting-started/`,
)
})

test('plaintext request identified by the Cloudflare visitor header is redirected to https', async ({
request,
}) => {
const redirected = await request.get('/react/', {
headers: { 'cf-visitor': '{"scheme":"http"}' },
maxRedirects: 0,
})
expect(redirected.status()).toBe(308)
expect(redirected.headers()['location']).toBe(
`${PRODUCTION_ORIGIN}/react/`,
)
// The header is the ONLY trigger: an ordinary request must still be
// served, or the rule would loop every visitor behind the proxy.
const plain = await request.get('/react/')
expect(plain.status()).toBe(200)
})

test('stale search-console sitemap URL permanently redirects to the live sitemap', async ({
request,
}) => {
// /sitemap-landing.xml is a Docusaurus-era submission that nothing has
// ever served; it 404'd until this rule landed.
const res = await request.get('/sitemap-landing.xml', {
maxRedirects: 0,
})
expect(res.status()).toBe(308)
expect(res.headers()['location']).toContain('/sitemap.xml')
})

test('production responses carry a preload-eligible HSTS header', async ({
request,
}) => {
const res = await request.get('/')
expect(res.headers()['strict-transport-security']).toBe(
'max-age=63072000; includeSubDomains; preload',
)
})

test('robots.txt names the AI crawler allow-list explicitly', async ({
request,
}) => {
const res = await request.get('/robots.txt')
expect(res.status()).toBe(200)
const body = await res.text()
expect(body).toContain('User-Agent: GPTBot')
expect(body).toContain('User-Agent: ClaudeBot')
expect(body).toContain('User-Agent: PerplexityBot')
})

test('footer links the llms.txt corpus from every page', async ({
page,
}) => {
await page.goto('/docs/getting-started/')
await expect(
page.locator('footer a[href="/llms.txt"]'),
).toHaveAttribute('href', '/llms.txt')
})
})
7 changes: 7 additions & 0 deletions apps/e2e-test/landing/thumbs-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ test.describe('Ask AI thumbs feedback', () => {
)
.toBe(true)

// The demo section is client-only and mounts the first time it comes
// within ~400px of the viewport (it is the heaviest thing on the page,
// and on a phone it costs seconds of main-thread time nobody who never
// scrolls to it should pay). Scroll it into view so the Ask-AI panel
// below actually exists.
await page.locator('#demo').scrollIntoViewIfNeeded()

// Ask one short question and wait for the assistant's completed turn.
const panel = page.locator('.upup-ie-ai-panel')
const input = panel.locator('#upup-ai-message')
Expand Down
2 changes: 1 addition & 1 deletion apps/landing/content/docs/ai-assistants.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Use upup with AI assistants
description: How coding agents (Claude Code, Cursor, and others) can discover and correctly integrate upup — including machine-readable context files and a paste-ready context block.
description: How coding agents (Claude Code, Cursor, and others) discover and correctly integrate upup — machine-readable context files and a paste-ready context block.
---

Coding agents choose a library by retrieving the docs page that answers the exact
Expand Down
2 changes: 1 addition & 1 deletion apps/landing/content/docs/api-reference/error-codes.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Error Codes
description: The complete UpupErrorCode enum, the seven UpupError classes that carry them, restriction reasons, and the response parser that turns server or S3 error bodies into typed errors.
description: The complete UpupErrorCode enum, the seven UpupError classes that carry them, restriction reasons, and the parser that turns error bodies into typed errors.
---

Most failures upup raises are an `UpupError` (or one of its six subclasses)
Expand Down
2 changes: 1 addition & 1 deletion apps/landing/content/docs/api-reference/events.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Events
title: File Upload Events
description: The complete upup event catalog — all 52 typed core events with their payloads, plus the React callback props and the core event each one is driven by.
---

Expand Down
2 changes: 1 addition & 1 deletion apps/landing/content/docs/api-reference/server-http.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Server HTTP API
description: The complete HTTP contract of @useupup/server — every route, request and response shape, status code, and operational headerfor wiring a custom client or debugging a live handler.
description: The complete HTTP contract of @useupup/server — every route, request and response shape, status code and header, for custom clients and live debugging.
---

`createUpupHandler(config)` returns one `(req: Request) => Promise<Response>`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Event Handlers
description: The UpupUploader callback props — onFilesSelected, onUploadStart, progress, per-file and batch completion, onFileRemoved, and onError — mirroring core upload state.
description: The UpupUploader callback props — onFilesSelected, onUploadStart, progress, per-file and batch completion, onFileRemoved and onError — mirroring core state.
---

React events mirror core upload state.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Optional Props
description: The complete optional-prop reference for UpupUploader — behavior, file validation, the processing pipeline, upload reliability, sources, appearance, localization, plus the headless-only engine options.
description: The complete optional-prop reference for UpupUploader — behavior, file validation, the processing pipeline, upload reliability, sources, appearance and i18n.
---

Every prop on this page is optional. The three upload-target props (`uploadEndpoint`, `serverUrl`, `resumable.endpoint`) and `provider` are covered in [Required Props](/docs/api-reference/upupuploader/required-props/); the 34 `on*` callbacks are covered in [Events & Callbacks](/docs/api-reference/events/).
Expand Down
Loading
Loading