chore(release): promote dev to master (landing first-load JS halved, #450) - #451
Conversation
chore: sync master back to dev
The /agent-setup/ index and the four per-agent guides shipped meta
descriptions of 163-200 characters, which Google truncates
mid-sentence. The copy is rewritten to 145-159 characters and still
names the agent, the package, the mount, the presign route and the
context file.
seo-copy-budgets.test.ts now holds these pages to the same budgets as
the home, framework and docs pages (title <= 60, description 120-160).
RED before the copy change:
x keeps the /agent-setup/ title and description inside the budgets
AssertionError: expected 163 to be less than or equal to 160
x gives /agent-setup/claude-code/ ... expected 192 ...
x gives /agent-setup/codex/ ... expected 186 ...
(cursor, opencode likewise)
GREEN after: landing vitest 13 files / 137 tests passed.
fix(landing): agent-setup meta descriptions fit the SERP width
…y roundups, Azure SAS tutorial
Server mode builds ServerCredentials and uses the same DirectUpload strategy as client mode (packages/core/src/resolve-upload-config.ts): @useupup/server answers /presign and /multipart/*, and the browser PUTs the bytes straight to the bucket. The only server -> storage byte path is the cloud-drive transfer (bounded 5 MiB parts). onFileUploaded fires only on multipart-complete and drive transfer, onUploadComplete only on multipart-complete; a direct presigned PUT never reaches the server on completion. Many surfaces said server mode "proxies uploads", that "the browser talks only to your server", or that the server "writes bytes to storage". Corrected: - guides/modes.mdx: server-mode section, text diagram (local-file presign -> PUT-to-bucket flow plus the drive list/transfer flow), bullets (admission via hooks.onBeforeUpload, server-chosen keys, limits before a URL is issued, drive secrets off the browser, drive files streamed server -> bucket), the hook-coverage caveat, and the comparison table (local/drive byte rows; multipart is server-mode only; both modes refresh drive tokens) - ModesDiagram: the accent byte hop now runs browser -> storage in both rows; server mode adds the drive -> server -> storage path - FeatureShowcase card + vignette: order is now Your Server -> Browser -> S3 - faq.mdx + lib/faqs.ts (homepage FAQPage JSON-LD): client-vs-server and CORS answers; the CORS answer now covers both modes - storage-providers, plugins, quickstarts/next, optional-props mode, server-mode-setup (title + intro), v1-to-v3, three comparison pages, ai-assistants context block + agent-setup config/prompt, root README, packages/server README, release-notes install comment claim-integrity.test.ts gains a negative assertion over the landing copy sources, the agent-setup prompt/config and every docs .mdx page. Proven RED by re-injecting a retired phrase into each path: src/components/FeatureShowcase/index.tsx says "proxies uploads" content/docs/guides/plugins.mdx says "browser talks only to"
… PostHog after load, LazyMotion)
perf(landing): halve the home page first-load JS (zod off the client, PostHog after load, LazyMotion)
Review or Edit in CodeSandboxOpen the branch in Web Editor • VS Code • Insiders |
Resolve vignettes.tsx: keep this branch's 403 comment and dev's LazyMotion m.span (#450).
docs(landing): UploadThing-vs-S3 rebuild, comparison hub, best-library roundups, Azure SAS tutorial
feat(landing): unique content, FAQs and breadcrumbs per framework page
|
Heads-up: I merged two green SEO PRs into
Both had |
docs: server mode signs uploads (browser PUTs to the bucket); bucket CORS needed in both modes
|
One more merged into |
PostHog session replay and heatmaps cover what Hotjar did, so the landing site no longer loads the Hotjar recorder in production. The privacy page never named Hotjar, so it needs no change. Split out of #444 so it can land without the MinIO e2e suite. #444 keeps the vocab:check rule that retires the token in every layer.
#445 cannot merge while it touches apps/e2e-test/ and scripts/docs/: the e2e resolver routes those paths to the MinIO suite, which cannot pull its private GHCR images and is red for every PR that routes it. This commit is the part of #445 that lives entirely under apps/landing/, so it routes only the docs e2e suite and can merge now. - Site entity graph: WebSite gains alternateName (useupup, upup file uploader, useupup.com) to separate it from other "upup" sites; Organization sameAs lists all nine @useupup/* npm packages; a new SoftwareSourceCode node describes the repository. - Docs FAQPage: an optional `faq: [{ q, a }]` frontmatter field, and DocsStructuredData emits a FAQPage node only on pages that set it. faq blocks on faq.mdx, the comparisons hub, the UploadThing page, the React/Vue/Angular roundups and the Azure Blob guide. The new docs-structured-data test pins every entry to the visible Q&A. - /docs/<slug>.md and /docs.md rewrite to the markdown twin, the URL shape agents guess first. - llms.ts maps a nested index.mdx to its folder slug. On dev today, comparisons/index.mdx is listed in llms.txt as /docs/comparisons/index/ and its markdown twin (/docs-md/comparisons/, behind "Copy page") 404s. No docs pages are added, so the page pins stay at 69 (sitemap 83). Stays in #445: the guides/, guides/storage/ and quickstarts/ hub pages and the guides meta.json title; the /documentation/comparisons and /documentation/quickstarts redirect retarget; the check-links nested-index fix with its fixtures and test; the docs.spec.ts redirect assertions; and the hub-only tests (sitemap hub list, breadcrumb middle crumbs).
…it from #449) The deleted Docusaurus app (apps/docs, removed in 59b56df) served its docs under baseUrl /documentation/ in two route shapes. The older one, routeBasePath 'docs' (2025-11 until 00eaa6b, 2026-07-12), put a page at /documentation/docs/<slug>. The newer one put it at /documentation/<slug>. next.config.mjs only mapped the newer shape, so its /documentation/:path* wildcard captured an older-shape URL as :path* = docs/<slug> and sent it to /docs/docs/<slug>/, a 404. Search Console still shows impressions on those URLs (event-handlers, icon-prop, error-handling, getting-started), so their ranking signal was landing on 404s. Fix, the next.config.mjs hunks of #449 unchanged: five rules ahead of the wildcard. They cover the bare /documentation/docs, the renamed migration/v2-to-v2.1, the two generated-index category pages, and then /documentation/docs/:path* -> /docs/:path*/, which drops the extra docs segment. The sitemap-rules comment update from #449 rides along (the two stale Search Console sitemap submissions were deleted on 2026-09-26). The one comment line that pointed at scripts/docs/check-links.mjs now points at the test below. Guard: src/__tests__/legacy-docs-redirects.test.ts replays all 80 URLs the Docusaurus app served, bare and trailing-slashed. That is the 36 pages from git ls-tree 59b56df^ -- apps/docs/docs in both shapes, the section indexes, and both bare roots; the frozen list lives in src/__tests__/legacy-docusaurus-urls.ts. The requests go through the redirect list Next actually serves: loadCustomRoutes(), which is the config redirects plus Next's own trailingSlash redirects, compiled and matched the way the router server does it. Every request must: - land on a page that exists under content/docs, served at /docs/<slug>/; - get there through exactly one next.config rule, which is also the last hop (Next's trailingSlash hop may only normalize the request first); - never pass through a /docs/docs/ path; - land on its own page when its slug still has one, not on a fallback. Mutation-checked: pointing the wildcard at /docs/ fails the own-page test, and dropping its trailing slash fails the one-hop test. RED against dev's unfixed next.config.mjs (vitest, apps/landing): ✓ replays all 80 legacy URLs, bare and trailing-slashed × lands every legacy URL of both route shapes on a docs page that exists × reaches that page through exactly one next.config redirect, the last hop × never sends a legacy URL through a /docs/docs/ path × sends a legacy page whose slug still exists to that page, not a fallback × lands the older-shape URLs Search Console still ranks on their exact pages AssertionError: expected [ …(78) ] to deeply equal [] + "/documentation/docs/ -> /docs/docs/: /docs/docs/ is no docs page", + "/documentation/docs/error-handling/ -> /docs/docs/error-handling/: /docs/docs/error-handling/ is no docs page", + "/documentation/docs/getting-started -> /documentation/docs/getting-started/ (trailingSlash) -> /docs/docs/getting-started/: /docs/docs/getting-started/ is no docs page", ... (all 78 older-shape requests; the 82 newer-shape requests pass) AssertionError: expected [ …(2) ] to deeply equal [] + "/documentation/docs/migration/v2-to-v2.1 -> /docs/docs/migration/v2-to-v2.1/ -> /docs/docs/migration/v2-to-v2.1 (trailingSlash)", AssertionError: /documentation/docs/error-handling/: expected [ '/docs/docs/error-handling/' ] to deeply equal [ '/docs/error-handling/' ] Tests 5 failed | 1 passed (6) GREEN after: 6/6; landing suite 197/197. Why split: #449 cannot merge while the MinIO e2e suite cannot pull its GHCR images, and its scripts/** and apps/e2e-test/** paths route that suite. This diff stays inside apps/landing, so the resolver routes only the docs e2e project. Stays in #449: the check-links legacy-URL replay (scripts/docs/check-links.mjs with its node:test cases and fixtures) and the two e2e cases in apps/e2e-test/landing/docs.spec.ts.
#449) The www catch-all answered with `${SITE_BASE}/:path*`. Next strips the trailing slash before matching a source and never re-appends it to an ABSOLUTE destination, so www/angular/ went to apex/angular, and the apex then spent a second 308 adding the slash back. Live on 2026-09-26, https://www.useupup.com/angular and /docs/ai-assistants each took 3 hops: www/x -> www/x/ (trailingSlash), then apex/x (the www rule drops the slash), then apex/x/. Search Console still reports impressions on www URLs, so every one of them paid the extra hop. Fix, the www hunk of #449 unchanged: the http -> https rules at the top of redirects() already solve this with two rules, and the www host now uses the same pattern. An extensionless-path rule (`/:path((?:[^/]+/)*[^/.]+)` -> `${SITE_BASE}/:path/`) restores the slash, the bare-root rule stays, and the `/:path*` catch-all is left for file paths (/llms.txt, /sitemap.xml), which must not gain a slash. Pinned by src/__tests__/host-redirects.test.ts, carried from #449 byte-for-byte. It replays the real redirects() array through Next's own matcher (getPathMatch in strict mode with modifyRouteRegex, then matchHas, then prepareDestination, then url.format for the Location header). It checks that www /angular/ and /docs/ai-assistants/ go to the apex WITH the slash and that the apex URL fires no further rule, that www /llms.txt and /sitemap.xml go to the apex with no slash, and it covers the bare root and the existing http -> https pair. RED against the old www rule on this branch (vitest, apps/landing): × sends an extensionless www page to the apex page with its trailing slash kept ✓ sends a www file path to the apex file with no slash appended ✓ sends the bare www root to the apex root ✓ keeps the trailing slash on pages and adds none to files AssertionError: www /angular/: expected 'https://useupup.com/angular' to be 'https://useupup.com/angular/' // Object.is equality Tests 1 failed | 3 passed (4) GREEN after: 4/4; landing suite 201/201. Split from #449 for the same reason as the previous commit: a diff confined to apps/landing routes only the docs e2e project. Stays in #449: the check-links legacy-URL replay (scripts/docs/**) and the two e2e cases in apps/e2e-test/landing/docs.spec.ts, including the one that sends a www Host header.
chore(landing): remove Hotjar, PostHog replaces it
feat(landing): brand entity schema and docs FAQPage (split from #445)
|
Two more green PRs are now on
Both had |
useReducedMotion() is null during SSR and the real preference on the
client's first render. Flow and CheckCircle gated `initial` on it, so the
server HTML always carried framer-motion's pathLength-0 dash
(pathLength="1" stroke-dasharray="0 1"), while a reduced-motion client
rendered initial={false} with no whileInView. With neither prop there is
no pathLength motion value, so framer-motion never overwrote the server's
dash: every solid arrow and check mark on the docs diagrams stayed
invisible, leaving only the arrowheads.
Server and client now render the same props: initial pathLength 0 and
whileInView pathLength 1. Reduced motion only swaps in a zero-duration
transition, so it jumps to the drawn end state instead of skipping the
animation. The dashed Flow's initial strokeDashoffset is no longer gated
either (it never went invisible, but its server markup also depended on
the preference); it still does not march under reduced motion.
RED - the new test against the unfixed primitives:
x server markup is identical whatever the reduced-motion preference
x reduced motion jumps solid flows and check marks to pathLength 1
v without reduced motion solid flows and check marks still draw over time
v the dashed flow marches only without reduced motion
AssertionError: expected '<div class="not-prose my-8 overflow-x...' to
be '<div class="not-prose my-8 overflow-x...' // Object.is equality
(the reduce=true markup drops pathLength="1" stroke-dashoffset="0"
stroke-dasharray="0 1" that the server markup carries)
AssertionError: expected false to deeply equal { pathLength: +0 }
Test Files 1 failed (1)
Tests 2 failed | 2 passed (4)
GREEN - the same test after the fix:
Test Files 1 passed (1)
Tests 4 passed (4)
Fixes #452
…s-landing fix(landing): legacy /documentation/docs/* URLs reach real pages, www→apex keeps the slash (split from #449)
|
One more on |
Search Console shows /react/, /vue/ and /angular/ collecting "best / most popular <framework> file upload" impressions at positions 43-60 with no clicks, while the docs roundups that answer those queries were linked only from each other and the comparisons hub. Each framework guide now carries a comparisonLink rendered as one sentence under its competitor table: React, Vue and Angular link their own best-<framework>-file-upload-libraries roundup; Svelte, vanilla JS and Preact, which have no roundup, link the /docs/comparisons/ hub. The three roundups link back to their framework page from upup's own entry. framework-pages.test.ts pins the per-framework target, that it renders between the table and "Keep reading", that it appears once in the guide, that it resolves to a real docs page, and that each roundup links back.
fix(landing): docs diagrams draw their arrows under reduced motion
|
One more on |
The hub link renders mid-sentence ("...side by side, see all upup
comparisons."), so its label no longer reads like a title. The roundup
labels keep their capital because they start with a framework name.
…inks feat(landing): link framework pages to their comparison roundups
|
One more on |
|
Merged at |
chore: sync master back into dev after #451
|
Post-deploy checks on production ( Deploy: the master-push autodeploy did fire. The job sat in Dokploy's shared deployment queue behind other projects' jobs for about 45 minutes, then deployed without intervention. No manual deploy was triggered. Probes: all return 200.
Content spot checks:
PostHog: a headless-Chrome visit to PageSpeed Insights, mobile,
Indexing: the sitemap was resubmitted to Search Console. The 20 changed URLs (the framework pages, the comparisons hub, the roundups, the Azure, modes, server-mode and FAQ pages, and the 4 legacy-URL targets) went to IndexNow (200) and to Bing URL submission. |
Promotes
devtomasterso the landing speed work reaches prod.What's in it (all from the SEO/perf program, nothing else is pending on
dev):/down from 426.8 to 210.0 KiB gzip.m.*with lazy features.Prod check after deploy: mobile PSI 3-run median on
/against the 74 baseline, plus nothing-broke probes (/,/react/, docs, sitemap, a PostHog event arriving).