Files
zitadel/apps
314b1b50dd docs: eliminate runtime ISR writes via full SSG (#12103)
# Which Problems Are Solved

The Vercel `docs` project generated ~64M ISR writes over 30 days (99.5%
of ISR writes across all projects, ~\$258/month).

Root causes in the Next.js 16 docs app:
- `apps/docs/app/[[...slug]]/page.tsx` had `dynamicParams = true` +
`revalidate = 3600`. Bot traffic hitting unknown URLs (`/docs/wp-admin`,
`/docs/.env`, fuzzed paths) got rendered via \`notFound()\`, and the 404
response was cached as an ISR entry — 1 write per unique bad URL. Known
pages were also rewritten hourly for no reason since content only
changes on deploy.
- `apps/docs/app/og/docs/[...slug]/route.tsx` had `revalidate = false` +
empty `generateStaticParams()` + implicit `dynamicParams = true`. Every
unique OG URL (including bot probes) was cached forever — writes
accumulated permanently.

# How the Problems Are Solved

Switch the docs routes to pure SSG (content is static and only changes
on deploy, so ISR provides no value):

- `app/[[...slug]]/page.tsx`: `dynamicParams = false`, `revalidate =
false`, `dynamic = 'force-static'`. Unknown URLs now return a static 404
at the CDN — no function invocation, no ISR write. All 390 pages from
`source.generateParams()` are still pre-rendered.
- `app/og/docs/[...slug]/route.tsx`: `generateStaticParams()` now
returns all 390 pages via the existing `getPageImage(page).segments`
helper, so every OG image is pre-built as a static asset. `dynamicParams
= false` + `dynamic = 'force-static'` locks it down.
- `app/llms-full.txt/route.ts`: added `dynamic = 'force-static'` as a
safety net (already `revalidate = false`, single URL).

The tradeoff is longer CI builds (~40s–2min for 390 OG image
generations, paid on every preview deploy) in exchange for eliminating
~\$258/month in ISR writes plus associated function invocations and CPU
time.

# Additional Changes

None.

# Additional Context

- No changes to `next.config.mjs`, `vercel.json`, or redirects.
- Existing `apps/docs/redirects.json` (3,261 entries) covers legacy URLs
so `dynamicParams = false` won't 404 moved pages linked from elsewhere.
- Versioned routes: `content/versions.json` and `v*/` folders don't
exist yet. When versioning is activated, `generateStaticParams()` in
both files must also include `versionSource.generateParams()` —
otherwise versioned URLs will 404 under `dynamicParams = false`.

## Test plan

- [ ] CI build succeeds (expect modest build-time increase for OG
pre-generation)
- [ ] Inspect `apps/docs/.next/prerender-manifest.json` — all 390 doc
routes + 390 OG routes listed with `initialRevalidateSeconds: false`
- [ ] Local smoke: `/docs` → 200, `/docs/wp-admin` → 404 (static, no
function), `/docs/og/docs/guides/start/image.png` → PNG,
`/docs/og/docs/bogus/image.png` → 404
- [ ] Post-deploy: Vercel **ISR Writes** metric drops to near-zero
within 24h
- [ ] Post-deploy: Vercel **Function Invocations** for `/og/docs/*` drop
to zero
- [ ] Verify no legitimate docs pages 404 (cross-check logs against
`apps/docs/app/sitemap.ts`)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-24 12:34:57 -03:00
..