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:
Federico Coppede
2026-09-11 10:47:53 -07:00
committed by GitHub
co-authored by Florian Forster Claude Fable 5.1
parent 7e34f04361
commit 8ccd5b206f
12 changed files with 284 additions and 1385 deletions
@@ -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 (![alt](src)) 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 = [
'## ![logo](/img/logo.png) Title',
'```bash',
'# [not a heading](https://example.com)',
'```',
].join('\n');
assert.strictEqual(unwrapHeadingLinks(input), input);
});
});
});