Skip to content

fix(docs): serve llms-full.txt as a static file written at build time, out of Vercel's 20 MB ISR fallback cap - #23640

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23625-llms-full-static
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23625-llms-full-static

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23625

Clause-②: no

/llms-full.txt is now a static file that apps/docs/scripts/build-llms-full.mjs writes into apps/docs/public/ during the docs build. The prerendered route handler it replaces is gone. The URL, the bytes, the content type and the X-Robots-Tag: noindex header all stay the same. What changes is how Vercel receives the file: as a static asset, not as an ISR page. The 20 MB ISR fallback cap was failing every production docs deploy, and it no longer applies to this file.

What was red (A1)

  • docs-deploy.yml scheduled runs 38663939159 (01:07Z) and 38695971788 (07:01Z) both failed at step 13, "Deploy the prebuilt output to production". Step 11 (vercel build --prod) and step 12 (output assertion) were green, per the run's job steps. The last green run was 38618717424 (2026-10-14 18:51Z).
  • The error text was NOT MEASURED here: the job-log download redirects to a blob host that this container's proxy refuses (403 on CONNECT). The card quotes it: FALLBACK_BODY_TOO_LARGE, "ISR page llms-full.txt is 20.00 MB, over the 20 MB limit". That string is not in the vercel@60.1.3 CLI dist (grep). So the check runs server-side, at upload.

How big, how fast (A3)

To measure, I ran the same generator (getLLMText over source.getPages(), joined by blank lines) on each commit after pnpm install and the docs build's three spec generators (gen:schema, gen:docs, gen:upgrade-guide). That includes the build-time content/docs/protocol-upgrade/ pages. On e611e2bda the result is byte-identical to the route's own prerendered body (below).

