diff --git a/apps/docs/app/[[...slug]]/layout.tsx b/apps/docs/app/[[...slug]]/layout.tsx index 41bfd4c32..b82931105 100644 --- a/apps/docs/app/[[...slug]]/layout.tsx +++ b/apps/docs/app/[[...slug]]/layout.tsx @@ -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(); +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(); + +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(); - 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 ( , { 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, })); } diff --git a/apps/docs/app/sitemap.ts b/apps/docs/app/sitemap.ts index 07aa3912b..5150cc994 100644 --- a/apps/docs/app/sitemap.ts +++ b/apps/docs/app/sitemap.ts @@ -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), + ); } diff --git a/apps/docs/lib/source.ts b/apps/docs/lib/source.ts index 8cf3c56ec..5f9a1d5ab 100644 --- a/apps/docs/lib/source.ts +++ b/apps/docs/lib/source.ts @@ -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[number]; +export type DocPage = + | LatestPage + | ReturnType[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) { +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) { +export async function getLLMText(page: LatestPage) { const processed = await page.data.getText('processed'); return `# ${page.data.title} diff --git a/apps/docs/source.config.ts b/apps/docs/source.config.ts index a57a945bc..044de3270 100644 --- a/apps/docs/source.config.ts +++ b/apps/docs/source.config.ts @@ -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,