docs: introduce versioned docs and migrate to fuma (#11166)

## Todos for release

- [x] Configure Env in docs project on vercel
- [x] Configure Root Path in the docs project on vercel
- [ ] Remove old CSP https://github.com/zitadel/website/pull/1592

## What we did

This pull request migrates the project documentation from the old
`docs/` directory to the new `apps/docs/` directory, introduces a new
documentation system built with Next.js and Fumadocs, and updates all
relevant references, configuration files, and documentation to reflect
this change. It also adds new configuration and ignore files for the new
documentation app, updates CI and linting to exclude the new docs from
certain checks, and revises the contributing guidelines accordingly.

**Documentation System Migration and New Docs App**

* Migrated all documentation from `docs/` to `apps/docs/`, and updated
all references in `README.md`, `CONTRIBUTING.md`, and other files to
point to the new location.
[[1]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L585-L640)
[[2]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L660-R607)
[[3]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L740-R687)
[[4]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L2-R3)
[[5]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L30-R30)
* Added a new Next.js/Fumadocs-based documentation app under
`apps/docs/`, including core app files, layouts, routing, search API,
and a comprehensive `README.md` with development and contribution
instructions.
[[1]](diffhunk://#diff-5a1b07344a2c1b4d3f37b23ff1388b62cd9f57dea3c1cd23d8a0412b7602b132R1-R74)
[[2]](diffhunk://#diff-462b9ad1eabbb7d1c29bb9c36e4931eb180fd7190900e6d2babf8f4d66ad1c28R1-R39)
[[3]](diffhunk://#diff-e16ae25660ded787b10ac35dea96d5ecaacf895dae0afc8a9bd4382dc79a8c87R1-R7)
[apps/docs/app/[[...slug]]/layout.tsxR1-R81](diffhunk://#diff-59e08acde4e805b7aeccef1dcf98f1d71dfc550777e6b9402085cee0e9fa4e0aR1-R81),
[apps/docs/app/[[...slug]]/page.tsxR1-R79](diffhunk://#diff-e5df3f80d0fa01e12d63d81f29c57a9d14e846c78fd3beabb2ef768e38fd9580R1-R79),
[[4]](diffhunk://#diff-389b34918e040cacaa87cd7201ffa462cc2b0b716736f537e3d3c660ac69353bR1-R7)
[[5]](diffhunk://#diff-d3b03416d1c457b19f1c27b26f2db741412df841b875c26ccadc36e4522247f4R1-R29)
[[6]](diffhunk://#diff-c8fb8339570a5305809be7c618e14705fd86390dc278e6ce8ba224a7bc8b0c3cR1-R25)

**Configuration and Tooling Updates**

* Updated `.github/workflows/codeql.yml`, `.golangci.yaml`, and
`.github/dependabot.yml` to properly handle the new docs app: excluded
`apps/docs` from certain checks, added npm dependency updates for the
docs app, and excluded generated content.
[[1]](diffhunk://#diff-12783128521e452af0cfac94b99b8d250413c516ec71fe6d97dbea666ff7ba27L8-R14)
[[2]](diffhunk://#diff-9917ddc9f1c3304218f7269265b746d997c5c0615478177b5fceecd33ef47cb5R5-R6)
[[3]](diffhunk://#diff-9917ddc9f1c3304218f7269265b746d997c5c0615478177b5fceecd33ef47cb5R126-R129)
[[4]](diffhunk://#diff-dd4fbda47e51f1e35defb9275a9cd9c212ecde0b870cba89ddaaae65c5f3cd28R89-R106)
* Updated `.devcontainer/devcontainer.json` to use the latest Go 1.25.3
version for consistency.

**Licensing and Miscellaneous**

* Added `apps/docs/` to the list of licensed directories in
`LICENSING.md`.

These changes ensure the documentation is now maintained in a modern,
scalable system and all project tooling is updated to support the new
structure.

---------

Co-authored-by: Federico Coppede <[email protected]>
This commit is contained in:
Florian Forster
2026-01-29 08:38:36 -08:00
committed by GitHub
co-authored by Federico Coppede
parent 7b6395c348
commit d8ea041c56
2826 changed files with 78739 additions and 23571 deletions
+7
View File
@@ -0,0 +1,7 @@
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
import { ReactNode } from 'react';
export default function Layout({ children }: { children: ReactNode }) {
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
}
+81
View File
@@ -0,0 +1,81 @@
import { 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';
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 = 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 });
}
return (
<DocsLayout
tree={tree}
{...baseOptions()}
sidebar={{
banner: (
<VersionSelector />
),
}}
>
{props.children}
</DocsLayout>
);
}
+79
View File
@@ -0,0 +1,79 @@
import { getPageImage, getPage, source } from '@/lib/source';
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
} from 'fumadocs-ui/layouts/docs/page';
import { notFound } from 'next/navigation';
import { getMDXComponents } from '@/mdx-components';
import type { Metadata } from 'next';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import { Callout } from 'fumadocs-ui/components/callout';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Feedback } from '@/components/feedback';
export default async function Page(props: any) {
const params = await props.params;
const { page, source: pageSource } = getPage(params.slug);
if (!page) notFound();
const MDX = page.data.body;
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDX
components={getMDXComponents({
Callout,
Tab,
Tabs,
// this allows you to link to other pages with relative file paths
a: createRelativeLink(pageSource, page),
})}
/>
</DocsBody>
<Feedback />
</DocsPage>
);
}
export const dynamicParams = true;
export const revalidate = 3600;
export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(
props: any,
): Promise<Metadata> {
const params = await props.params;
const { page } = getPage(params.slug);
if (!page) notFound();
const baseUrl = 'https://zitadel.com/docs';
const url = params.slug ? `${baseUrl}/${params.slug.join('/')}` : baseUrl;
let canonicalUrl = url;
if (params.slug?.[0]?.startsWith('v')) {
const unversionedSlug = params.slug.slice(1);
const unversionedPage = source.getPage(unversionedSlug);
if (unversionedPage) {
canonicalUrl = `${baseUrl}${unversionedPage.url === '/' ? '' : unversionedPage.url}`;
}
}
return {
title: page.data.title,
description: page.data.description,
alternates: {
canonical: canonicalUrl,
},
openGraph: {
images: getPageImage(page).url,
},
};
}
+7
View File
@@ -0,0 +1,7 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
export const { GET } = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages
language: 'english',
});
+408
View File
@@ -0,0 +1,408 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import 'fumadocs-openapi/css/preset.css';
@theme {
/* 1. REGISTER STANDARD UTILITIES */
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
/* 2. MAP FUMADOCS VARIABLES */
--color-fd-primary: var(--color-primary);
--color-fd-primary-foreground: var(--color-primary-foreground);
--color-fd-background: var(--color-background);
--color-fd-foreground: var(--color-foreground);
--color-fd-card: var(--color-card);
--color-fd-card-foreground: var(--color-card-foreground);
--color-fd-popover: var(--color-popover);
--color-fd-popover-foreground: var(--color-popover-foreground);
--color-fd-muted: var(--color-muted);
--color-fd-muted-foreground: var(--color-muted-foreground);
--color-fd-accent: var(--color-accent);
--color-fd-accent-foreground: var(--color-accent-foreground);
--color-fd-border: var(--color-border);
--color-fd-ring: var(--color-ring);
--color-zitadelpink: #ff2069;
--font-sans: var(--font-lato), "Lato", "ui-sans-serif", "system-ui";
}
/* --- LIGHT MODE VARIABLES --- */
:root {
--primary: 230 59% 58%;
--primary-foreground: 0 0% 100%;
--background: 0 0% 98%;
--foreground: 0 0% 10%;
--card: 0 0% 93%;
--card-foreground: 0 0% 10%;
--muted: 0 0% 96%;
--muted-foreground: 0 0% 45%;
--accent: 0 0% 92%;
--accent-foreground: 230 59% 58%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 90%;
--input: 0 0% 90%;
--ring: 230 59% 58%;
--radius: 0.5rem;
--overlaycolor: #6c90b420;
}
/* --- DARK MODE VARIABLES --- */
:root[class~="dark"], [data-theme="dark"] {
--primary: 340 100% 56%;
--primary-foreground: 0 0% 100%;
--background: 235 45% 14%;
--foreground: 0 0% 98%;
--card: 220 40% 25%;
--card-foreground: 0 0% 98%;
--muted: 230 43% 15%;
--muted-foreground: 230 20% 70%;
--accent: 230 43% 20%;
--accent-foreground: 340 100% 65%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 230 43% 25%;
--input: 230 43% 25%;
--ring: 340 100% 56%;
--overlaycolor: #ffffff15;
/* FORCE OVERRIDE for Fumadocs Variables */
--color-fd-background: hsl(var(--background));
--color-fd-foreground: hsl(var(--foreground));
--color-fd-primary: hsl(var(--primary));
--color-fd-primary-foreground: hsl(var(--primary-foreground));
--color-fd-card: hsl(var(--card));
--color-fd-card-foreground: hsl(var(--card-foreground));
--color-fd-popover: hsl(var(--card));
--color-fd-popover-foreground: hsl(var(--foreground));
--color-fd-muted: hsl(var(--muted));
--color-fd-muted-foreground: hsl(var(--muted-foreground));
--color-fd-accent: hsl(var(--accent));
--color-fd-accent-foreground: hsl(var(--accent-foreground));
--color-fd-border: hsl(var(--border));
--color-fd-ring: hsl(var(--ring));
}
@layer components {
/* 1. VISIBILITY HELPERS */
.hideondark { display: block !important; }
.hideonlight { display: none !important; }
:root[class~="dark"] .hideondark, [data-theme="dark"] .hideondark {
display: none !important;
}
:root[class~="dark"] .hideonlight, [data-theme="dark"] .hideonlight {
display: block !important;
}
/* 2. MAIN CARD LAYOUT (Tile Wrapper) */
.tile-wrapper {
@apply flex flex-row flex-wrap gap-6 my-6;
}
.tile-wrapper > * {
@apply flex flex-col items-center text-center rounded-lg border relative;
background-color: hsl(var(--card)) !important;
border-color: hsl(var(--border));
justify-content: flex-start;
min-width: 160px;
min-height: 180px;
padding: 2rem;
padding-bottom: 3rem;
}
.tile-wrapper > * > :last-child {
margin-top: auto;
}
.tile-wrapper > * > h3,
.tile-wrapper > * > h4,
.tile-wrapper > * > strong {
@apply mt-0;
}
/* 3. ICON STYLING */
.custom-rounded {
@apply flex items-center justify-center p-2 rounded-full w-14 h-14 shrink-0 m-0;
}
.custom-rounded svg {
@apply fill-white h-7 w-7;
}
.custom-rounded i {
@apply text-white;
}
/* Gradients */
.custom-rounded-start { background: linear-gradient(40deg, #059669 30%, #047857); }
.custom-rounded-password { background: linear-gradient(40deg, #f59e0b 30%, #b45309); }
.custom-rounded-login, .custom-rounded-register { background: linear-gradient(40deg, #059669 30%, #047857); }
.custom-rounded-privatelabel, .custom-rounded-phone, .custom-rounded-email, .custom-rounded-storage, .custom-rounded-service { background: linear-gradient(40deg, #ff2069 30%, #cc1a54); }
.custom-rounded-split { background: linear-gradient(40deg, #ff2069, #cc1a54); }
.custom-rounded-texts, .custom-rounded-help, .custom-rounded-architecture { background: linear-gradient(40deg, #dc2626 30%, #db2777); }
.custom-rounded-system, .custom-rounded-apis, .custom-rounded-policy, .custom-rounded-instance { background: linear-gradient(40deg, #1f2937, #111827); }
/* 4. B2B / DIAGRAM LAYOUTS (Fixes the B2B Component) */
.flexrowbetween {
@apply flex flex-row items-center justify-around gap-4 my-8 w-full;
}
.arrowright {
@apply w-8 h-8 shrink-0 text-muted-foreground;
}
.b2borg, .b2bproject, .b2bapp, .b2buser {
@apply rounded-xl p-4 border text-left;
background: var(--overlaycolor);
border-color: hsl(var(--border));
}
.b2buser { @apply mt-4; }
.b2borg span, .b2bproject span, .b2bapp span, .b2buser span {
@apply ml-4 mb-2 block font-semibold;
}
/* Specific styling for the role text */
.b2bprojectrole {
@apply mt-2 ml-4 block text-sm opacity-80 font-normal;
}
/* 5. FOOTER / PAGINATION CARDS */
a.\@max-lg\:col-span-full {
@apply flex flex-col p-6 rounded-xl border transition-all no-underline;
background-color: hsl(var(--card)) !important;
border-color: hsl(var(--border)) !important;
}
a.\@max-lg\:col-span-full:hover {
@apply shadow-sm;
border-color: hsl(var(--primary)) !important;
background-color: hsl(var(--card)) !important;
}
a.\@max-lg\:col-span-full > div:first-child {
@apply flex items-center gap-3;
}
a.\@max-lg\:col-span-full svg {
@apply w-5 h-5 m-0;
}
a.\@max-lg\:col-span-full p {
margin-bottom: 0 !important;
margin-top: 0 !important;
}
a.\@max-lg\:col-span-full p.text-fd-muted-foreground {
@apply mt-2 text-sm opacity-80;
}
/* 6. VIDEO PLAYER WRAPPER */
/* Fixes aspect ratio for ReactPlayer embeds */
.player-wrapper {
@apply relative mb-8;
padding-top: 56.25%; /* 16:9 Aspect Ratio */
}
.react-player {
@apply absolute top-0 left-0;
}
/* 7. GET STARTED BUTTON */
.get-started {
@apply inline-flex items-center justify-center px-6 py-2 rounded-full font-bold border-2 transition-all;
border-color: #ff2069;
background-color: #ff2069;
color: white;
}
.get-started:hover {
@apply opacity-90;
color: white !important;
}
/* Target any div that uses the callout color variable */
div[style*="--callout-color"] {
@apply rounded-lg shadow-sm mb-4;
/* Force Docusaurus Layout: Left Border Only */
border: 1px solid rgba(0,0,0,0.05) !important;
border-left-width: 4px !important;
/* Hide the default Fumadocs vertical colored bar inside */
& > div[role="none"] {
display: none !important;
}
}
/* --- INFO (Blue) --- */
/* We match the variable name in the style string */
div[style*="--color-fd-info"] {
background-color: #eff6ff !important;
border-left-color: #3b82f6 !important;
color: #1e3a8a !important;
}
div[style*="--color-fd-info"] svg {
color: #3b82f6 !important;
fill: transparent !important; /* Remove fill to look cleaner */
}
/* Dark Mode Info */
:is(.dark div[style*="--color-fd-info"]) {
background-color: rgba(59, 130, 246, 0.1) !important;
color: #dbeafe !important;
border-top-color: rgba(255,255,255,0.05) !important;
border-right-color: rgba(255,255,255,0.05) !important;
border-bottom-color: rgba(255,255,255,0.05) !important;
}
/* --- WARNING (Amber) --- */
div[style*="--color-fd-warn"] {
background-color: #fffbeb !important;
border-left-color: #f59e0b !important;
color: #78350f !important;
}
div[style*="--color-fd-warn"] svg {
color: #f59e0b !important;
fill: transparent !important;
}
/* Dark Mode Warning */
:is(.dark div[style*="--color-fd-warn"]) {
background-color: rgba(245, 158, 11, 0.1) !important;
color: #fef3c7 !important;
border-top-color: rgba(255,255,255,0.05) !important;
border-right-color: rgba(255,255,255,0.05) !important;
border-bottom-color: rgba(255,255,255,0.05) !important;
}
/* --- ERROR (Red) --- */
div[style*="--color-fd-error"] {
background-color: #fef2f2 !important;
border-left-color: #ef4444 !important;
color: #7f1d1d !important;
}
div[style*="--color-fd-error"] svg {
color: #ef4444 !important;
fill: transparent !important;
}
/* Dark Mode Error */
:is(.dark div[style*="--color-fd-error"]) {
background-color: rgba(239, 68, 68, 0.12) !important;
color: #fee2e2 !important;
border-top-color: rgba(255,255,255,0.05) !important;
border-right-color: rgba(255,255,255,0.05) !important;
border-bottom-color: rgba(255,255,255,0.05) !important;
}
/* --- TITLE BOLD --- */
div[style*="--callout-color"] strong,
div[style*="--callout-color"] p:first-child strong {
font-weight: 700 !important;
}
}
@layer base {
/* HEADINGS */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-sans);
color: hsl(var(--foreground)) !important;
margin-bottom: 1rem;
letter-spacing: -0.025em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
color: inherit !important;
text-decoration: none !important;
}
h1 {
font-size: 3.5rem !important;
font-weight: 700 !important;
line-height: 1.1 !important;
margin-bottom: 1.5rem !important;
}
h2 {
font-size: 2.25rem !important;
font-weight: 600 !important;
margin-top: 3rem !important;
margin-bottom: 1rem !important;
padding-bottom: 0.5rem !important;
border-bottom: 1px solid hsl(var(--border)) !important;
}
h3 {
font-size: 2rem !important;
font-weight: 600 !important;
margin-top: 2.5rem !important;
margin-bottom: 1rem !important;
}
h4 {
font-size: 1.5rem !important;
font-weight: 600 !important;
margin-top: 2.5rem !important;
margin-bottom: 1rem !important;
}
p {
font-size: 1rem;
line-height: 1.7;
margin-bottom: 1.5rem;
}
html, body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
a {
color: hsl(var(--primary));
text-decoration: none;
transition: opacity 0.2s;
}
a:hover {
opacity: 0.8;
}
}
+29
View File
@@ -0,0 +1,29 @@
import { type BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
import { BookOpen, Code2, Server } from 'lucide-react';
export const baseOptions: BaseLayoutProps = {
nav: {
title: 'ZITADEL Docs',
},
links: [
{
text: 'Guides',
url: '/',
active: 'nested-url',
icon: <BookOpen />,
},
{
text: 'APIs',
url: '/apis/introduction',
active: 'nested-url',
icon: <Code2 />,
},
{
text: 'Legal',
url: '/legal/terms-of-service',
active: 'nested-url',
icon: <Server />, // Using Server icon as placeholder or pick another one
},
],
githubUrl: 'https://github.com/zitadel/zitadel',
};
+25
View File
@@ -0,0 +1,25 @@
import './global.css';
import type { Metadata } from 'next';
import { Providers } from './providers';
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'),
icons: {
other: [
{
rel: 'stylesheet',
url: '/docs/img/icons/line-awesome/css/line-awesome.min.css',
},
],
},
};
export default function Layout({ children }: any) {
return (
<html lang="en" suppressHydrationWarning>
<body className="flex flex-col min-h-screen font-sans bg-fd-background text-fd-foreground">
<Providers>{children}</Providers>
</body>
</html>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { getLLMText, source } from '@/lib/source';
export const revalidate = false;
export async function GET() {
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
return new Response(scanned.join('\n\n'));
}
+28
View File
@@ -0,0 +1,28 @@
import { ImageResponse } from 'next/og';
import { notFound } from 'next/navigation';
import { source } from '@/lib/source';
import { generate as DefaultImage } from 'fumadocs-ui/og';
export const revalidate = false;
export async function GET(request: Request, context: any) {
const { slug } = context.params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
return new ImageResponse(
<DefaultImage
title={page.data.title}
description={page.data.description}
site="My App"
/>,
{
width: 1200,
height: 630,
},
);
}
export function generateStaticParams() {
return [];
}
+26
View File
@@ -0,0 +1,26 @@
'use client';
import { RootProvider } from 'fumadocs-ui/provider/next';
import dynamic from "next/dynamic";
import { ThemeProvider } from 'next-themes';
import MixpanelProvider from '@/components/mixpanel-provider';
import PlausibleProvider from '@/components/plausible-provider';
const SearchDialog = dynamic(() => import("@/components/inkeep-search"));
export function Providers({ children }: { children: React.ReactNode }) {
return (
<RootProvider
search={{
enabled: true,
SearchDialog: SearchDialog as any,
}}
>
<PlausibleProvider />
<MixpanelProvider>
{children}
</MixpanelProvider>
</RootProvider>
);
}
+27
View File
@@ -0,0 +1,27 @@
import { MetadataRoute } from 'next';
import { source } from '@/lib/source';
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)
);
}