perf: optimize docs static generation cost (#12108)

## Summary
- Switch the docs catch-all route to full static generation and prebuild
both latest and versioned docs paths.
- Prebuild OG images for all docs pages, make sitemap and LLM export
static, and remove nondeterministic sitemap timestamps.
- Reduce build-time overhead by memoizing docs sidebar trees and
skipping processed markdown generation for versioned docs.

## Testing
- `pnpm nx run @zitadel/docs:build`
- `pnpm nx run @zitadel/docs:lint`
- `pnpm nx run @zitadel/docs:check-types`
- Verified the prerender manifest contains 8,816 prerendered routes,
3,293 versioned docs routes, 4,406 OG routes, and zero revalidating docs
routes.
This commit is contained in:
Florian Forster
2026-04-27 15:34:00 +00:00
committed by GitHub
parent f96a9c54ac
commit 24d0f34f39
6 changed files with 94 additions and 89 deletions
+47 -55
View File
@@ -1,69 +1,61 @@
import { source, versionSource } from '@/lib/source';
import { getAllDocPages, source, versionSource } from '@/lib/source';
import { ReactNode } from 'react';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';
import { buildCustomTree } from '@/lib/custom-tree';
import rawVersions from '@/content/versions.json';
const versions = rawVersions as any[];
import { VersionSelector } from '@/components/version-selector';
type DocVersion = { param: string };
type PageTree = typeof source.pageTree;
const versions = rawVersions as DocVersion[];
const versionParams = new Set(versions.map((version) => version.param));
const labelsMap = new Map<string, string>();
for (const page of getAllDocPages()) {
if (page.data.sidebar_label) {
labelsMap.set(page.url, page.data.sidebar_label);
}
}
const latestTree = buildCustomTree(source.pageTree, { labels: labelsMap });
const versionTreeCache = new Map<string, PageTree>();
function getVersionTree(currentVersion: string): PageTree {
const cached = versionTreeCache.get(currentVersion);
if (cached) return cached;
const children = (versionSource.pageTree as any).children || [];
const versionFolder = children.find((node: any) => {
if (node.type !== 'folder') return false;
if (node.index?.url && node.index.url.includes(`/${currentVersion}`)) return true;
return Boolean(node.children?.[0]?.url?.includes(`/${currentVersion}`));
});
const tree = versionFolder
? buildCustomTree(
{
name: versionFolder.name,
children: versionFolder.children,
} as any,
{
stripPrefix: `/${currentVersion}`,
labels: labelsMap,
},
)
: buildCustomTree(versionSource.pageTree, { labels: labelsMap });
versionTreeCache.set(currentVersion, tree);
return tree;
}
export default async function Layout(props: { children: ReactNode; params: Promise<{ slug?: string[] }> }) {
const params = await props.params;
const slug = params.slug || [];
// Determine current version and tail path
// If first slug segment matches a version param (e.g. v4.10), that's the version.
// Otherwise it's latest.
const versionParam = slug[0] && versions.find(v => v.param === slug[0]) ? slug[0] : undefined;
const currentVersion = versionParam || 'latest';
const allPages = [...source.getPages(), ...versionSource.getPages()];
const labelsMap = new Map<string, string>();
allPages.forEach(p => {
if (p.data.sidebar_label) {
labelsMap.set(p.url, p.data.sidebar_label);
}
});
let tree: typeof source.pageTree;
if (currentVersion !== 'latest') {
// Hoist the version folder to root to flatten sidebar
// versionSource.pageTree -> [ v4.10 folder, v4.9 folder ... ]
// We want the children of the specific version folder.
const children = (versionSource.pageTree as any).children || [];
const versionFolder = children.find((node: any) => {
if (node.type !== 'folder') return false;
// Check if the folder's index page belongs to this version
if (node.index?.url && node.index.url.includes(`/${currentVersion}`)) return true;
// Fallback: check first child if index missing
if (node.children?.[0]?.url?.includes(`/${currentVersion}`)) return true;
return false;
});
if (versionFolder) {
// Hoist children
const hoistedTree = {
name: versionFolder.name,
children: versionFolder.children,
} as any;
// Apply custom sidebar structure (from local sidebar-data.ts)
// We strip the prefix '/v4.10' so the lookup matches generic keys 'guides/...'
// The logs showed the URLs start with /[version] not /docs/[version]
const prefix = currentVersion === 'latest' ? '/docs' : `/${currentVersion}`;
tree = buildCustomTree(hoistedTree, {
stripPrefix: prefix,
labels: labelsMap
});
} else {
tree = buildCustomTree(versionSource.pageTree, { labels: labelsMap });
}
} else {
tree = buildCustomTree(source.pageTree, { labels: labelsMap });
}
const currentVersion = slug[0] && versionParams.has(slug[0]) ? slug[0] : 'latest';
const tree = currentVersion === 'latest' ? latestTree : getVersionTree(currentVersion);
return (
<DocsLayout
+2 -2
View File
@@ -1,4 +1,4 @@
import { getPageImage, getPage, source } from '@/lib/source';
import { generateAllDocParams, getPageImage, getPage, source } from '@/lib/source';
import {
DocsBody,
DocsPage,
@@ -43,7 +43,7 @@ export const revalidate = false;
export const dynamic = 'force-static';
export async function generateStaticParams() {
return source.generateParams();
return generateAllDocParams();
}
export async function generateMetadata(
+7 -5
View File
@@ -1,22 +1,24 @@
import { ImageResponse } from 'next/og';
import { notFound } from 'next/navigation';
import { source, getPageImage } from '@/lib/source';
import { getAllDocPages, getPage, 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) {
export async function GET(_request: Request, context: any) {
const { slug } = await context.params;
const page = source.getPage(slug.slice(0, -1));
if (slug[slug.length - 1] !== 'image.png') notFound();
const { page } = getPage(slug.slice(0, -1));
if (!page) notFound();
return new ImageResponse(
<DefaultImage
title={page.data.title}
description={page.data.description}
site="My App"
site="ZITADEL Docs"
/>,
{
width: 1200,
@@ -26,7 +28,7 @@ export async function GET(request: Request, context: any) {
}
export function generateStaticParams() {
return source.getPages().map((page) => ({
return getAllDocPages().map((page) => ({
slug: getPageImage(page).segments,
}));
}
+19 -20
View File
@@ -1,27 +1,26 @@
import { MetadataRoute } from 'next';
import { source } from '@/lib/source';
export const revalidate = false;
export const dynamic = 'force-static';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://zitadel.com/docs';
const baseUrl = 'https://zitadel.com/docs';
const docsPages = source.getPages().map((page) => ({
url: `${baseUrl}${page.url === '/' ? '' : page.url}`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
const docsPages = source.getPages().map((page) => ({
url: `${baseUrl}${page.url === '/' ? '' : page.url}`,
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 1,
},
...docsPages,
].filter((item, index, self) =>
index === self.findIndex((t) => t.url === item.url)
);
return [
{
url: baseUrl,
changeFrequency: 'daily' as const,
priority: 1,
},
...docsPages,
].filter(
(item, index, self) => index === self.findIndex((t) => t.url === item.url),
);
}
+19 -4
View File
@@ -1,7 +1,9 @@
import { docs, versions } from '../.source/server';
import { type InferPageType, loader } from 'fumadocs-core/source';
import { loader } from 'fumadocs-core/source';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
const DOCS_BASE_PATH = '/docs';
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
baseUrl: '/',
@@ -15,6 +17,11 @@ export const versionSource = loader({
plugins: [lucideIconsPlugin()],
});
type LatestPage = ReturnType<typeof source.getPages>[number];
export type DocPage =
| LatestPage
| ReturnType<typeof versionSource.getPages>[number];
export function getPage(slugs: string[] | undefined) {
const safeSlugs = slugs || [];
// If the first slug matches a known version pattern (e.g., starts with 'v' and is in our list), use versionSource
@@ -27,16 +34,24 @@ export function getPage(slugs: string[] | undefined) {
return { page: source.getPage(safeSlugs), source: source };
}
export function getPageImage(page: InferPageType<typeof source>) {
export function getAllDocPages(): DocPage[] {
return [...source.getPages(), ...versionSource.getPages()];
}
export function generateAllDocParams() {
return [...source.generateParams(), ...versionSource.generateParams()];
}
export function getPageImage(page: DocPage) {
const segments = [...page.slugs, 'image.png'];
return {
segments,
url: `/og/docs/${segments.join('/')}`,
url: `${DOCS_BASE_PATH}/og/docs/${segments.join('/')}`,
};
}
export async function getLLMText(page: InferPageType<typeof source>) {
export async function getLLMText(page: LatestPage) {
const processed = await page.data.getText('processed');
return `# ${page.data.title}
-3
View File
@@ -39,9 +39,6 @@ export const versions = defineDocs({
sidebar_label: z.string().optional(),
}),
files: ['v*/**/*.md', 'v*/**/*.mdx', '!**/_*'], // Include only versioned folders from content
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,