commit (main) committed pages bytes vs 20,000,000
0d91eaa6e 2026-08-25 403 8,046,416 -11,953,584
8c657f7dd 2026-09-14 402 14,632,088 -5,367,912
05be35259 2026-09-30 408 16,787,480 -3,212,520
51290bca2 2026-10-07 410 17,357,732 -2,642,268
989aa483f (last green deploy) 2026-10-14 17:58Z 418 19,998,748 -1,252
252775d1d (first red deploy) 2026-10-15 00:30Z 418 20,004,539 +4,539
e611e2bda (this branch's base) 2026-10-15 06:55Z 418 20,032,800 +32,800
23e689dc7 (main merged in) 2026-10-15 418 20,040,773 +40,773
  • The cap is 20,000,000 bytes (decimal MB). The last green deploy was 1,252 bytes under it and the first red one was 4,539 over. Both print as "20.00 MB".
  • Growth: 12.0 MB in 50.6 days since 2026-08-25 (1.66 MB/week). The last 30 days averaged 1.25 MB/week; the last 7 days, 2.57 MB/week. The 2026-08-25 figure matches the "8 MB today" that next.config.mjs recorded that day.
  • What it is made of (at e611e2bda): the auto-generated references/** pages are about 66% of the bytes (ui 3.22 MB, api 3.06 MB, data 2.61 MB, system 1.51 MB). The build-time protocol-upgrade/** pages are 9.4%. The largest single page is protocol-upgrade/18, at 1.46 MB. Growth follows the spec surface, not hand-written docs. So any fix that only trimmed the file back under 20 MB would have failed again within weeks.

Why a public/ file escapes the cap (A4)

  • I read this from the builder that vercel@60.1.3 pins, @vercel/next@15.0.1. Its getStaticFiles() globs public/** after the build command and emits each file as a static output. Prerendered routes are emitted as prerenders with a fallback body, and that body is what the deploy API measured. A route handler with revalidate = false is a prerender (.next/server/app/llms-full.txt.body + .meta, routeCache.kind: APP_ROUTE).
  • After this change the largest prerender in the build is en/docs/protocol-upgrade/18.html, at 3,916,137 bytes (19.6% of the cap). Next is references/api/protocol.html at 2,422,127.
  • A static-file limit could still apply, and I could not verify it. vercel.com is unreachable from this container (proxy 403; docs.vercel.com does not resolve). A search-result excerpt of Vercel's limits page gives "Static File uploads 100 MB (Hobby) / 1 GB (Pro)". If 100 MB applies, the file has about 31–64 weeks of headroom at the measured rates. At 1 GB it has years. Before that, the Vercel build and upload never ran here: there is no VERCEL_TOKEN in this container.

The change

  • apps/docs/app/llms-full.txt/route.ts is deleted, so the file has one producer.
  • New apps/docs/scripts/build-llms-full.mjs, run by the docs build script after the spec generators and before next build:
    • It regenerates .source/ with fumadocs-mdx's own postInstall(), which is what next build does at config load. On a fresh checkout, pnpm install writes .source/ before gen:upgrade-guide creates content/docs/protocol-upgrade/, so without this step those three pages (1.87 MB) would be silently missing.
    • It imports lib/source.ts as plain Node, so the text comes from the same getLLMText and source.getPages() the other agent-reader routes use, not from a copy. fumadocs-mdx's Node loader (fumadocs-mdx/node register()) compiles each page with the same source.config.ts.
    • It writes public/llms-full.txt and prints N pages, B bytes.
    • It exits non-zero on 0 pages, or on a Node without type stripping (process.features.typescript, Node 22.18+), so the build fails rather than deploying without the file.
  • New apps/docs/scripts/llms-full/resolve-hooks.mjs: the Node spelling of the resolutions the Next build already performs. These are the two tsconfig paths aliases, plus bare imports from content/** resolved from the app, the same reason turbopack.resolveAlias exists.
  • apps/docs/.gitignore ignores /public/llms-full.txt.
  • A next.config.mjs comment now gives the current size and says the noindex header covers the static file.
  • docs-deploy.yml and vercel.json are untouched; turbo.json adds public/llms-full.txt to the @objectstack/docs#build outputs (patch round, b7c786c2). No bypass flag.

Proof that this changes delivery only

  • On e611e2bda: the route's prerendered body was 20,032,800 bytes, sha256 cbbf6f782618778397483dcbea544cc1bc1c144d002e409807e7d2bae1986d0b. The generator, reading the .source/ that same build left behind, produced the same sha256.
  • On this branch (40af08370), I ran apps/docs/vercel.json's buildCommand from a fresh-checkout state: no .next, .source/ regenerated without protocol-upgrade, protocol-upgrade/ deleted. It succeeded (turbo: 2 tasks, 0 cached). The build printed build-llms-full: 418 pages, 20,032,800 bytes, and the route table no longer lists /llms-full.txt.
  • Same content, possibly different page order. A proof script split the file against the 418 per-page bodies Next prerendered in that same build (/llms.mdx/docs/**, each one getLLMText(page)) and the order llms.txt lists them in. Result: PERMUTATION-EQUAL: 418/418 pages consumed. The e611e2bda route body is also PERMUTATION-EQUAL against them. A one-byte mutant of it fails (FAIL at offset 9886232). I reran this on d777181db with the same result.
  • Page order already varied before this PR. fumadocs-mdx writes the .source/ index in a different order on every run: four runs of fumadocs-mdx on one tree gave four different index hashes. So the route's page order already changed from build to build. One difference within a single build: this file's order no longer matches /llms.txt's, because the generator and next build each regenerate .source/. Neither file's order was ever stable, and nothing reads it.
  • Served: I ran next start on that build and fetched GET /llms-full.txt. Response: 200, Content-Type: text/plain; charset=UTF-8, X-Robots-Tag: noindex, Content-Length: 20032800, bytes cmp-equal to public/llms-full.txt. /llms.txt and /docs/ai/agents.mdx still answer 200. The old route answered text/plain;charset=UTF-8 with the same header.

What this PR can and cannot prove

  • It proves: the build writes the file and fails if it cannot; no prerender above 3.9 MB remains; the local production server serves the file with the same headers.
  • It cannot prove that Vercel accepts the upload. The card's Done criterion is a green scheduled docs-deploy.yml run serving /llms-full.txt, and that run happens after merge: the next run is at :43 past 00/06/12/18 UTC, or sooner via the workflow's "Run workflow" button.

Tests and gates (head f7ce0ddb6: this branch with origin/main 23e689dc7 merged in)

  • At f7ce0ddb6, after pnpm install and the three spec generators: node scripts/build-llms-full.mjs printed 418 pages, 20,040,773 bytes, and pnpm --filter @objectstack/docs typecheck (fumadocs-mdx && next typegen && tsc --noEmit) exited 0. The app has no test script. tsconfig.json includes **/*.ts(x) only, so the two new .mjs scripts are outside tsc. The build and the proofs above are their tests.
  • At f7ce0ddb6, node scripts/pm/dispatch-gates.mjs --commands derived 38 commands. All exited 0 (the same 38 also exited 0 at d777181db, before the merge), and the --ran reconciliation with exit codes says "38 derived famil(ies) accounted for — 38 run, 0 NOT-MEASURED (a DERIVED zero)". The list includes check:llms-txt, check-doc-frontmatter, check-docs-nav-label (+ self-tests), check:docs-locale-catch-all, check:published-files, check:turbo-task-graph, check:test-source-alias, check:nul-bytes, check:watch-hint-literal and check-undeclared-dep-imports.
  • A5: check:llms-txt reads packages/spec/llms.txt (its TARGET), the hand-kept summary inside the spec tarball, not the docs site's /llms.txt or /llms-full.txt. It is unaffected and green.
  • Build Docs (ci.yml) runs this same vercel.json command on the PR. That check-run is the CI half of the build proof.
  • No changeset: @objectstack/docs is private: true and publishes nothing, so this PR takes skip-changeset.

Acceptance notes

  • next dev no longer serves /llms-full.txt until a build, or node scripts/build-llms-full.mjs in apps/docs, has written the file. The old route rendered it on demand in dev. Production is unaffected.
  • Build time: the generator compiles every page in Node. That took 81–110 s on a shared 4-core container; next build alone took 208 s there.
  • Turbo outputs: @objectstack/docs#build now declares public/llms-full.txt (patch round, b7c786c2); a cache-hit replay restored it byte-identical (measured on b7c786c2, report on docs deploy is red: llms-full.txt reached 20 MB, Vercel's cap for an ISR page, so every production docs deploy now fails #23625 comment 6188441795). Both production paths also set TURBO_FORCE.
  • The next prerender to watch for the same cap is /docs/protocol-upgrade/18, at 3.9 MB of HTML. Its source page grows with every 18.x upgrade entry. Owner: none.

Patch round (seat-added)

b7c786c2 adds public/llms-full.txt to the @objectstack/docs#build outputs in root turbo.json, so a turbo cache-hit replay restores the file this PR now builds (the dev's open question, option B, taken by the seat; claim addendum on #23625 comment 6187979278). Both production paths force execution today, so this closes a latent gap, not a live one.


Generated by Claude Code

…prerendering an ISR route

Vercel refuses a prerender fallback body over 20 MB; the concatenation
measured 20,032,800 bytes at e611e2b. A public/ file is a static asset.

Claude-Session: https://claude.ai/code/session_01ESFcvW3staenyBCGq7kcDH
Co-authored-by: Claude <noreply@anthropic.com>
…de paths absolute

Claude-Session: https://claude.ai/code/session_01ESFcvW3staenyBCGq7kcDH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation labels Oct 15, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 15, 2026
…d task

A turbo cache-hit replay restored .next/** only, so a replayed build
would deploy without /llms-full.txt and report success.

Claude-Session: https://claude.ai/code/session_01ESFcvW3staenyBCGq7kcDH
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · domain:devx seat 1 · session_01ESFcvW3staenyBCGq7kcDH · 2026-10-15T08:52Z

Verdict: ACCEPT for #23625, on head b7c786c2. Read against GitHub and the tree, not the report.

  1. Shape.
    • Draft PR on main. The body opens with Fixes #23625; that is its only closing keyword.
    • Seven files, +185 / −13 (get_files), all inside the claimed surface plus the declared turbo.json addendum 6187979278.
    • check-governed-merges --pr 23640: NOT governed, 198 changed lines.
    • skip-changeset: @objectstack/docs is private: true.
  2. Premises, measured by the seat.
    • The red is live. The scheduled docs-deploy.yml runs 38663939159 (01:07Z) and 38695971788 (07:01Z) failed. The last green is 38618717424 (2026-10-14 18:51Z).
    • The deploy path picks up the new file. docs-deploy.yml runs vercel build --prod, which runs apps/docs/vercel.json's buildCommand and therefore this app's build script. That script now runs node scripts/build-llms-full.mjs before next build, and public/** becomes static output.
    • The workflow's assertion step checks only apps/docs/.next/BUILD_ID and .vercel/output/config.json, so removing the route moves nothing it reads.
    • Node. The script needs TypeScript type stripping (Node 22.18+). Both docs-deploy.yml (line 134) and ci.yml Build Docs install node-version: '22'.
    • Cache. Both production paths set TURBO_FORCE: 'true'. The patch round adds public/llms-full.txt to the existing @objectstack/docs#build override's outputs only, so no other package's cache changes and a replay restores the file.
    • One producer. The script imports lib/source.ts's getLLMText over source.getPages(). The route is deleted. The URL, text/plain and X-Robots-Tag: noindex are unchanged. The dev measured next start serving it 200, byte-equal.
    • Size, the dev's reading (not re-measured by the seat): 20,032,800 bytes over 418 pages. The last green commit 989aa483f measured 19,998,748 and the first red 252775d1d 20,004,539 against a decimal 20,000,000 cap. Growth is about 1.25 MB/week over 30 days and 2.57 MB/week over the last 7.
  3. Gates (check-runs on b7c786c2). All 40 runs are success or skipped. That includes Build Docs (the production build command, run on CI), Lint & Repo Gates, TypeScript Type Check, Test Core and Governed Surface Queue Guard. check-expected-skips --pr 23640: OK, 6 skips, every one on the roster.
  4. Accepted deviations.
    • The failing step's log text could not be read from this container (blob host refused). The failing step was read from the job's step list.
    • The static-file size limit is not verified against Vercel's own docs.
    • Byte identity is proven as permutation-equality: .source order changes on every build, as it did for the route.
    • The full next build ran at 40af08370. Later heads re-ran the generator, the proofs and the gates, and CI Build Docs ran the full command on b7c786c2.
  5. Out of scope, where it goes. Acceptance notes, carrier none, nothing filed:
    • .source order is unstable per build, with no consumer of the order.
    • /docs/protocol-upgrade/18 is the next-largest prerender, at 3.9 MB (headroom only).
    • next dev no longer serves /llms-full.txt until a build writes it.
  6. Landing. The seat flips the PR ready and arms auto-merge. The card's Done (a green scheduled docs-deploy.yml with /llms-full.txt served in production) is read after the merge, on the next scheduled run (about 12:53Z). The card closes on the merge, and that reading is recorded on it.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 15, 2026 08:54
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 15, 2026 08:54
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 42718df Oct 15, 2026
55 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23625-llms-full-static branch October 15, 2026 09:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs deploy is red: llms-full.txt reached 20 MB, Vercel's cap for an ISR page, so every production docs deploy now fails

2 participants