fix(build): remove extension-stripped slug aliases that pollute allSlugs

addVirtualPageSlugAliases injected extension-stripped aliases for
every PageType-registered extension into ctx.allSlugs (e.g. for
`Canvas.canvas` it added an alias `canvas`). Intended to help
`![[file.canvas]]` transclusions resolve to virtual pages, but:

1. CrawlLinks' shortest strategy treats allSlugs entries as real
   files. When an author writes `[[Canvas]]` in a note, shortest
   lookup matches BOTH the alias `canvas` and real basenames
   like `features/canvas`. With matchingFileNames.length === 2,
   the uniqueness check fails and transformLink falls back to
   absolute resolution, producing a dead href `./canvas` that
   points to no emitted file.

2. The aliases were never actually needed for transclusion.
   renderPage.tsx already has its own extension-stripping
   fallback (lines 107-119) that consults allFiles directly.
   Virtual pages (canvas-page, bases-page) emit at slugs that
   KEEP their extension (e.g. `canvas.canvas`), so
   `![[Canvas.canvas]]` resolves naturally without any alias.

3. The aliases mispresent Obsidian semantics. In Obsidian,
   `[[Canvas]]` resolves to `Canvas.md`; non-md files require
   the explicit extension (`[[Canvas.canvas]]`). Injecting
   extension-stripped aliases into allSlugs made Quartz claim
   bare names can refer to non-md virtual pages, which is
   wrong.

Reproduction: docs/features/index.md contains `[[Canvas]]` which
should resolve to docs/features/Canvas.md (slug features/canvas).
Under the old behavior it produced href="../canvas" with
data-slug="canvas" pointing nowhere. After this fix it produces
href="../features/canvas" with data-slug="features/canvas"
correctly.

Verified with fresh `npx quartz build -d docs`:
- [[Canvas]] → ./features/canvas ✓
- ![[Canvas.canvas]] transclusion still works ✓
- ![[Base.base]] virtual page still emits ✓
- 0 internal broken links across all output ✓
- all 91 existing tests pass
This commit is contained in:
saberzero1
2026-04-17 03:05:36 +02:00
parent 0572f06d08
commit c32d11cb8b

View File

@@ -9,7 +9,7 @@ import { parseMarkdown } from "./processors/parse"
import { filterContent } from "./processors/filter" import { filterContent } from "./processors/filter"
import { emitContent } from "./processors/emit" import { emitContent } from "./processors/emit"
import cfg from "../quartz" import cfg from "../quartz"
import { FilePath, FullSlug, joinSegments, slugifyFilePath } from "./util/path" import { FilePath, joinSegments, slugifyFilePath } from "./util/path"
import { detectSlugCollisions, formatCollisionWarning } from "./util/slugCollisions" import { detectSlugCollisions, formatCollisionWarning } from "./util/slugCollisions"
import chokidar from "chokidar" import chokidar from "chokidar"
import { ProcessedContent } from "./plugins/vfile" import { ProcessedContent } from "./plugins/vfile"
@@ -20,7 +20,7 @@ import { options } from "./util/sourcemap"
import { Mutex } from "async-mutex" import { Mutex } from "async-mutex"
import { getStaticResourcesFromPlugins } from "./plugins" import { getStaticResourcesFromPlugins } from "./plugins"
import { randomIdNonSecure } from "./util/random" import { randomIdNonSecure } from "./util/random"
import { ChangeEvent, QuartzPageTypePluginInstance } from "./plugins/types" import { ChangeEvent } from "./plugins/types"
import { minimatch } from "minimatch" import { minimatch } from "minimatch"
function reportSlugCollisions(content: ProcessedContent[]): void { function reportSlugCollisions(content: ProcessedContent[]): void {
@@ -29,37 +29,6 @@ function reportSlugCollisions(content: ProcessedContent[]): void {
console.warn(styleText("yellow", formatCollisionWarning(collisions))) console.warn(styleText("yellow", formatCollisionWarning(collisions)))
} }
function getPageTypeExtensions(ctx: BuildCtx): Set<string> {
const extensions = new Set<string>()
const pageTypes = (ctx.cfg.plugins.pageTypes ?? []) as unknown as QuartzPageTypePluginInstance[]
for (const pt of pageTypes) {
if (pt.fileExtensions) {
for (const ext of pt.fileExtensions) {
extensions.add(ext)
}
}
}
return extensions
}
// For files whose extensions are handled by PageType plugins (e.g. .canvas, .base),
// add extension-stripped slug aliases so that wikilink resolution (CrawlLinks) maps
// `![[file.canvas]]` to the virtual-page slug `file` instead of the raw `file.canvas`.
function addVirtualPageSlugAliases(allSlugs: FullSlug[], extensions: Set<string>): FullSlug[] {
const extra: FullSlug[] = []
for (const slug of allSlugs) {
for (const ext of extensions) {
if (slug.endsWith(ext)) {
const stripped = slug.slice(0, -ext.length) as FullSlug
if (!allSlugs.includes(stripped) && !extra.includes(stripped)) {
extra.push(stripped)
}
}
}
}
return extra
}
type ContentMap = Map< type ContentMap = Map<
FilePath, FilePath,
| { | {
@@ -121,14 +90,6 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
ctx.allFiles = allFiles ctx.allFiles = allFiles
ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath)) ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath))
// Add extension-stripped slug aliases for PageType-registered extensions
// so that wikilinks like ![[file.canvas]] resolve to virtual page slugs
const ptExtensions = getPageTypeExtensions(ctx)
if (ptExtensions.size > 0) {
const aliases = addVirtualPageSlugAliases(ctx.allSlugs, ptExtensions)
ctx.allSlugs.push(...aliases)
}
const parsedFiles = await parseMarkdown(ctx, filePaths) const parsedFiles = await parseMarkdown(ctx, filePaths)
reportSlugCollisions(parsedFiles) reportSlugCollisions(parsedFiles)
const filteredContent = filterContent(ctx, parsedFiles) const filteredContent = filterContent(ctx, parsedFiles)
@@ -305,12 +266,6 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
ctx.allFiles = Array.from(contentMap.keys()) ctx.allFiles = Array.from(contentMap.keys())
ctx.allSlugs = ctx.allFiles.map((fp) => slugifyFilePath(fp as FilePath)) ctx.allSlugs = ctx.allFiles.map((fp) => slugifyFilePath(fp as FilePath))
// Add extension-stripped slug aliases for PageType-registered extensions
const ptExtensions = getPageTypeExtensions(ctx)
if (ptExtensions.size > 0) {
const aliases = addVirtualPageSlugAliases(ctx.allSlugs, ptExtensions)
ctx.allSlugs.push(...aliases)
}
const markdownContent = Array.from(contentMap.values()) const markdownContent = Array.from(contentMap.values())
.filter((file) => file.type === "markdown") .filter((file) => file.type === "markdown")
.map((file) => file.content) .map((file) => file.content)