Skip to content

chore(release): promote dev to master (landing first-load JS halved, #450) - #451

Merged
AminDhouib merged 26 commits into
masterfrom
dev
Sep 27, 2026
Merged

AminDhouib merged 26 commits into
masterfrom
dev

Conversation

@BSalaeddin

Copy link
Copy Markdown
Collaborator

Promotes dev to master so the landing speed work reaches prod.

What's in it (all from the SEO/perf program, nothing else is pending on dev):

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).

AminDhouib and others added 10 commits September 23, 2026 19:22
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
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"
perf(landing): halve the home page first-load JS (zod off the client, PostHog after load, LazyMotion)
@codesandbox

codesandbox Bot commented Sep 26, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web Editor • VS Code • Insiders

Open Preview

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
@AminDhouib

Copy link
Copy Markdown
Member

Heads-up: I merged two green SEO PRs into dev, so this promotion now carries them too, and its checks will re-run on the new head.

Both had Status Check and E2E Status Check green, and I trial-merged them against dev after #450 with no conflicts. The landing is the only thing that changes.

docs: server mode signs uploads (browser PUTs to the bucket); bucket CORS needed in both modes
@AminDhouib

Copy link
Copy Markdown
Member

One more merged into dev: #448 (docs: server mode signs uploads and the browser PUTs to the bucket; bucket CORS is needed in both modes). Both rollups were green on its head, which already includes #450; I resolved the one-line vignettes.tsx conflict with m.span in a merge commit. It changes landing and docs content, both READMEs and a release-notes script comment; no package code.

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)
@AminDhouib

Copy link
Copy Markdown
Member

Two more green PRs are now on dev, so this promotion carries them and its checks will re-run on the new head:

Both had Status Check and E2E Status Check green, including Docs E2E. Neither touches package code.

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)
@AminDhouib

Copy link
Copy Markdown
Member

One more on dev: #455 (landing only). Legacy /documentation/docs/* URLs now reach their real docs pages instead of /docs/docs/* 404s; Search Console still shows impressions on them. The www → apex redirect also keeps the trailing slash. Both rollups were green, including Docs E2E.

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
@AminDhouib

Copy link
Copy Markdown
Member

One more on dev: #456 (landing only). Docs diagrams now draw their arrow shafts and check marks under prefers-reduced-motion: reduce; before, only the arrowheads showed. Reduced motion now jumps straight to the drawn state, and the server markup no longer depends on the preference. Both rollups were green, including Docs E2E. It fixes #452, which closes once this promotion lands on master.

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
@AminDhouib

Copy link
Copy Markdown
Member

One more on dev: #457 (landing only). The framework pages now link to their comparison roundups: /react/, /vue/ and /angular/ to their own "best file upload libraries" page, and /svelte/, /vanilla/ and /preact/ to the comparisons hub. Each roundup links back to its framework page. Search Console shows the framework pages collecting the "best / most popular X file upload" impressions at positions 43-60, but nothing linked them to the roundups that answer those queries. Both rollups were green, including Docs E2E.

@AminDhouib
AminDhouib merged commit 42f46e1 into master Sep 27, 2026
22 checks passed
@AminDhouib

Copy link
Copy Markdown
Member

Merged at 42f46e15 with the maintainer's go-ahead. The PR had been green on e9d20092 for a while and all the SEO work was waiting on it. I'm running your listed prod checks now: mobile PSI 3-run median on / against the 74 baseline, plus probes for /, /react/, docs, sitemap and a PostHog event arriving. I'll post the results here. Then I'll open the master→dev sync.

AminDhouib added a commit that referenced this pull request Sep 27, 2026
chore: sync master back into dev after #451
@AminDhouib

Copy link
Copy Markdown
Member

Post-deploy checks on production (42f46e15, live on useupup.com at 03:16Z).

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.

  • /
  • /react/, /vue/, /angular/, /svelte/, /vanilla/, /preact/
  • /docs/, /docs/faq/
  • /docs/comparisons/, the three best-{react,vue,angular}-file-upload-libraries/ roundups, upup-vs-uploadthing/
  • /docs/guides/storage/azure-blob/, /docs/guides/modes/, /docs/guides/server-mode-setup/
  • /sitemap.xml (83 URLs), /robots.txt, /llms.txt, /llms-full.txt
  • /docs/faq.md, served as text/markdown

Content spot checks:

Check Result
/react/ links to its roundup under the comparison table once
/svelte/, /vanilla/, /preact/ link to /docs/comparisons/ "see all upup comparisons", once each
The Vue roundup links back to /vue/ yes
JSON-LD on / WebSite, Organization, SoftwareApplication, SoftwareSourceCode, FAQPage
JSON-LD on /docs/faq/ and the Azure guide FAQPage, BreadcrumbList, TechArticle
JSON-LD on /vue/ FAQPage, BreadcrumbList
/documentation/docs/error-handling/ one 308 to /docs/error-handling/
www.useupup.com/angular 308 to www/angular/, then 308 to https://useupup.com/angular/ (two hops, down from three)
Hotjar on / none
Diagram strokes under reduced motion (#452) checked on the fix PR #456, not re-measured on prod

PostHog: a headless-Chrome visit to /docs/comparisons/?deploycheck=451 at 03:19Z shows up in project 11 as $pageview at 03:19:04Z.

PageSpeed Insights, mobile, /: 3 distinct runs gave 83, 90 and 82, a median of 83 against the 74 baseline.

  • An identical re-run came back byte-for-byte equal to the first because PSI caches results, so the third distinct run used /?psi=run3.
  • LCP was 3.2–4.0 s, FCP 1.2–2.3 s, TBT 10–30 ms and CLS 0.002.
  • The top remaining opportunities are unused JS and unused CSS.

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.

#452 should close now that #456 is on master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants