From 33f5f2f99b8e480a6997204d2b20e3aebc123110 Mon Sep 17 00:00:00 2001 From: Alaeddin <15094821+BSalaeddin@users.noreply.github.com> Date: Sat, 12 Sep 2026 04:01:25 +0100 Subject: [PATCH] seo(landing): query-driven titles, H1s and descriptions for the framework pages, home and top storage guides --- apps/landing/content/docs/ai-assistants.mdx | 2 +- .../docs/api-reference/error-codes.mdx | 2 +- .../content/docs/api-reference/events.mdx | 2 +- .../docs/api-reference/server-http.mdx | 2 +- .../upupuploader/event-handlers.mdx | 2 +- .../upupuploader/optional-props.mdx | 2 +- .../docs/comparisons/upup-vs-uploadthing.mdx | 4 +- .../content/docs/comparisons/upup-vs-uppy.mdx | 4 +- .../content/docs/guides/accessibility.mdx | 2 +- .../content/docs/guides/auth/next-auth.mdx | 2 +- .../content/docs/guides/file-processing.mdx | 2 +- apps/landing/content/docs/guides/headless.mdx | 2 +- .../content/docs/guides/reliability.mdx | 2 +- .../content/docs/guides/server-auth.mdx | 2 +- .../content/docs/guides/server-mode-setup.mdx | 4 +- apps/landing/content/docs/guides/sources.mdx | 2 +- .../content/docs/guides/storage-providers.mdx | 2 +- .../docs/guides/storage/azure-blob.mdx | 4 +- .../docs/guides/storage/cloudflare-r2.mdx | 4 +- .../guides/storage/digitalocean-spaces.mdx | 4 +- .../content/docs/guides/storage/minio.mdx | 4 +- apps/landing/content/docs/guides/theming.mdx | 2 +- apps/landing/content/docs/index.mdx | 4 +- apps/landing/content/docs/localization.mdx | 2 +- .../content/docs/migration/v1-to-v3.mdx | 2 +- .../content/docs/quickstarts/angular.mdx | 2 +- .../landing/content/docs/quickstarts/next.mdx | 2 +- .../content/docs/quickstarts/preact.mdx | 2 +- .../content/docs/quickstarts/react.mdx | 2 +- .../content/docs/quickstarts/svelte.mdx | 2 +- .../content/docs/quickstarts/vanilla.mdx | 2 +- apps/landing/content/docs/quickstarts/vue.mdx | 2 +- .../content/docs/resumable-uploads.mdx | 4 +- .../src/__tests__/seo-copy-budgets.test.ts | 154 ++++++++++++++++++ apps/landing/src/app/[framework]/page.tsx | 9 +- apps/landing/src/app/layout.tsx | 4 +- apps/landing/src/app/page.tsx | 7 +- .../src/components/HomepageHero/index.tsx | 29 +++- apps/landing/src/lib/frameworks.tsx | 18 +- apps/landing/src/lib/siteConfig.ts | 8 +- 40 files changed, 252 insertions(+), 61 deletions(-) create mode 100644 apps/landing/src/__tests__/seo-copy-budgets.test.ts diff --git a/apps/landing/content/docs/ai-assistants.mdx b/apps/landing/content/docs/ai-assistants.mdx index 71d0cbafc..854d6e1d3 100644 --- a/apps/landing/content/docs/ai-assistants.mdx +++ b/apps/landing/content/docs/ai-assistants.mdx @@ -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 diff --git a/apps/landing/content/docs/api-reference/error-codes.mdx b/apps/landing/content/docs/api-reference/error-codes.mdx index 9596fe04c..d66fdecd1 100644 --- a/apps/landing/content/docs/api-reference/error-codes.mdx +++ b/apps/landing/content/docs/api-reference/error-codes.mdx @@ -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) diff --git a/apps/landing/content/docs/api-reference/events.mdx b/apps/landing/content/docs/api-reference/events.mdx index e9e699f8e..9e263baed 100644 --- a/apps/landing/content/docs/api-reference/events.mdx +++ b/apps/landing/content/docs/api-reference/events.mdx @@ -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. --- diff --git a/apps/landing/content/docs/api-reference/server-http.mdx b/apps/landing/content/docs/api-reference/server-http.mdx index 55cccdfbd..92885c5e9 100644 --- a/apps/landing/content/docs/api-reference/server-http.mdx +++ b/apps/landing/content/docs/api-reference/server-http.mdx @@ -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 header — for 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` diff --git a/apps/landing/content/docs/api-reference/upupuploader/event-handlers.mdx b/apps/landing/content/docs/api-reference/upupuploader/event-handlers.mdx index e2ca3fa87..02c8c7c77 100644 --- a/apps/landing/content/docs/api-reference/upupuploader/event-handlers.mdx +++ b/apps/landing/content/docs/api-reference/upupuploader/event-handlers.mdx @@ -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. diff --git a/apps/landing/content/docs/api-reference/upupuploader/optional-props.mdx b/apps/landing/content/docs/api-reference/upupuploader/optional-props.mdx index 726964c3a..b4b90eba0 100644 --- a/apps/landing/content/docs/api-reference/upupuploader/optional-props.mdx +++ b/apps/landing/content/docs/api-reference/upupuploader/optional-props.mdx @@ -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/). diff --git a/apps/landing/content/docs/comparisons/upup-vs-uploadthing.mdx b/apps/landing/content/docs/comparisons/upup-vs-uploadthing.mdx index cdcdab006..b78054e9b 100644 --- a/apps/landing/content/docs/comparisons/upup-vs-uploadthing.mdx +++ b/apps/landing/content/docs/comparisons/upup-vs-uploadthing.mdx @@ -1,6 +1,6 @@ --- -title: upup vs UploadThing -description: An honest, factual comparison of upup and UploadThing — a self-hosted MIT library versus a managed hosted upload service — so you can choose the right model. +title: upup vs UploadThing — Self-Hosted Alternative +description: A factual upup vs UploadThing comparison — a free, MIT-licensed uploader you self-host against your own S3 bucket versus a managed, hosted upload service. --- UploadThing (by Ping Labs) is a hosted file-upload **service** for full-stack diff --git a/apps/landing/content/docs/comparisons/upup-vs-uppy.mdx b/apps/landing/content/docs/comparisons/upup-vs-uppy.mdx index 9a3ae779e..28db1aeb7 100644 --- a/apps/landing/content/docs/comparisons/upup-vs-uppy.mdx +++ b/apps/landing/content/docs/comparisons/upup-vs-uppy.mdx @@ -1,6 +1,6 @@ --- -title: upup vs Uppy -description: An honest, factual comparison of upup and Uppy — two MIT-licensed file uploaders with headless cores, cloud drives, and resumable uploads — to help you choose. +title: upup vs Uppy — Which Uploader to Choose +description: A factual upup vs Uppy comparison — two free, MIT-licensed file uploaders with headless cores, cloud drives and resumable uploads, and where they differ. --- Uppy (by Transloadit) is the most established open-source JavaScript uploader, diff --git a/apps/landing/content/docs/guides/accessibility.mdx b/apps/landing/content/docs/guides/accessibility.mdx index 68b377b76..2938a0cd6 100644 --- a/apps/landing/content/docs/guides/accessibility.mdx +++ b/apps/landing/content/docs/guides/accessibility.mdx @@ -1,6 +1,6 @@ --- title: Accessibility -description: What upup's uploader ships by default — keyboard interaction, ARIA roles and live regions, focus management on overlays, the animations prop and prefers-reduced-motion handling — and the parts that stay your responsibility. +description: What upup ships by default — keyboard interaction, ARIA roles and live regions, focus management on overlays, and prefers-reduced-motion handling. --- upup's accessibility semantics are not an opt-in layer. Roles, labels, live diff --git a/apps/landing/content/docs/guides/auth/next-auth.mdx b/apps/landing/content/docs/guides/auth/next-auth.mdx index 0da7901e6..a207504e7 100644 --- a/apps/landing/content/docs/guides/auth/next-auth.mdx +++ b/apps/landing/content/docs/guides/auth/next-auth.mdx @@ -1,6 +1,6 @@ --- title: Authenticate uploads with NextAuth (Auth.js v5) -description: Scope @useupup/server uploads to the NextAuth session — the auth() helper in the App Router, getToken outside a request context, and the missing session.user.id. +description: Scope @useupup/server uploads to the NextAuth session — the auth() helper in the App Router, getToken outside a request, and the missing session.user.id. --- In the App Router, the `auth()` helper exported from your `auth.ts` reads the diff --git a/apps/landing/content/docs/guides/file-processing.mdx b/apps/landing/content/docs/guides/file-processing.mdx index 957b1b0bb..536c92847 100644 --- a/apps/landing/content/docs/guides/file-processing.mdx +++ b/apps/landing/content/docs/guides/file-processing.mdx @@ -1,6 +1,6 @@ --- title: File Processing -description: upup's client-side pipeline — image compression, HEIC to JPEG conversion, EXIF stripping, thumbnails, SHA-256 checksums and deduplication, Web Worker offload, and custom steps. +description: upup client-side pipeline — image compression, HEIC to JPEG conversion, EXIF stripping, thumbnails, SHA-256 checksums, Web Worker offload and custom steps. --- Before a byte leaves the browser, upup can run each file through a processing diff --git a/apps/landing/content/docs/guides/headless.mdx b/apps/landing/content/docs/guides/headless.mdx index bec5bb57c..80506bf80 100644 --- a/apps/landing/content/docs/guides/headless.mdx +++ b/apps/landing/content/docs/guides/headless.mdx @@ -1,6 +1,6 @@ --- title: Headless Usage -description: Build your own uploader UI on top of upup's engine — the useUpupUpload React hook, driving UpupCore directly in any runtime, and the webWorker, HEIC, and resumable pipeline opt-ins. +description: Build your own uploader UI on the upup engine — the useUpupUpload React hook, driving UpupCore directly, and the webWorker, HEIC and resumable opt-ins. --- The upup UI is optional. Under it sits a framework-agnostic engine — diff --git a/apps/landing/content/docs/guides/reliability.mdx b/apps/landing/content/docs/guides/reliability.mdx index 4513fc0e0..324d29041 100644 --- a/apps/landing/content/docs/guides/reliability.mdx +++ b/apps/landing/content/docs/guides/reliability.mdx @@ -1,6 +1,6 @@ --- title: Reliability -description: How upup survives flaky networks — the per-file retry policy and its backoff math, custom success predicates, upload concurrency, online/offline events, and the difference between crash recovery and multipart session resume. +description: How upup survives flaky networks — the per-file retry policy and its backoff math, custom success predicates, upload concurrency, and multipart resume. --- Uploads fail for boring reasons: a tunnel, a dropped VPN, a 502 from a storage diff --git a/apps/landing/content/docs/guides/server-auth.mdx b/apps/landing/content/docs/guides/server-auth.mdx index 7af7e788c..559bd2da8 100644 --- a/apps/landing/content/docs/guides/server-auth.mdx +++ b/apps/landing/content/docs/guides/server-auth.mdx @@ -1,6 +1,6 @@ --- title: Server Auth & Trust Model -description: How @useupup/server authenticates uploads — the mandatory HMAC upload-token secret, the secure-by-default 403 on anonymous uploads, per-user key scoping, and what forged requests get. +description: How @useupup/server authenticates uploads — the mandatory HMAC upload-token secret, the secure-by-default 403 on anonymous uploads, and per-user scoping. --- `@useupup/server` is the trust boundary between the browser and your storage. diff --git a/apps/landing/content/docs/guides/server-mode-setup.mdx b/apps/landing/content/docs/guides/server-mode-setup.mdx index e0f5f425a..f4e2e281d 100644 --- a/apps/landing/content/docs/guides/server-mode-setup.mdx +++ b/apps/landing/content/docs/guides/server-mode-setup.mdx @@ -1,6 +1,6 @@ --- -title: Server Mode — Setup -description: Set up mode="server" — mount createUpupHandler, configure storage, secrets, limits and hooks, then pick an adapter — Express, Fastify, Hono, or Next.js. +title: Server Mode Setup — Upload Through Your Server +description: Set up upup server mode — mount createUpupHandler, configure S3 storage, secrets, limits and hooks, then pick an Express, Fastify, Hono or Next.js adapter. --- End-to-end setup for `mode="server"`. In server mode the browser talks only diff --git a/apps/landing/content/docs/guides/sources.mdx b/apps/landing/content/docs/guides/sources.mdx index 63e3ef5d5..5c928fb98 100644 --- a/apps/landing/content/docs/guides/sources.mdx +++ b/apps/landing/content/docs/guides/sources.mdx @@ -1,6 +1,6 @@ --- title: Upload Sources -description: Every input the uploader ships — local files, drag-and-drop, paste, folder upload, camera, microphone, screen capture, URL import, and cloud drives — plus the sources prop that decides which chips appear. +description: Every input the uploader ships — local files, drag and drop, paste, folder upload, camera, microphone, screen capture, URL import, and cloud drives. --- upup collects files from nine sources. One prop — `sources` — decides which of diff --git a/apps/landing/content/docs/guides/storage-providers.mdx b/apps/landing/content/docs/guides/storage-providers.mdx index 75c436727..9fab035ad 100644 --- a/apps/landing/content/docs/guides/storage-providers.mdx +++ b/apps/landing/content/docs/guides/storage-providers.mdx @@ -1,6 +1,6 @@ --- title: Storage Providers -description: Connect upup to any S3-compatible storage — AWS S3, Cloudflare R2, MinIO, Backblaze B2, DigitalOcean Spaces, Wasabi, and more — with the full provider list, the shared storage config shape, and a setup guide per provider. +description: Connect upup to any S3-compatible storage — AWS S3, Cloudflare R2, MinIO, Backblaze B2, DigitalOcean Spaces, Wasabi and more, with a guide per provider. --- upup uploads to any **S3-compatible** object store. One uploader UI, one config diff --git a/apps/landing/content/docs/guides/storage/azure-blob.mdx b/apps/landing/content/docs/guides/storage/azure-blob.mdx index 59020cd2c..b5a64b856 100644 --- a/apps/landing/content/docs/guides/storage/azure-blob.mdx +++ b/apps/landing/content/docs/guides/storage/azure-blob.mdx @@ -1,6 +1,6 @@ --- -title: Upload files to Azure Blob Storage -description: Azure Blob Storage has no S3 API, so @useupup/server rejects it. Upload to Azure from React, Vue, Svelte, or plain JS with upup via client mode and a SAS URL. +title: Upload Files to Azure Blob Storage with a SAS URL +description: Upload files to Azure Blob Storage from the browser with upup — a server-issued SAS URL, the mandatory x-ms-blob-type PUT header, no account key in the client. --- Azure Blob Storage is the one provider in upup's list that server mode cannot diff --git a/apps/landing/content/docs/guides/storage/cloudflare-r2.mdx b/apps/landing/content/docs/guides/storage/cloudflare-r2.mdx index 58283b2ec..6c936629c 100644 --- a/apps/landing/content/docs/guides/storage/cloudflare-r2.mdx +++ b/apps/landing/content/docs/guides/storage/cloudflare-r2.mdx @@ -1,6 +1,6 @@ --- -title: Upload files to Cloudflare R2 -description: Upload files to Cloudflare R2 from React, Vue, Svelte, or plain JS with upup — account-scoped endpoint, region auto, API tokens, CORS, and a copy-paste config. +title: Upload Files to Cloudflare R2 from the Browser +description: Upload files to Cloudflare R2 with upup — the account-scoped endpoint, region auto, API tokens, CORS, and a copy-paste React, Vue or plain JS config. --- Cloudflare R2 speaks the S3 API with zero egress fees, which makes it a common diff --git a/apps/landing/content/docs/guides/storage/digitalocean-spaces.mdx b/apps/landing/content/docs/guides/storage/digitalocean-spaces.mdx index 344c2dc83..a41378f99 100644 --- a/apps/landing/content/docs/guides/storage/digitalocean-spaces.mdx +++ b/apps/landing/content/docs/guides/storage/digitalocean-spaces.mdx @@ -1,6 +1,6 @@ --- -title: Upload files to DigitalOcean Spaces -description: Upload files to DigitalOcean Spaces from React, Vue, Svelte, or plain JS with upup — Spaces keys, the region endpoint, CORS, the CDN, and a copy-paste config. +title: Upload Files to DigitalOcean Spaces from the Browser +description: Upload files to DigitalOcean Spaces with upup — Spaces access keys, the regional S3 endpoint, CORS, the CDN, and a copy-paste React, Vue or plain JS config. --- DigitalOcean Spaces is S3-compatible object storage where the datacenter region diff --git a/apps/landing/content/docs/guides/storage/minio.mdx b/apps/landing/content/docs/guides/storage/minio.mdx index 70ece3386..2611292bf 100644 --- a/apps/landing/content/docs/guides/storage/minio.mdx +++ b/apps/landing/content/docs/guides/storage/minio.mdx @@ -1,6 +1,6 @@ --- -title: Upload files to MinIO -description: Upload files to a self-hosted MinIO server from React, Vue, Svelte, or plain JS with upup — Docker setup, path-style addressing, CORS, and a copy-paste config. +title: Upload Files to MinIO from the Browser +description: Upload files to a self-hosted MinIO server with upup — Docker setup, path-style addressing, CORS, and a copy-paste React, Vue, Svelte or plain JS config. --- MinIO is self-hosted S3-compatible storage, which makes it the fastest way to diff --git a/apps/landing/content/docs/guides/theming.mdx b/apps/landing/content/docs/guides/theming.mdx index c4dffc8a5..d8c1255ef 100644 --- a/apps/landing/content/docs/guides/theming.mdx +++ b/apps/landing/content/docs/guides/theming.mdx @@ -1,6 +1,6 @@ --- title: Theming -description: Style the upup uploader with the theme prop — light/dark/system modes, design tokens exposed as CSS variables, per-component slot class overrides, and the stable data-upup-slot DOM hooks for raw CSS. +description: Style the upup uploader with the theme prop — light, dark and system modes, design tokens as CSS variables, slot class overrides, and data-upup-slot hooks. --- upup gives you three layers of control, from coarse to surgical: diff --git a/apps/landing/content/docs/index.mdx b/apps/landing/content/docs/index.mdx index 20ae27dc8..a57221572 100644 --- a/apps/landing/content/docs/index.mdx +++ b/apps/landing/content/docs/index.mdx @@ -1,6 +1,6 @@ --- -title: upup Documentation -description: A file uploader with a native UI for React, Vue, Svelte, Angular, Vanilla JS, and Preact, built on a shared headless core with an optional server mode. +title: File Uploader Documentation +description: Docs for upup, the free and open-source file uploader with native UI for React, Vue, Svelte, Angular, Vanilla JS and Preact, plus an optional server mode. --- upup is a file uploader with a native UI for **React, Vue, Svelte, Angular, diff --git a/apps/landing/content/docs/localization.mdx b/apps/landing/content/docs/localization.mdx index 8f9fe84b4..3d7db37a2 100644 --- a/apps/landing/content/docs/localization.mdx +++ b/apps/landing/content/docs/localization.mdx @@ -1,6 +1,6 @@ --- title: Localization (i18n) -description: upup ships nine ICU locale bundles from @useupup/core/i18n — set the locale on any framework's uploader, override individual namespaced message keys, and get automatic RTL and pluralization. +description: upup ships nine ICU locale bundles from @useupup/core/i18n — set the locale on any framework uploader, override message keys, and get RTL and plurals. --- upup uses ICU locale bundles from `@useupup/core/i18n`. Messages are grouped diff --git a/apps/landing/content/docs/migration/v1-to-v3.mdx b/apps/landing/content/docs/migration/v1-to-v3.mdx index a232bc85e..a45633351 100644 --- a/apps/landing/content/docs/migration/v1-to-v3.mdx +++ b/apps/landing/content/docs/migration/v1-to-v3.mdx @@ -1,6 +1,6 @@ --- title: Migrating from v1 to v3 -description: Upgrade from upup v1 (upup-react-file-uploader) to v3 — the package rename to @useupup/react, the full v1→v3 prop map, the UpupError/UpupErrorCode taxonomy, and client- vs server-mode uploads. +description: Upgrade from upup v1 (upup-react-file-uploader) to v3 — the rename to @useupup/react, the v1 to v3 prop map, the error taxonomy, and client vs server mode. --- v1 shipped as a single React package, `upup-react-file-uploader`. v3 is a diff --git a/apps/landing/content/docs/quickstarts/angular.mdx b/apps/landing/content/docs/quickstarts/angular.mdx index 2ea2d05e6..2f0bb1895 100644 --- a/apps/landing/content/docs/quickstarts/angular.mdx +++ b/apps/landing/content/docs/quickstarts/angular.mdx @@ -1,6 +1,6 @@ --- title: Angular Quickstart -description: Add a full-featured file uploader to an Angular 19+ app with @useupup/angular — a standalone component port of the canonical React UI, with cloud drives, camera, and resumable uploads. +description: Add a file uploader to an Angular 19+ app with @useupup/angular — a standalone component, drag and drop, cloud drives, camera, and resumable uploads. --- `@useupup/angular` is a native Angular 19+ port of the canonical upup React UI — diff --git a/apps/landing/content/docs/quickstarts/next.mdx b/apps/landing/content/docs/quickstarts/next.mdx index 4166db73b..91e5d0152 100644 --- a/apps/landing/content/docs/quickstarts/next.mdx +++ b/apps/landing/content/docs/quickstarts/next.mdx @@ -1,6 +1,6 @@ --- title: Next.js Quickstart -description: Add a full-featured file uploader to a Next.js app with @useupup/next — the client UI and the server handlers in one install, split so the AWS SDK never reaches your client bundle. +description: Add a file uploader to a Next.js app with @useupup/next — client UI and server handlers in one install, split so the AWS SDK never reaches your bundle. --- `@useupup/next` is the Next.js integration for upup: one install gives you the client diff --git a/apps/landing/content/docs/quickstarts/preact.mdx b/apps/landing/content/docs/quickstarts/preact.mdx index 95ecc8b23..e0eac6a4b 100644 --- a/apps/landing/content/docs/quickstarts/preact.mdx +++ b/apps/landing/content/docs/quickstarts/preact.mdx @@ -1,6 +1,6 @@ --- title: Preact Quickstart -description: Add a full-featured file uploader to a Preact app with @useupup/preact — a preact/compat re-export of @useupup/react, same UI and API, with cloud drives, camera, and resumable uploads. +description: Add a file uploader to a Preact app with @useupup/preact — a preact/compat re-export of @useupup/react, drag and drop, cloud drives, and resumable uploads. --- `@useupup/preact` is a **`preact/compat` re-export of diff --git a/apps/landing/content/docs/quickstarts/react.mdx b/apps/landing/content/docs/quickstarts/react.mdx index 783adb572..f2446a884 100644 --- a/apps/landing/content/docs/quickstarts/react.mdx +++ b/apps/landing/content/docs/quickstarts/react.mdx @@ -1,6 +1,6 @@ --- title: React Quickstart -description: Add a full-featured file uploader to a React 19 app with @useupup/react — drag-and-drop, cloud drives, camera, and resumable uploads, with no upload server to run. +description: Add a file uploader to a React 19 app with @useupup/react — one npm install, drag and drop, cloud drives, camera, and resumable uploads, no server to run. --- `@useupup/react` is the canonical upup UI: a drag-and-drop uploader with file diff --git a/apps/landing/content/docs/quickstarts/svelte.mdx b/apps/landing/content/docs/quickstarts/svelte.mdx index 33048de38..2cd08f5a2 100644 --- a/apps/landing/content/docs/quickstarts/svelte.mdx +++ b/apps/landing/content/docs/quickstarts/svelte.mdx @@ -1,6 +1,6 @@ --- title: Svelte Quickstart -description: Add a full-featured file uploader to a Svelte 5 app with @useupup/svelte — a native port of the canonical React UI, DOM-identical, with cloud drives, camera, and resumable uploads. +description: Add a file uploader to a Svelte 5 app with @useupup/svelte — one npm install, drag and drop, cloud drives, camera, and resumable uploads to any S3 storage. --- `@useupup/svelte` is a native Svelte 5 port of the canonical upup React UI — diff --git a/apps/landing/content/docs/quickstarts/vanilla.mdx b/apps/landing/content/docs/quickstarts/vanilla.mdx index 241fbe9a3..556fbf4bb 100644 --- a/apps/landing/content/docs/quickstarts/vanilla.mdx +++ b/apps/landing/content/docs/quickstarts/vanilla.mdx @@ -1,6 +1,6 @@ --- title: Vanilla JS Quickstart -description: Add a full-featured file uploader to any page with @useupup/vanilla — framework-free, DOM-identical to the canonical React UI, with cloud drives, camera, and resumable uploads. +description: Add a file uploader to any page with @useupup/vanilla — framework-free, drag and drop, cloud drives, camera, and resumable uploads to any S3 storage. --- `@useupup/vanilla` is the framework-free upup uploader (built on lit-html) — diff --git a/apps/landing/content/docs/quickstarts/vue.mdx b/apps/landing/content/docs/quickstarts/vue.mdx index f92eb906c..119b18b9d 100644 --- a/apps/landing/content/docs/quickstarts/vue.mdx +++ b/apps/landing/content/docs/quickstarts/vue.mdx @@ -1,6 +1,6 @@ --- title: Vue Quickstart -description: Add a full-featured file uploader to a Vue 3 app with @useupup/vue — a native port of the canonical React UI, DOM-identical, with cloud drives, camera, and resumable uploads. +description: Add a file uploader to a Vue 3 app with @useupup/vue — one npm install, drag and drop, cloud drives, camera, and resumable uploads to any S3 storage. --- `@useupup/vue` is a native Vue 3 port of the canonical upup React UI — DOM-identical diff --git a/apps/landing/content/docs/resumable-uploads.mdx b/apps/landing/content/docs/resumable-uploads.mdx index 4a4500021..ba69b259a 100644 --- a/apps/landing/content/docs/resumable-uploads.mdx +++ b/apps/landing/content/docs/resumable-uploads.mdx @@ -1,6 +1,6 @@ --- -title: Resumable Uploads -description: Two resumable protocols — S3 multipart through @useupup/server, or tus against an external tus-compatible service. Full option reference, part-size rules, and what each protocol actually resumes. +title: Resumable Uploads — S3 Multipart & tus +description: Resumable file uploads in upup — S3 multipart through @useupup/server or the tus protocol, with part-size rules and what each protocol actually resumes. --- upup supports two resumable protocols. Both are opt-in through the single diff --git a/apps/landing/src/__tests__/seo-copy-budgets.test.ts b/apps/landing/src/__tests__/seo-copy-budgets.test.ts new file mode 100644 index 000000000..9f391aa53 --- /dev/null +++ b/apps/landing/src/__tests__/seo-copy-budgets.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, it } from 'vitest' +import { generateMetadata as frameworkMetadata } from '@/app/[framework]/page' +import { metadata as homeMetadata } from '@/app/page' +import { FRAMEWORK_IDS, FRAMEWORKS } from '@/lib/frameworks' +import { siteConfig } from '@/lib/siteConfig' +import { source } from '@/lib/docs/source' + +// Search Console (90 days to 2026-09-08, sc-domain:useupup.com) showed the +// money pages earning impressions and almost no clicks with titles Google +// truncated and descriptions two to three times the width a SERP renders. +// These are the widths that actually display, so a future copy edit that +// blows past them fails here instead of silently costing clicks. +const MAX_TITLE = 60 +const MAX_DOCS_TITLE = 65 // frontmatter title + the " | upup docs" template +const DOCS_TITLE_TEMPLATE_SUFFIX = ' | upup docs' +const MIN_DESCRIPTION = 120 +const MAX_DESCRIPTION = 160 + +// A separate PR removes this claim from the marketing copy; nothing may +// reintroduce it through a title or a description. +const RETIRED_CLAIM = 'byte-identical' + +function textOf(value: unknown): string { + return typeof value === 'string' ? value : '' +} + +describe('home page search-result copy', () => { + it('renders a title short enough to survive SERP truncation', () => { + expect(homeMetadata.title.length).toBeLessThanOrEqual(MAX_TITLE) + }) + + it('leads the title with the brand, which is the site top query', () => { + expect(homeMetadata.title.startsWith('upup')).toBe(true) + }) + + it('renders a description inside the width Google displays', () => { + expect(homeMetadata.description.length).toBeGreaterThanOrEqual( + MIN_DESCRIPTION, + ) + expect(homeMetadata.description.length).toBeLessThanOrEqual( + MAX_DESCRIPTION, + ) + }) + + it('keeps the site-wide fallback title and tagline inside the same budgets', () => { + // layout.tsx serves these to every route that declares no metadata of + // its own, so they are search-result copy too. + expect(siteConfig.title.length).toBeLessThanOrEqual(MAX_TITLE) + expect(siteConfig.tagline.length).toBeGreaterThanOrEqual( + MIN_DESCRIPTION, + ) + expect(siteConfig.tagline.length).toBeLessThanOrEqual(MAX_DESCRIPTION) + }) +}) + +describe('framework page search-result copy', () => { + it.each(FRAMEWORK_IDS)( + 'gives /%s/ a title inside the 60-character budget', + async id => { + const meta = await frameworkMetadata({ + params: Promise.resolve({ framework: id }), + }) + expect(textOf(meta.title).length).toBeLessThanOrEqual(MAX_TITLE) + }, + ) + + it.each(FRAMEWORK_IDS)( + 'leads the /%s/ title with the phrase the impressions arrive on', + async id => { + const meta = await frameworkMetadata({ + params: Promise.resolve({ framework: id }), + }) + // "vue file uploader", "react uploader", "angular file uploader" — + // the query language has to be in front of the brand, not behind it. + expect(textOf(meta.title)).toContain( + `${FRAMEWORKS[id].name} File Uploader`, + ) + }, + ) + + it.each(FRAMEWORK_IDS)( + 'gives /%s/ a description between 120 and 160 characters', + async id => { + const meta = await frameworkMetadata({ + params: Promise.resolve({ framework: id }), + }) + const description = textOf(meta.description) + expect(description.length).toBeGreaterThanOrEqual(MIN_DESCRIPTION) + expect(description.length).toBeLessThanOrEqual(MAX_DESCRIPTION) + }, + ) + + it.each(FRAMEWORK_IDS)( + 'names the install command for /%s/ in its description', + async id => { + const meta = await frameworkMetadata({ + params: Promise.resolve({ framework: id }), + }) + expect(textOf(meta.description)).toContain( + `npm install ${FRAMEWORKS[id].pkg}`, + ) + }, + ) +}) + +describe('docs frontmatter search-result copy', () => { + it('keeps every docs title inside 65 characters once the template is appended', () => { + const overBudget = source + .getPages() + .map(page => ({ + url: page.url, + rendered: `${page.data.title}${DOCS_TITLE_TEMPLATE_SUFFIX}`, + })) + .filter(entry => entry.rendered.length > MAX_DOCS_TITLE) + .map(entry => `${entry.url} (${entry.rendered.length})`) + expect(overBudget).toEqual([]) + }) + + it('keeps every docs description that exists inside 160 characters', () => { + const overBudget = source + .getPages() + .map(page => ({ + url: page.url, + description: textOf(page.data.description), + })) + .filter(entry => entry.description.length > MAX_DESCRIPTION) + .map(entry => `${entry.url} (${entry.description.length})`) + expect(overBudget).toEqual([]) + }) +}) + +describe('retired parity claim', () => { + it('is absent from every title and description the site emits', async () => { + const copy: string[] = [ + homeMetadata.title, + homeMetadata.description, + siteConfig.title, + siteConfig.tagline, + ] + for (const id of FRAMEWORK_IDS) { + const meta = await frameworkMetadata({ + params: Promise.resolve({ framework: id }), + }) + copy.push(textOf(meta.title), textOf(meta.description)) + } + for (const page of source.getPages()) { + copy.push(page.data.title, textOf(page.data.description)) + } + const offenders = copy.filter(text => + text.toLowerCase().includes(RETIRED_CLAIM), + ) + expect(offenders).toEqual([]) + }) +}) diff --git a/apps/landing/src/app/[framework]/page.tsx b/apps/landing/src/app/[framework]/page.tsx index 000eb7825..dcec5fb53 100644 --- a/apps/landing/src/app/[framework]/page.tsx +++ b/apps/landing/src/app/[framework]/page.tsx @@ -31,8 +31,13 @@ export async function generateMetadata({ const fw = getFramework(framework) if (!fw) return {} - const title = `${fw.name} File Uploader — upup` - const description = `${fw.tagline} Open-source & MIT-licensed — npm install ${fw.pkg}.` + // " File Uploader" leads because that is the phrase the + // impressions arrive on ("vue file uploader", "react uploader", + // "angular file uploader"); the brand trails. Widest case is + // "Vanilla JS" at 57 chars — the ≤60 title and ≤155 description budgets + // are pinned by src/__tests__/seo-copy-budgets.test.ts. + const title = `${fw.name} File Uploader – Open-Source Drag & Drop | upup` + const description = fw.tagline const url = canonicalUrl(fw.id) const image = `${siteUrl()}/img/social-card.png` diff --git a/apps/landing/src/app/layout.tsx b/apps/landing/src/app/layout.tsx index bd1bfaac8..9751b1a43 100644 --- a/apps/landing/src/app/layout.tsx +++ b/apps/landing/src/app/layout.tsx @@ -35,7 +35,7 @@ export const metadata: Metadata = { title: siteConfig.title, description: siteConfig.tagline, openGraph: { - title: 'upup – One File Uploader for Every Framework', + title: 'upup – Open-Source File Uploader for Every Framework', description: 'One open-source file uploader with a headless core and native UI for React, Vue, Svelte, Angular, Vanilla JS, and Preact. Cloud drives, camera, screen capture, and secure server-mode uploads to any S3-compatible storage. MIT-licensed.', images: [`${siteUrl()}/img/social-card.png`], @@ -45,7 +45,7 @@ export const metadata: Metadata = { }, twitter: { card: 'summary_large_image', - title: 'upup – One File Uploader for Every Framework', + title: 'upup – Open-Source File Uploader for Every Framework', description: 'One uploader, native UI for React, Vue, Svelte, Angular, Vanilla JS & Preact. Headless core, cloud drives, and secure server-mode uploads to any S3-compatible storage. Open-source, MIT.', images: [`${siteUrl()}/img/social-card.png`], diff --git a/apps/landing/src/app/page.tsx b/apps/landing/src/app/page.tsx index 8fdf78b71..23dcbaf8a 100644 --- a/apps/landing/src/app/page.tsx +++ b/apps/landing/src/app/page.tsx @@ -11,10 +11,13 @@ import FAQSection from '@/components/FAQSection' import Section from '@/components/ui/Section' import { canonicalUrl } from '@/lib/site-url' +// Title is brand-first and ≤60 chars ("upup" is the site's biggest query and +// the old 77-char title truncated in SERPs); description is ≤155 so it renders +// whole. Both budgets are pinned by src/__tests__/seo-copy-budgets.test.ts. export const metadata = { - title: 'upup – One File Uploader for React, Vue, Svelte, Angular, Vanilla JS & Preact', + title: 'upup – Open-Source File Uploader for Every Framework', description: - 'One open-source, MIT-licensed file uploader with a headless core and byte-identical native UI for six frameworks. Drag-and-drop, cloud drives (Google Drive, OneDrive, Dropbox, Box), camera, screen capture, and secure server-mode uploads to S3-compatible storage.', + 'upup is a free, open-source drag and drop file uploader with native UI for React, Vue, Svelte, Angular, Vanilla JS and Preact. MIT-licensed.', alternates: { canonical: canonicalUrl(), }, diff --git a/apps/landing/src/components/HomepageHero/index.tsx b/apps/landing/src/components/HomepageHero/index.tsx index ad56cb2e7..7fc558dd7 100644 --- a/apps/landing/src/components/HomepageHero/index.tsx +++ b/apps/landing/src/components/HomepageHero/index.tsx @@ -227,7 +227,11 @@ export default function HeroSection({ variants={headingVariants} > - {fw?.name ?? 'Every Framework'} + {fw + ? 'One core, every framework' + : 'for Every Framework'} @@ -259,8 +265,8 @@ export default function HeroSection({ className="text-base sm:text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed max-w-xl" variants={subtitleVariants} > - A drag-and-drop file uploader with a headless core - and native UI for{' '} + A free, open-source drag-and-drop file uploader with + a headless core and native UI for{' '} {fw?.name ?? 'React, Vue, Svelte, Angular, Vanilla JS & Preact'} @@ -307,6 +313,21 @@ export default function HeroSection({ + + {/* Framework pages only: the shortest path from + this page to working code is that framework's + own quickstart doc (/docs/quickstarts//). + Plain Link — no motion wrapper — so the CTA row + stays trivially mergeable. */} + {fw && ( + + {fw.name} Quickstart + + + )} {/* Install Command with Package Manager Select — the page's diff --git a/apps/landing/src/lib/frameworks.tsx b/apps/landing/src/lib/frameworks.tsx index d308f5dd1..f91c41004 100644 --- a/apps/landing/src/lib/frameworks.tsx +++ b/apps/landing/src/lib/frameworks.tsx @@ -46,7 +46,11 @@ export interface FrameworkMeta { docsLang: string /** The image editor is a real-React island — React/Preact only. */ hasImageEditor: boolean - /** Framework-specific hero sub-line. */ + /** + * The page's meta description — query-led (" file uploader", "free", + * "open-source", "drag and drop", "npm install ") and held to + * 120–155 chars by src/__tests__/seo-copy-budgets.test.ts. + */ tagline: string /** Official brand logo (react-icons/si). */ Icon: IconType @@ -118,7 +122,7 @@ export const FRAMEWORKS: Record = { docsLang: 'tsx', hasImageEditor: true, tagline: - 'A headless-core React file uploader with drag & drop, cloud drives, an image editor, and secure server-mode uploads.', + 'Free, open-source React file uploader with drag and drop, cloud drives, an image editor and resumable S3 uploads — npm install @useupup/react.', Icon: SiReact, brand: '#61DAFB', }, @@ -132,7 +136,7 @@ export const FRAMEWORKS: Record = { docsLang: 'vue', hasImageEditor: false, tagline: - 'A native Vue file uploader — the same headless core and UI as React, drag & drop, cloud drives, and secure server-mode uploads.', + 'Free, open-source Vue file uploader with drag and drop, cloud drives and resumable uploads to any S3 storage — npm install @useupup/vue.', Icon: SiVuedotjs, brand: '#42B883', }, @@ -146,7 +150,7 @@ export const FRAMEWORKS: Record = { docsLang: 'svelte', hasImageEditor: false, tagline: - 'A native Svelte file uploader — the same headless core and UI as React, drag & drop, cloud drives, and secure server-mode uploads.', + 'Free, open-source Svelte file uploader with drag and drop, cloud drives and resumable uploads to any S3 storage — npm install @useupup/svelte.', Icon: SiSvelte, brand: '#FF3E00', }, @@ -160,7 +164,7 @@ export const FRAMEWORKS: Record = { docsLang: 'ts', hasImageEditor: false, tagline: - 'A native Angular file uploader — the same headless core and UI as React, drag & drop, cloud drives, and secure server-mode uploads.', + 'Free, open-source Angular file uploader with drag and drop, cloud drives and resumable uploads to any S3 storage — npm install @useupup/angular.', Icon: SiAngular, brand: '#DD0031', }, @@ -174,7 +178,7 @@ export const FRAMEWORKS: Record = { docsLang: 'ts', hasImageEditor: false, tagline: - 'A framework-free file uploader for plain JavaScript & TypeScript — drag & drop, cloud drives, and secure server-mode uploads.', + 'Free, open-source file uploader for plain JavaScript and TypeScript — drag and drop, cloud drives, resumable S3 uploads. npm install @useupup/vanilla.', Icon: SiJavascript, brand: '#F7DF1E', }, @@ -188,7 +192,7 @@ export const FRAMEWORKS: Record = { docsLang: 'tsx', hasImageEditor: true, tagline: - 'A Preact file uploader (React-compatible) with drag & drop, cloud drives, an image editor, and secure server-mode uploads.', + 'Free, open-source Preact file uploader with drag and drop, cloud drives, an image editor and resumable S3 uploads — npm install @useupup/preact.', Icon: SiPreact, brand: '#673AB8', }, diff --git a/apps/landing/src/lib/siteConfig.ts b/apps/landing/src/lib/siteConfig.ts index 21280af73..6c07775ec 100644 --- a/apps/landing/src/lib/siteConfig.ts +++ b/apps/landing/src/lib/siteConfig.ts @@ -1,5 +1,9 @@ export const siteConfig = { - title: 'upup – One File Uploader for Every Framework | Open Source', + // Brand-first and inside the ~60-char SERP budget: "upup" is our own + // highest-impression query and was previously buried behind a 77-char + // title that Google truncated. Length budgets are pinned by + // src/__tests__/seo-copy-budgets.test.ts. + title: 'upup – Open-Source File Uploader for Every Framework', tagline: - 'Open-source file uploader with a headless core and native UI for React, Vue, Svelte, Angular, Vanilla JS, and Preact. Cloud drives, camera, screen capture, and secure server-mode uploads to any S3-compatible storage.', + 'Free and open-source (MIT) drag and drop file uploader for React, Vue, Svelte, Angular, Vanilla JS and Preact, with resumable uploads to S3.', }