|
1 | 1 | /** |
2 | | - * Rehype plugin that rewrites relative .md links for Astro's directory-based output. |
| 2 | + * Rehype plugin that rewrites relative .md links to absolute paths for Astro's directory-based output. |
3 | 3 | * |
4 | 4 | * Authors write GitHub-friendly relative links with .md extensions: |
5 | 5 | * [Quickstart](quickstart.md) |
6 | 6 | * [Concepts](../concepts/canisters.md#lifecycle) |
7 | 7 | * |
8 | | - * Astro outputs each page as a directory (project-structure.md → project-structure/index.html), |
9 | | - * so the browser resolves relative links one level deeper than the author expects. |
10 | | - * This plugin strips .md extensions and prepends an extra ../ to compensate. |
| 8 | + * Astro outputs each page as a directory (resource-limits.md → resource-limits/index.html). |
| 9 | + * Relative hrefs in the output HTML are resolved by the browser relative to the current URL. |
| 10 | + * This means links break when the page is accessed without a trailing slash (e.g. /resource-limits |
| 11 | + * instead of /resource-limits/) because `../foo/` resolves to different paths in each case. |
11 | 12 | * |
12 | | - * Exception: index.md files are output as <dir>/index.html (not <dir>/<dir>/index.html), |
13 | | - * so the browser's base URL is already at the correct directory level — no extra ../ |
14 | | - * is needed for those pages. |
| 13 | + * This plugin avoids the ambiguity by emitting absolute paths. It locates the current file |
| 14 | + * within the docs tree, resolves the relative .md link against that position, and writes a |
| 15 | + * root-relative href that works regardless of whether the browser URL has a trailing slash. |
15 | 16 | * |
16 | | - * Result (regular pages): |
17 | | - * quickstart.md → ../quickstart/ |
18 | | - * ../concepts/canisters.md#lifecycle → ../../concepts/canisters/#lifecycle |
19 | | - * ./sibling.md → ../sibling/ |
| 17 | + * Result: |
| 18 | + * quickstart.md → /getting-started/quickstart/ |
| 19 | + * ../concepts/canisters.md#lifecycle → /concepts/canisters/#lifecycle |
| 20 | + * ./sibling.md → /references/sibling/ |
| 21 | + * backends/data-persistence.md → /guides/backends/data-persistence/ |
20 | 22 | * |
21 | | - * Result (index pages): |
22 | | - * backends/data-persistence.md → backends/data-persistence/ |
23 | | - * ../concepts/canisters.md → ../concepts/canisters/ |
24 | | - * |
25 | | - * Only relative links are affected — external URLs, anchors, and absolute paths are untouched. |
| 23 | + * Only relative links with a .md extension are affected — external URLs, anchor-only links, |
| 24 | + * and already-absolute paths are untouched. |
26 | 25 | * |
27 | 26 | * Important: Astro caches rendered content in node_modules/.astro/data-store.json. |
28 | 27 | * After changing this plugin, delete that file to force re-rendering. |
|
31 | 30 | * Astro's markdown.remarkPlugins, but rehypePlugins are correctly merged. See: |
32 | 31 | * https://github.com/dfinity/icp-cli/issues/423 |
33 | 32 | */ |
| 33 | +import { posix as posixPath } from "path"; |
34 | 34 | import { visit } from "unist-util-visit"; |
35 | 35 |
|
36 | 36 | export default function rehypeRewriteLinks() { |
37 | 37 | return (tree, file) => { |
38 | | - // Detect whether this file is an index page (e.g. guides/index.md). |
39 | | - // Index pages are output as <dir>/index.html, so the browser's base URL |
40 | | - // is already at the directory level — no extra ../ compensation needed. |
41 | | - const filePath = file?.path || file?.history?.[0] || ""; |
42 | | - const isIndexPage = /(?:^|[\\/])index\.(?:md|mdx)$/.test(filePath); |
| 38 | + const filePath = (file?.path || file?.history?.[0] || "").replace(/\\/g, "/"); |
| 39 | + |
| 40 | + // Extract the docs-relative directory of the current file. |
| 41 | + // Handles both the real path (.../docs/references/resource-limits.md) |
| 42 | + // and the symlinked path (.../src/content/docs/references/resource-limits.md). |
| 43 | + const docsRelMatch = filePath.match(/(?:\/src\/content\/docs|\/docs)\/(.*)/); |
| 44 | + const docsRelPath = docsRelMatch ? docsRelMatch[1] : ""; |
| 45 | + // e.g. "references/resource-limits.md" → "references/" |
| 46 | + const fileDir = docsRelPath.replace(/[^/]+$/, ""); |
43 | 47 |
|
44 | 48 | visit(tree, "element", (node) => { |
45 | 49 | if (node.tagName !== "a") return; |
@@ -68,30 +72,28 @@ export default function rehypeRewriteLinks() { |
68 | 72 | url = url.replace(/(^|\/)index(#|$|\?)/, "$1$2"); |
69 | 73 |
|
70 | 74 | // Split off anchor/query suffix |
71 | | - const splitMatch = url.match(/^([^#?]*)((?:#|\\?).*)?$/); |
72 | | - let path = splitMatch[1] || ""; |
| 75 | + const splitMatch = url.match(/^([^#?]*)((?:#|\?).*)?$/); |
| 76 | + let linkPath = splitMatch[1] || ""; |
73 | 77 | const suffix = splitMatch[2] || ""; |
74 | 78 |
|
75 | 79 | // Add trailing slash if the path doesn't already end with one |
76 | | - if (path && !path.endsWith("/")) { |
77 | | - path += "/"; |
| 80 | + if (linkPath && !linkPath.endsWith("/")) { |
| 81 | + linkPath += "/"; |
78 | 82 | } |
79 | 83 |
|
80 | | - // Strip leading ./ if present (normalize before prepending ../) |
81 | | - if (path.startsWith("./")) { |
82 | | - path = path.slice(2); |
| 84 | + // Strip leading ./ if present |
| 85 | + if (linkPath.startsWith("./")) { |
| 86 | + linkPath = linkPath.slice(2); |
83 | 87 | } |
84 | 88 |
|
85 | | - // Prepend ../ to compensate for Astro's directory-based output. |
86 | | - // Regular pages (e.g. project-structure.md → project-structure/index.html) |
87 | | - // need the extra ../ because the browser base is one level deeper than |
88 | | - // the author expects. Index pages don't need this — they're already at |
89 | | - // the correct directory level. |
90 | | - if (!isIndexPage) { |
91 | | - path = "../" + path; |
92 | | - } |
| 89 | + // Resolve the relative link against the current file's absolute docs path. |
| 90 | + // posixPath.resolve strips trailing slashes, so re-add one afterward. |
| 91 | + // This produces a root-relative href that works regardless of whether the |
| 92 | + // browser URL has a trailing slash. |
| 93 | + const resolved = posixPath.resolve("/" + fileDir, linkPath || "."); |
| 94 | + const absoluteHref = resolved === "/" ? "/" : resolved + "/"; |
93 | 95 |
|
94 | | - node.properties.href = path + suffix; |
| 96 | + node.properties.href = absoluteHref + suffix; |
95 | 97 | }); |
96 | 98 | }; |
97 | 99 | } |
0 commit comments