Files
2026-07-13 12:38:34 +08:00

142 lines
4.9 KiB
TypeScript

/**
* Filters the combined page tree to show only the selected API version.
* v3.1 pages are at /reference/{page} and /reference/api-reference/{tag}/{op}
* v3.0 pages are at /reference/v3/{page} and /reference/v3/api-reference/{tag}/{op}
*
* For v3.1: hide the V3 folder entirely.
* For v3.0: lift V3 folder contents to the top, hide v3.1-only nodes.
*/
interface PageTreeNode {
type: 'page' | 'folder' | 'separator';
name?: unknown;
url?: string;
children?: PageTreeNode[];
index?: PageTreeNode;
[key: string]: unknown;
}
interface PageTreeRoot {
children: PageTreeNode[];
[key: string]: unknown;
}
/**
* API-reference tags that we intentionally hide on our side even though the
* upstream OpenAPI spec (from hermes) still includes them. Matched by tag slug.
*
* Hiding happens in two places that must stay in sync:
* - `scripts/generate-api-index.ts` skips generating (and deletes) their
* `index.mdx` overview pages.
* - `filterHiddenTags` (below) drops their folders/pages from the reference
* page tree so the fumadocs-openapi operation pages disappear from the
* sidebar, llms.txt walk, and search.
*/
export const HIDDEN_API_TAGS: ReadonlySet<string> = new Set([
'consumer',
'invite-codes',
'authentication',
]);
/** True if a URL points at a hidden tag's pages (v3.1 or v3.0). */
function isHiddenTagUrl(url: string): boolean {
for (const tag of HIDDEN_API_TAGS) {
if (
url.startsWith(`/reference/api-reference/${tag}/`) ||
url === `/reference/api-reference/${tag}` ||
url.startsWith(`/reference/v3/api-reference/${tag}/`) ||
url === `/reference/v3/api-reference/${tag}`
) {
return true;
}
}
return false;
}
/** True if a node (page or folder) belongs entirely to a hidden tag. */
function isHiddenTagNode(node: PageTreeNode): boolean {
if (node.type === 'page' && typeof node.url === 'string') {
return isHiddenTagUrl(node.url);
}
if (node.type === 'folder') {
if (node.index && isHiddenTagNode(node.index)) return true;
// A folder whose every child is hidden (and has at least one) is itself hidden.
const children = node.children ?? [];
if (children.length > 0 && children.every(isHiddenTagNode)) return true;
}
return false;
}
/** Recursively drops folders/pages whose tag slug is in HIDDEN_API_TAGS. */
function filterHiddenTags(nodes: PageTreeNode[]): PageTreeNode[] {
return nodes
.filter((node) => !isHiddenTagNode(node))
.map((node) =>
node.type === 'folder' && node.children
? { ...node, children: filterHiddenTags(node.children) }
: node,
);
}
function isV3Node(node: PageTreeNode): boolean {
if (node.type === 'page' && typeof node.url === 'string') {
return node.url.startsWith('/reference/v3/') || node.url === '/reference/v3';
}
if (node.type === 'folder') {
if (node.index && isV3Node(node.index)) return true;
return node.children?.some(isV3Node) ?? false;
}
return false;
}
/** Checks if a folder contains v3.1 API reference pages (URLs under /reference/api-reference/). */
function isV31ApiFolder(node: PageTreeNode): boolean {
if (node.type !== 'folder') return false;
const hasV31ApiPage = (n: PageTreeNode): boolean => {
if (n.type === 'page' && typeof n.url === 'string') {
return n.url.startsWith('/reference/api-reference/');
}
if (n.type === 'folder') {
if (n.index && hasV31ApiPage(n.index)) return true;
return n.children?.some(hasV31ApiPage) ?? false;
}
return false;
};
if (node.index && hasV31ApiPage(node.index)) return true;
return node.children?.some(hasV31ApiPage) ?? false;
}
export function prepareTree<T extends PageTreeRoot>(tree: T, version: string): T {
// Drop intentionally-hidden tags (consumer, invite-codes) from the whole tree
// first, so neither version surfaces their operation pages.
const children = filterHiddenTags(tree.children as PageTreeNode[]);
if (version === '3.1') {
// Just hide the V3 folder
return {
...tree,
children: children.filter((node) => !isV3Node(node)),
};
}
// v3.0: lift V3 folder contents, keep version-independent folders (SDK Reference, Meta Tools)
const v3Folder = children.find(
(node) => node.type === 'folder' && isV3Node(node),
);
// Nodes that should appear in both versions (exclude v3 nodes, v3.1 API Reference folder,
// and top-level pages which are version-specific — both versions have their own copies)
const sharedNodes = children.filter(
(node) => node.type !== 'page' && !isV3Node(node) && !isV31ApiFolder(node),
);
if (v3Folder?.children) {
// Include the folder's index page (v3/index.mdx → overview) which fumadocs
// stores in .index rather than .children
const indexPage = v3Folder.index ? [v3Folder.index] : [];
return { ...tree, children: [...indexPage, ...v3Folder.children, ...sharedNodes] };
}
return { ...tree, children: [...children.filter(isV3Node), ...sharedNodes] };
}