mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-02 20:32:19 +00:00
docs: migrate to native Fumadocs search & UI fixes (#12732)
Changes: - **Search**: Replaced Inkeep with Fumadocs' native built-in search (createFromSource). Removed @inkeep/cxkit-react and custom search dialogs. - **Hydration Fix**: Removed markdown links from ### headings in egress.mdx (across all versions) to fix the <a inside a> DOM mismatch error. - **Console Fix**: Added the missing key prop to <VersionSelector/> in the layout to resolve the React list warning. --------- Co-authored-by: Florian Forster <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Florian Forster
Claude Fable 5.1
parent
7e34f04361
commit
8ccd5b206f
@@ -83,7 +83,38 @@ export function resetCache() {
|
||||
cachedRef = null;
|
||||
}
|
||||
|
||||
// Parses `git ls-remote --tags` output into the `{ name }` shape the GitHub API returns.
|
||||
export function parseLsRemoteTags(output) {
|
||||
const names = new Set();
|
||||
for (const line of output.split('\n')) {
|
||||
const ref = line.split('\t')[1];
|
||||
if (!ref?.startsWith('refs/tags/')) continue;
|
||||
// annotated tags are listed twice, the second time peeled as `<tag>^{}`
|
||||
names.add(ref.slice('refs/tags/'.length).replace(/\^\{\}$/, ''));
|
||||
}
|
||||
return [...names].map((name) => ({ name }));
|
||||
}
|
||||
|
||||
async function fetchTags() {
|
||||
// Unauthenticated GitHub REST API calls are limited to 60/hour per IP, which shared
|
||||
// build runners such as Vercel regularly exhaust. `git ls-remote` is not rate limited.
|
||||
const remote = `https://github.com/${REPO}.git`;
|
||||
try {
|
||||
console.log(`Listing tags via git ls-remote ${remote}...`);
|
||||
const output = execSync(`git ls-remote --tags ${remote}`, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
||||
});
|
||||
const tags = parseLsRemoteTags(output);
|
||||
if (tags.length === 0) throw new Error('no tags returned');
|
||||
console.log(`Found ${tags.length} tags.`);
|
||||
return tags;
|
||||
} catch (err) {
|
||||
console.warn(`[tags] git ls-remote failed (${safeLog(err.message)}), falling back to the GitHub API...`);
|
||||
}
|
||||
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const headers = { 'User-Agent': 'node-fetch' };
|
||||
if (token) headers['Authorization'] = `token ${token}`;
|
||||
@@ -449,6 +480,42 @@ async function fixRelativeImports(versionDir, tagOrBranch) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* fumadocs wraps heading text in its own anchor (`<a href="#id">`), so a markdown
|
||||
* link inside a heading ends up as nested `<a>` tags. Browsers split those while
|
||||
* parsing, which breaks React hydration (error #418) and the heading's anchor.
|
||||
* Latest content is fixed at the source; versioned content is downloaded from git
|
||||
* tags, so the link is unwrapped into plain heading text here instead.
|
||||
*/
|
||||
export function unwrapHeadingLinks(content) {
|
||||
let inFence = false;
|
||||
return content
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (/^\s*(```|~~~)/.test(line)) inFence = !inFence;
|
||||
if (inFence || !/^#{1,6}\s/.test(line)) return line;
|
||||
// [text](url) -> text; leaves images () alone
|
||||
return line.replace(/(?<!!)\[([^\]]+)\]\([^)]*\)/g, '$1');
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function fixHeadingLinks(versionDir) {
|
||||
if (!fs.existsSync(versionDir)) return;
|
||||
for (const file of fs.readdirSync(versionDir, { recursive: true })) {
|
||||
const filePath = join(versionDir, file);
|
||||
if (!filePath.endsWith('.mdx') && !filePath.endsWith('.md')) continue;
|
||||
if (!fs.statSync(filePath).isFile()) continue;
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const fixed = unwrapHeadingLinks(content);
|
||||
if (fixed !== content) {
|
||||
console.log(`[fix-headings] Unwrapped link in heading: ${file}`);
|
||||
fs.writeFileSync(filePath, fixed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLocalVersion() {
|
||||
const vercelBranch = process.env.VERCEL_GIT_COMMIT_REF;
|
||||
let branch = vercelBranch;
|
||||
@@ -515,6 +582,7 @@ async function run() {
|
||||
// Correctly pass sourceRef here so external files are fetched from the same place (local or remote)
|
||||
await fixRelativeImports(contentDest, sourceRef);
|
||||
}
|
||||
fixHeadingLinks(contentDest);
|
||||
}));
|
||||
|
||||
const versionsJson = [
|
||||
|
||||
@@ -2,7 +2,7 @@ import { test, describe, beforeEach, afterEach, mock } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getCurrentRef, resetCache, downloadFileContent, isValidRef, safeLog } from './fetch-remote-content.mjs';
|
||||
import { getCurrentRef, resetCache, downloadFileContent, isValidRef, safeLog, unwrapHeadingLinks, parseLsRemoteTags } from './fetch-remote-content.mjs';
|
||||
|
||||
const TEST_TMP_DIR = path.join(process.cwd(), '.test-tmp');
|
||||
const MOCK_REPO_ROOT = path.join(TEST_TMP_DIR, 'mock-repo');
|
||||
@@ -158,4 +158,45 @@ describe('fetch-remote-content', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- parseLsRemoteTags Tests ---
|
||||
describe('parseLsRemoteTags', () => {
|
||||
test('extracts tag names and de-duplicates peeled annotated tags', () => {
|
||||
const output = [
|
||||
'aaa\trefs/tags/v4.17.3',
|
||||
'bbb\trefs/tags/v4.17.3^{}',
|
||||
'ccc\trefs/tags/v4.16.0',
|
||||
'ddd\trefs/heads/main',
|
||||
'',
|
||||
].join('\n');
|
||||
assert.deepStrictEqual(parseLsRemoteTags(output), [{ name: 'v4.17.3' }, { name: 'v4.16.0' }]);
|
||||
});
|
||||
});
|
||||
|
||||
// --- unwrapHeadingLinks Tests ---
|
||||
describe('unwrapHeadingLinks', () => {
|
||||
test('unwraps markdown links inside headings only', () => {
|
||||
const input = [
|
||||
'### [Identity Providers & Federation](/guides/integrate/identity-providers/introduction)',
|
||||
'#### **1. [List my project roles](/reference/api/auth/AuthService.ListMyProjectPermissions)**',
|
||||
'A paragraph with a [link](/kept) is left untouched.',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
'### Identity Providers & Federation',
|
||||
'#### **1. List my project roles**',
|
||||
'A paragraph with a [link](/kept) is left untouched.',
|
||||
].join('\n');
|
||||
assert.strictEqual(unwrapHeadingLinks(input), expected);
|
||||
});
|
||||
|
||||
test('leaves images in headings and comments inside code fences alone', () => {
|
||||
const input = [
|
||||
'##  Title',
|
||||
'```bash',
|
||||
'# [not a heading](https://example.com)',
|
||||
'```',
|
||||
].join('\n');
|
||||
assert.strictEqual(unwrapHeadingLinks(input), input);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user