Files
zitadel/apps/docs/components/github-code-block.tsx
f4f43f5248 docs: fix broken GitHub code-reference blocks after fumadocs migration (#12213)
# Which Problems Are Solved

Pages in `apps/docs` that embed source from GitHub via the Docusaurus
convention

````
```js reference

https://github.com/zitadel/actions/blob/main/examples/org_metadata_claim.js
```
````

stopped working after the migration from Docusaurus to fumadocs. The old
`docusaurus-theme-github-codeblock` plugin used to fetch the file and
render it; fumadocs has no support for that meta, so the page rendered
the raw URL as plain code-block text. Visible at
`/docs/apis/actions/code-examples` and 16 other pages.

# How the Problems Are Solved

- Converted every ```` ```<lang> reference\n<URL>\n``` ```` block (46
total across 17 `.mdx` files) to the native fumadocs JSX form:
`<GithubCodeBlock url="<URL>" />`. The existing `<details>`/`<summary>`
collapsibles around blocks are kept — they're an authoring choice, not
part of the rendering bug.
- Updated `apps/docs/components/github-code-block.tsx` to render via
`DynamicCodeBlock` from `fumadocs-ui/components/dynamic-codeblock`
(proper shiki highlighting) instead of raw `CodeBlock` + `Pre` (which
produced unhighlighted output). Also fixed language detection so a URL
hash like `#L10-L20` no longer pollutes the language token.
- Registered `GithubCodeBlock` globally in
`apps/docs/mdx-components.tsx`, matching how every other shared
component (`APIPage`, `Callout`, `Tab/Tabs`, `Step/Steps`, `Admonition`,
`TerminologyUpdate`) is exposed. MDX files no longer need a local
`import`.

# Additional Changes

- Normalized the two MDX files that were already using the JSX form
(`examples/secure-api/python-django.mdx`,
`examples/secure-api/java-spring.mdx`): removed their now-redundant
local `import { GithubCodeBlock }` and rewrote 9 long-form
`<GithubCodeBlock url="..."></GithubCodeBlock>` tags to self-closing for
consistency.

# Additional Context

Verified locally with `pnpm --filter @zitadel/docs dev`:

- `/docs/apis/actions/code-examples` — 20 shiki-highlighted code blocks
rendered inside the `<details>` collapsibles (was 0).
- `/docs/apis/openidoauth/claims` — line-range hashes (`#L9-L11`)
honored.
- `/docs/examples/login/flutter` — mixed languages (xml/dart/html)
detected and highlighted.
- `/docs/guides/integrate/external-audit-log` — edge case of fenced
reference indented inside a numbered list also converted and rendered.

Greps:
- `^[ \t]*\`\`\`[a-zA-Z0-9]+ reference` in `apps/docs/content/**/*.mdx`
→ 0 matches.
- `<GithubCodeBlock url="` in `apps/docs/content/**/*.mdx` → 55 matches.
- `from '@/components/github-code-block'` in
`apps/docs/content/**/*.mdx` → 0 matches.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:02:44 -07:00

54 lines
2.1 KiB
TypeScript

import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
const REVALIDATE_SECONDS = 60 * 60 * 24;
function toRawGithubUrl(url: string): string {
const parsed = new URL(url);
if (parsed.protocol !== 'https:' || parsed.hostname !== 'github.com') {
throw new Error(`GithubCodeBlock only accepts https://github.com URLs, got: ${url}`);
}
const segments = parsed.pathname.split('/').filter(Boolean);
if (segments.length < 5 || segments[2] !== 'blob') {
throw new Error(`Unexpected GitHub URL shape, expected /<owner>/<repo>/blob/<ref>/<path>, got: ${url}`);
}
const [owner, repo, , ref, ...path] = segments;
return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path.join('/')}`;
}
function parseLineRange(hash: string): { start: number; end: number } | null {
const match = hash.match(/L(\d+)(?:-L(\d+))?/);
if (!match) return null;
const start = parseInt(match[1], 10);
const end = match[2] ? parseInt(match[2], 10) : start;
return { start, end };
}
function sliceAndDedent(code: string, range: { start: number; end: number }): string {
const selected = code.split('\n').slice(range.start - 1, range.end);
const minIndent = selected.reduce((min, line) => {
if (line.trim().length === 0) return min;
const indent = line.match(/^\s*/)?.[0].length ?? 0;
return Math.min(min, indent);
}, Infinity);
const dedent = minIndent === Infinity ? 0 : minIndent;
return selected.map(line => line.slice(dedent)).join('\n');
}
export async function GithubCodeBlock({ url }: { url: string }) {
const rawUrl = toRawGithubUrl(url);
const response = await fetch(rawUrl, { next: { revalidate: REVALIDATE_SECONDS } });
if (!response.ok) {
throw new Error(`GithubCodeBlock failed to fetch ${rawUrl}: ${response.status} ${response.statusText}`);
}
let code = await response.text();
const range = parseLineRange(new URL(url).hash);
if (range) {
code = sliceAndDedent(code, range);
}
const lang = new URL(url).pathname.split('.').pop() || 'text';
return <DynamicCodeBlock lang={lang} code={code} />;
}