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]>
This commit is contained in:
Florian Forster
2026-04-24 12:34:57 -03:00
committed by GitHub
co-authored by Claude Opus 4.7
parent cc74a36b65
commit 314b1b50dd
3 changed files with 11 additions and 5 deletions
+3 -2
View File
@@ -38,8 +38,9 @@ export default async function Page(props: any) {
);
}
export const dynamicParams = true;
export const revalidate = 3600;
export const dynamicParams = false;
export const revalidate = false;
export const dynamic = 'force-static';
export async function generateStaticParams() {
return source.generateParams();
+1
View File
@@ -1,6 +1,7 @@
import { getLLMText, source } from '@/lib/source';
export const revalidate = false;
export const dynamic = 'force-static';
export async function GET() {
const scan = source.getPages().map(getLLMText);
+7 -3
View File
@@ -1,12 +1,14 @@
import { ImageResponse } from 'next/og';
import { notFound } from 'next/navigation';
import { source } from '@/lib/source';
import { source, getPageImage } from '@/lib/source';
import { generate as DefaultImage } from 'fumadocs-ui/og';
export const revalidate = false;
export const dynamicParams = false;
export const dynamic = 'force-static';
export async function GET(request: Request, context: any) {
const { slug } = context.params;
const { slug } = await context.params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
@@ -24,5 +26,7 @@ export async function GET(request: Request, context: any) {
}
export function generateStaticParams() {
return [];
return source.getPages().map((page) => ({
slug: getPageImage(page).segments,
}));
}