From 90c722d94c1ca48220bc0a519e95a543404137b3 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 6 Oct 2026 15:19:31 +0200 Subject: [PATCH] fix(wiki): resolve relative links between vault files The "From:" link on every document page in the wiki led to a 404. The vault links files relative to each other, which Forgejo and Obsidian resolve, but the wiki reads every link as a path from the vault root, so `../../../correspondents/springfield-power.md` landed at `/correspondents/springfield-power` instead of `/family/correspondents/springfield-power`. The wiki now resolves a relative link against its file's directory before it builds the page. The vault files are unchanged. --- stacklets/memory/Dockerfile | 1 + stacklets/memory/quartz/plugins/vaultLinks.ts | 34 +++++++++++++++++++ stacklets/memory/quartz/quartz.config.ts | 3 ++ 3 files changed, 38 insertions(+) create mode 100644 stacklets/memory/quartz/plugins/vaultLinks.ts diff --git a/stacklets/memory/Dockerfile b/stacklets/memory/Dockerfile index ef5cc086..70b67c9c 100644 --- a/stacklets/memory/Dockerfile +++ b/stacklets/memory/Dockerfile @@ -87,6 +87,7 @@ COPY quartz/components/FamilyHead.tsx ./quartz/components/FamilyHead.tsx COPY quartz/components/FamilyCrumbs.tsx ./quartz/components/FamilyCrumbs.tsx COPY quartz/components/scripts/familyNav.inline.ts ./quartz/components/scripts/familyNav.inline.ts COPY quartz/plugins/familyLists.tsx ./quartz/plugins/emitters/familyLists.tsx +COPY quartz/plugins/vaultLinks.ts ./quartz/plugins/transformers/vaultLinks.ts # The shipped ontology, for the topic areas a vault installed earlier # does not name yet (see readOntology in familyModel.ts). COPY seeds/ontology.toml ./ontology.seed.toml diff --git a/stacklets/memory/quartz/plugins/vaultLinks.ts b/stacklets/memory/quartz/plugins/vaultLinks.ts new file mode 100644 index 00000000..7231d32d --- /dev/null +++ b/stacklets/memory/quartz/plugins/vaultLinks.ts @@ -0,0 +1,34 @@ +import { QuartzTransformerPlugin } from "../types" +import { Root, Link } from "mdast" +import { visit } from "unist-util-visit" +import path from "path" + +// NEW TRANSFORMER (not an upstream override). +// +// The vault writes links between its own files relative to the file, +// `[Springfield Power](../../../correspondents/springfield-power.md)`, +// because that is the form Forgejo and Obsidian resolve. CrawlLinks runs +// with markdownLinkResolution "absolute" (see quartz.config.ts) and reads +// every link as a path from the vault root, `..` included, so such a link +// lands one or more levels too high and 404s. This resolves each relative +// link against its file's directory first and hands CrawlLinks the +// root path it expects. A link that would climb out of the vault is left +// as it is. + +const RELATIVE = /^\.\.?\// + +export const VaultLinks: QuartzTransformerPlugin = () => ({ + name: "VaultLinks", + markdownPlugins() { + return [ + () => (tree: Root, file) => { + const dir = path.posix.dirname(file.data.relativePath ?? "") + visit(tree, "link", (node: Link) => { + if (!RELATIVE.test(node.url)) return + const resolved = path.posix.normalize(path.posix.join(dir, node.url)) + if (!resolved.startsWith("..")) node.url = "/" + resolved + }) + }, + ] + }, +}) diff --git a/stacklets/memory/quartz/quartz.config.ts b/stacklets/memory/quartz/quartz.config.ts index 63f7814d..98db3973 100644 --- a/stacklets/memory/quartz/quartz.config.ts +++ b/stacklets/memory/quartz/quartz.config.ts @@ -14,6 +14,7 @@ import { QuartzConfig } from "./quartz/cfg" import * as Plugin from "./quartz/plugins" import { FamilyLists } from "./quartz/plugins/emitters/familyLists" +import { VaultLinks } from "./quartz/plugins/transformers/vaultLinks" // Runtime env. `WIKI_HOST` is "memory." in domain mode and // the empty-domain rendering "memory." in port mode — we treat the @@ -136,6 +137,8 @@ const config: QuartzConfig = { Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), Plugin.GitHubFlavoredMarkdown(), Plugin.TableOfContents(), + // Relative vault links to root paths, before CrawlLinks reads them. + VaultLinks(), Plugin.CrawlLinks({ markdownLinkResolution: "absolute" }), Plugin.Description(), // MathJax, not KaTeX, and the reason is privacy rather than