From 314b1b50dddf537b0cf7b5dd0869211a64d9a8c1 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Fri, 24 Apr 2026 08:34:57 -0700 Subject: [PATCH] docs: eliminate runtime ISR writes via full SSG (#12103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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) --- apps/docs/app/[[...slug]]/page.tsx | 5 +++-- apps/docs/app/llms-full.txt/route.ts | 1 + apps/docs/app/og/docs/[...slug]/route.tsx | 10 +++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/docs/app/[[...slug]]/page.tsx b/apps/docs/app/[[...slug]]/page.tsx index a4bdf1592..5a9e12eef 100644 --- a/apps/docs/app/[[...slug]]/page.tsx +++ b/apps/docs/app/[[...slug]]/page.tsx @@ -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(); diff --git a/apps/docs/app/llms-full.txt/route.ts b/apps/docs/app/llms-full.txt/route.ts index d494d2cbb..fd7e36872 100644 --- a/apps/docs/app/llms-full.txt/route.ts +++ b/apps/docs/app/llms-full.txt/route.ts @@ -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); diff --git a/apps/docs/app/og/docs/[...slug]/route.tsx b/apps/docs/app/og/docs/[...slug]/route.tsx index b88c88389..1cdd5a013 100644 --- a/apps/docs/app/og/docs/[...slug]/route.tsx +++ b/apps/docs/app/og/docs/[...slug]/route.tsx @@ -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, + })); }