fix(docs): resolve relative markdown and wiki links across Fumadocs and GitHub wiki (#11834)

Resolução de links relativos entre Fumadocs e a wiki do GitHub, com dois arquivos de teste novos e bem focados (`docs-link-resolver.test.ts`, `sync-wiki.test.ts`). Validado no worktree combinado. Obrigado!
This commit is contained in:
Andrew B.
2026-08-30 07:09:36 -05:00
committed by GitHub
parent e0029eb5a6
commit 4c187de99b
5 changed files with 433 additions and 10 deletions

View File

@@ -5,6 +5,7 @@ import { DEFAULT_LOCALE, LOCALE_COOKIE } from "@/i18n/config";
import fs from "node:fs";
import path from "node:path";
import { resolveSafeI18nSectionDir } from "@/lib/docsI18nPath";
import { resolveDocHref, normalizeDocsMarkdownLinks } from "@/lib/docsLinkResolver";
import { getTranslations } from "next-intl/server";
// ── Locale detection ────────────────────────────────────────────────────────
@@ -63,7 +64,9 @@ async function tryI18nFallback(slug: string[], locale: string): Promise<string |
import("marked"),
import("@/lib/docsSanitizer"),
]);
const html = marked.parse(body) as string;
const docRelPath = `${slug.join("/")}.md`;
const normalizedBody = normalizeDocsMarkdownLinks(body, docRelPath);
const html = marked.parse(normalizedBody) as string;
return sanitizeDocsHtml(html);
}
@@ -93,12 +96,18 @@ export default async function Page(props: { params: Promise<{ slug: string[] }>
);
}
// Default: English MDX rendered natively by Fumadocs
// Default: English MDX rendered natively by Fumadocs with resolved links
const MDX = page.data.body;
const docPath = page.file?.path || `${params.slug.join("/")}.md`;
const DocsLink = (linkProps: React.ComponentProps<typeof defaultMdxComponents.a>) => {
const resolved = linkProps.href ? resolveDocHref(linkProps.href, docPath) : linkProps.href;
return <defaultMdxComponents.a {...linkProps} href={resolved} />;
};
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsBody>
<MDX components={{ ...defaultMdxComponents }} />
<MDX components={{ ...defaultMdxComponents, a: DocsLink }} />
</DocsBody>
</DocsPage>
);

108
src/lib/docsLinkResolver.ts Normal file
View File

@@ -0,0 +1,108 @@
import path from "node:path";
export const GITHUB_REPO_BLOB_URL = "https://github.com/diegosouzapw/OmniRoute/blob/main";
/**
* Resolves a doc link (e.g. `../routing/AUTO-COMBO.md#14-factors`, `./RESILIENCE_GUIDE.md`,
* or `../../src/lib/db/core.ts`) into a proper Next.js Fumadocs route path or GitHub blob URL.
*
* - Relative doc links within `docs/` -> `/docs/<section>/<slug>[#anchor]`
* - Root-level doc paths (`/docs/...` or `docs/...`) -> `/docs/<section>/<slug>[#anchor]`
* - Links to repository source code or root files outside `docs/` -> GitHub blob URL
* - Pure anchor links (`#section`) and external links (`https://...`) -> untouched
*/
export function resolveDocHref(href: string, currentDocRelPath: string = ""): string {
if (!href || /^(?:https?:|mailto:|#)/i.test(href)) {
return href;
}
const [rawPath, anchor] = href.includes("#")
? [href.slice(0, href.indexOf("#")), href.slice(href.indexOf("#") + 1)]
: [href, ""];
const anchorSuffix = anchor ? `#${anchor}` : "";
if (!rawPath) {
return href;
}
// Absolute /docs/... or docs/... links
if (/^\/?docs\//i.test(rawPath)) {
const cleaned = rawPath.replace(/^\/?docs\//i, "");
if (cleaned.toLowerCase().endsWith(".md")) {
return `/docs/${cleaned.slice(0, -3)}${anchorSuffix}`;
}
return `/docs/${cleaned}${anchorSuffix}`;
}
// Relative links starting with ./ or ../
if (rawPath.startsWith("./") || rawPath.startsWith("../")) {
const currentDir = currentDocRelPath ? path.dirname(currentDocRelPath) : "";
const resolvedInDocs = path.normalize(path.join(currentDir, rawPath)).replace(/\\/g, "/");
// If the path escapes the docs/ tree into repo root (e.g. ../../src/... or ../../package.json)
if (resolvedInDocs.startsWith("../") || resolvedInDocs === "..") {
const repoRel = path.normalize(path.join("docs", currentDir, rawPath)).replace(/\\/g, "/");
return `${GITHUB_REPO_BLOB_URL}/${repoRel}${anchorSuffix}`;
}
// Inside docs/ ending with .md -> strip .md to form Fumadocs route slug
if (resolvedInDocs.toLowerCase().endsWith(".md")) {
return `/docs/${resolvedInDocs.slice(0, -3)}${anchorSuffix}`;
}
// Static asset inside docs/ (e.g. diagrams/exported/foo.svg)
if (/\.(?:png|jpe?g|gif|svg|webp|json|yaml|yml|ts|js|mjs)$/i.test(resolvedInDocs)) {
return `${GITHUB_REPO_BLOB_URL}/docs/${resolvedInDocs}${anchorSuffix}`;
}
return `/docs/${resolvedInDocs}${anchorSuffix}`;
}
// Relative doc path without leading dots, e.g. "routing/AUTO-COMBO.md"
if (rawPath.toLowerCase().endsWith(".md")) {
return `/docs/${rawPath.slice(0, -3)}${anchorSuffix}`;
}
return href;
}
/**
* Normalizes all relative markdown links in a raw markdown string for HTML/i18n rendering.
*/
export function normalizeDocsMarkdownLinks(
markdown: string,
currentDocRelPath: string = ""
): string {
if (!markdown) return markdown;
let out = markdown;
// 1. Inline links: [text](url "title"?)
out = out.replace(
/(^|[^!])\[([^\]]*)\]\(([^)\s]+)(\s+["'][^"']*["'])?\)/g,
(_match, lead, text, href, title) => {
const newHref = resolveDocHref(href, currentDocRelPath);
return `${lead}[${text}](${newHref}${title || ""})`;
}
);
// 2. Reference links: ^[label]: href "title"?
out = out.replace(
/^\[([^\]]+)\]:\s*([^\s]+)(\s+["'][^"']*["'])?$/gm,
(_match, label, href, title) => {
const newHref = resolveDocHref(href, currentDocRelPath);
return `[${label}]: ${newHref}${title || ""}`;
}
);
// 3. HTML anchors: <a ... href="..." ...>
out = out.replace(
/<a\b([^>]*\bhref=["'])([^"']+)(["'][^>]*)>/gi,
(_match, before, href, after) => {
const newHref = resolveDocHref(href, currentDocRelPath);
return `<a${before}${newHref}${after}>`;
}
);
return out;
}