fix: preserve nested navigation logos

This commit is contained in:
dadachann
2026-09-04 14:52:36 +00:00
parent c317ca6ea7
commit dfcec6b7db
4 changed files with 76 additions and 4 deletions
+4 -3
View File
@@ -14,6 +14,7 @@
"fumadocs-ui": "npm:@fumadocs/base-ui@^16.15.6",
"fumapress": "^1.2.0",
"hono": "^4.9.9",
"lucide-react": "^1.41.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"shiki": "^3.20.0",
@@ -4143,9 +4144,9 @@
}
},
"node_modules/lucide-react": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.40.0.tgz",
"integrity": "sha512-MaG+8WnOXkDWz9XeElj7TnQ890tTZUB0a36i03aRRCKGWE6e7jJpmdCvtxxuxcjjdqyN6m4sL6qlHVuSUDtYgg==",
"version": "1.41.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.41.0.tgz",
"integrity": "sha512-6lksP35l6KszDKUeRTi4LV7i6DEe0Yzl2ALJm9j4c5xEYN91GdW1xGsawGMOg2mgjF5GHBVX8pKX9kP+cWsP3Q==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+1
View File
@@ -29,6 +29,7 @@
"fumadocs-ui": "npm:@fumadocs/base-ui@^16.15.6",
"fumapress": "^1.2.0",
"hono": "^4.9.9",
"lucide-react": "^1.41.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"shiki": "^3.20.0",
+37 -1
View File
@@ -9,6 +9,7 @@ import { update } from "fumadocs-core/source";
import { uiTranslations } from "fumadocs-ui/i18n";
import { defineDocs } from "fumadocs-mdx/macro";
import { createOpenAPI, type OpenAPIServer } from "fumadocs-openapi/server";
import { Bot } from "lucide-react";
import { readFileSync, readdirSync } from "node:fs";
import path from "node:path";
@@ -23,6 +24,9 @@ function nodeName(node: PageTreeNode) {
}
function renderNavigationIcon(icon: PageTreeNode["icon"]) {
if (icon === "robot") {
return <Bot aria-hidden="true" className="size-4 shrink-0" />;
}
if (typeof icon !== "string" || !icon.startsWith("/")) return icon;
return (
<img
@@ -48,6 +52,35 @@ function renderNavigationIcons(node: PageTreeNode): PageTreeNode {
return { ...node, icon: renderNavigationIcon(node.icon) };
}
type ConfiguredNavigationGroup = {
group: string;
icon?: string;
pages?: unknown[];
};
function restoreConfiguredFolderIcons(
nodes: PageTreeNode[],
configuredPages: unknown[],
): PageTreeNode[] {
const configuredGroups = new Map<string, ConfiguredNavigationGroup>();
for (const entry of configuredPages) {
if (!entry || typeof entry !== "object" || !("group" in entry)) continue;
const group = entry as ConfiguredNavigationGroup;
configuredGroups.set(group.group, group);
}
return nodes.map((node) => {
if (node.type !== "folder") return node;
const configured = configuredGroups.get(nodeName(node));
if (!configured) return node;
return {
...node,
icon: configured.icon ?? node.icon,
children: restoreConfiguredFolderIcons(node.children, configured.pages ?? []),
};
});
}
const openApiTagOrder = Object.fromEntries(LOCALES.map((locale) => {
const spec = JSON.parse(readFileSync(`openapi/service-api-${locale}.json`, "utf8"));
const tags: string[] = [];
@@ -115,7 +148,10 @@ function sectionizeMintlifyTree(
const sourceGroup = sourceGroups.get(group.group);
if (!sourceGroup) continue;
if (sourceGroup.index) children.push(sourceGroup.index);
children.push(...sourceGroup.children);
children.push(...restoreConfiguredFolderIcons(
sourceGroup.children,
"pages" in group ? group.pages ?? [] : [],
));
}
const tab: Folder = { ...node, root: true, children };
return tab;
+34
View File
@@ -119,6 +119,40 @@ test("platform navigation renders logo assets instead of icon path text", async
}
});
test("nested bot groups preserve the logos declared in docs.json", async () => {
const html = await readFile(
path.join(publicRoot, "zh/usage/platforms/discord/index.html"),
"utf8",
);
const asideStart = html.indexOf('<aside id="nd-sidebar"');
const asideEnd = html.indexOf("</aside>", asideStart);
const sidebar = html.slice(asideStart, asideEnd);
for (const [title, logo] of [
["企业微信", "wecom.svg"],
["微信", "wechat.svg"],
["QQ 官方机器人", "qq.svg"],
["QQ (OneBot v11)", "qq.svg"],
]) {
const escapedTitle = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const escapedLogo = logo.replace(".", "\\.");
assert.match(
sidebar,
new RegExp(`<img[^>]+src="/images/platforms/${escapedLogo}"[^>]*>\\s*${escapedTitle}<svg`),
`zh sidebar is missing the ${logo} logo for ${title}`,
);
}
assert.doesNotMatch(
sidebar,
/>robot(?:<!-- -->)?Satori/,
"zh sidebar exposes the named robot icon as text",
);
assert.match(
sidebar,
/<svg[^>]+class="[^"]*lucide-bot[^"]*"[^>]*>[\s\S]*?<\/svg>Satori/,
"zh sidebar is missing the Satori robot icon",
);
});
test("all locales emit the OpenAPI surface with endpoint semantics", async () => {
const representatives = {
en: ["system/get-system-information", "Get system information", "GET", "/api/v1/system/info"],