# 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]>
@zitadel/docs
Documentation for Zitadel, built with Fumadocs and Next.js.
Getting Started
Ensure you have followed the root quick start to set up dependencies.
Local Development
Ensure all dependencies are installed:
pnpm install
Start the development server:
pnpm nx run @zitadel/docs:dev
The site will be available at http://localhost:3000.
Scripts
Key scripts for documentation workflows:
| Script | Description |
|---|---|
dev |
Starts the development server. |
build |
Builds the production application. |
fetch:remote-content |
Fetches remote tags and referenced content. |
generate |
Runs all generation steps (fetch:remote-content, generate:proto-docs, generate:api-reference, generate:index-pages). |
check:links |
Validates content integrity (broken links, missing frontmatter, schema errors). |
check-types |
Validates typescript types. |
test |
Runs all validation steps (check-types, check:links). |
lint |
checks for code style and syntax errors (ESLint). |
clean |
Cleans the build output and generated files. |
Validation
- Code Quality: Run
pnpm nx run @zitadel/docs:lintto check for syntax and style issues in JS/TS/MDX files. - Content Integrity: Run
pnpm nx run @zitadel/docs:check-linksto validate content structure, including:- Broken internal links
- Missing required front-matter (e.g.,
title) - Image references
Troubleshooting
Issue: Nx configuration not found or "nx" command not found
If you see errors like:
NX Cannot find configuration for task @zitadel/zitadel:@zitadel/docs:generate
Pass --verbose to see the stacktrace.
or
pnpm nx run @zitadel/docs:dev
ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "nx" not found
Did you mean "pnpm nx"?
Root cause: Your dependency cache or node_modules is out of sync.
Solution: Run these commands from the repository root in order:
- Clear node_modules:
rm -rf node_modules— removes the installed dependency folder - Clear pnpm cache:
pnpm cache delete— clears pnpm's internal package cache - Reinstall dependencies:
pnpm install— reinstalls all dependencies fresh - Test the setup:
pnpm nx run @zitadel/docs:generate— verifies everything works
This resolves 99% of setup-related issues.
Contributing
Build Process
The docs build process automatically handles the following steps via generate:
- Downloads required protoc plugins.
- Generates gRPC documentation from proto files.
- Generates API documentation from OpenAPI specs.
- Generates index files for directory structures.
Style Guide
- Variables: Use environment variables in code snippets where possible.
- Embedded Content: Use
_filename.mdxfor content embedded in other pages (not indexed individually). - Code Embedding: Use the
fileproperty in code blocks to embed code from the repo. - Voice: Use active voice and sentence case for titles.
Refer to the Google Developer Style Guide for general guidelines.
Adding Content
All documentation content is located in the content directory. Note that the system strictly accepts only .mdx files.
To add a new page:
- Create a
.mdxfile in the appropriate subdirectory ofcontent. - Register the new page in the sidebar settings at
lib/sidebar-data.tsto make it accessible in the navigation.
Pull Requests
Use docs(<scope>): <short summary> for PR titles.
Pass quality checks before submitting:
pnpm nx run @zitadel/docs:build