mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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 docsPages = source.getPages().map((page) => ({
|
||||
url: `${baseUrl}${page.url === '/' ? '' : page.url}`,
|
||||
lastModified: new Date(),
|
||||
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)
|
||||
].filter(
|
||||
(item, index, self) => index === self.findIndex((t) => t.url === item.url),
|
||||
);
|
||||
}
|
||||
|
||||
+19
-4
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user