From 3e40202a09773c638bb4e45fc1f09a8e3c546a7a Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sat, 21 Feb 2026 13:57:07 +1100 Subject: [PATCH] docs: fix custom-site internal link routing --- docs-builder/build.js | 64 +++++++++++++++++++++++++++++++++++-- docs/releases/unreleased.md | 2 ++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/docs-builder/build.js b/docs-builder/build.js index 2aa31c12..8fa8758a 100644 --- a/docs-builder/build.js +++ b/docs-builder/build.js @@ -62,6 +62,24 @@ function parseMarkdown(raw) { return { fm, html }; } +function splitHref(href) { + const queryIndex = href.indexOf('?'); + const hashIndex = href.indexOf('#'); + let end = href.length; + if (queryIndex !== -1 && queryIndex < end) end = queryIndex; + if (hashIndex !== -1 && hashIndex < end) end = hashIndex; + return { pathPart: href.slice(0, end), suffix: href.slice(end) }; +} + +function mdLinkPathToUrl(linkPath, mdPath) { + const pageDir = '/' + path.posix.dirname(mdPath).replace(/^\.(?:\/|$)/, ''); + const resolved = linkPath.startsWith('/') + ? path.posix.normalize(linkPath) + : path.posix.normalize(path.posix.join(pageDir || '/', linkPath)); + const relativeMdPath = resolved.replace(/^\/+/, ''); + return mdPathToUrl(relativeMdPath); +} + // Rewrite relative image src/href paths to absolute URL paths. // Markdown files reference assets relative to their own location, but the // built HTML is served from a URL path that doesn't match the source path @@ -76,6 +94,24 @@ function resolveAssetPaths(html, mdPath) { }); } +// Rewrite local markdown links to route URLs so links stay valid regardless +// of the current page path (e.g. `foo.md` -> `/foo/`). +function resolveMarkdownLinks(html, mdPath) { + return html.replace(/(]*\shref=")([^"]+)(")/g, (_, pre, href, post) => { + if (!href) return _; + if (href.startsWith('#')) return _; + if (href.startsWith('//')) return _; + if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(href)) return _; + + const { pathPart, suffix } = splitHref(href); + if (!pathPart) return _; + if (!/\.md$/i.test(pathPart)) return _; + + const rewritten = mdLinkPathToUrl(pathPart, mdPath); + return pre + rewritten + suffix + post; + }); +} + function extractTitle(html, fm) { if (fm.title) return fm.title; const m = html.match(/]*>([\s\S]*?)<\/h1>/); @@ -158,6 +194,26 @@ async function ensureDir(p) { await fs.mkdir(p, { recursive: true }); } +async function listMarkdownFiles(rootDir) { + const files = []; + + async function walk(dir) { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + await walk(fullPath); + continue; + } + if (!entry.isFile() || !entry.name.endsWith('.md')) continue; + files.push(path.relative(rootDir, fullPath).split(path.sep).join('/')); + } + } + + await walk(rootDir); + return files; +} + async function readSource(mdPath) { // Local override takes precedence over the source docs file const override = path.join(SRC, 'overrides', mdPath); @@ -204,8 +260,10 @@ async function main() { await fs.copyFile(cname, path.join(DIST, 'CNAME')); } - // Build every page listed in the nav - const pages = flattenNav(nav); + // Build every markdown doc so cross-links outside the nav still resolve + const navPages = flattenNav(nav); + const allDocPages = await listMarkdownFiles(DOCS); + const pages = [...new Set([...navPages, ...allDocPages])]; let built = 0, skipped = 0; for (const mdPath of pages) { @@ -213,7 +271,7 @@ async function main() { if (!raw) { skipped++; continue; } const { fm, html: rawHtml } = parseMarkdown(raw); - const html = resolveAssetPaths(rawHtml, mdPath); + const html = resolveMarkdownLinks(resolveAssetPaths(rawHtml, mdPath), mdPath); const url = mdPathToUrl(mdPath); const title = extractTitle(html, fm); const body = stripH1(html); diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 3d3d4024..3eee5db9 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -35,3 +35,5 @@ Example: - Thanks to @martin-forge for reporting - Consolidated documentation cleanup for accuracy and clarity across API, webhook, NLP, privacy, settings, and view docs (corrected outdated endpoint/behavior details, normalized current settings paths, and tightened non-release prose) - Fixed a broken docs cross-reference from Property Types Reference to Task Properties settings +- Fixed docs site link generation so internal Markdown links resolve to route URLs instead of broken `.md` paths (for example `/views/default-base-templates/`) +- Fixed docs release-note links by building all Markdown docs pages, including pages not listed directly in sidebar nav