From f4f64e121c64b05aec753e185e63cf3613e30d46 Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Fri, 27 Feb 2026 01:41:57 +0100 Subject: [PATCH] fix(links): virtual page transclusion --- quartz/components/renderPage.tsx | 14 +++++++- quartz/plugins/pageTypes/dispatcher.ts | 50 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index 8cf54392c..5907967bb 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -102,7 +102,19 @@ function renderTranscludes( } visited.add(transcludeTarget) - const page = componentData.allFiles.find((f) => f.slug === transcludeTarget) + let page = componentData.allFiles.find((f) => f.slug === transcludeTarget) + if (!page) { + // Virtual pages from PageType plugins have slugs without extensions + // (e.g. "plugins/CanvasPage") but CrawlLinks resolves wikilinks like + // ![[CanvasPage.canvas]] to "plugins/CanvasPage.canvas". Fall back to + // stripping the extension from the transclude target. + const dotIdx = transcludeTarget.lastIndexOf(".") + const slashIdx = transcludeTarget.lastIndexOf("/") + if (dotIdx > slashIdx + 1) { + const stripped = transcludeTarget.slice(0, dotIdx) as FullSlug + page = componentData.allFiles.findLast((f) => f.slug === stripped) + } + } if (!page) { return } diff --git a/quartz/plugins/pageTypes/dispatcher.ts b/quartz/plugins/pageTypes/dispatcher.ts index 3aa5adbd7..e3af1e1a2 100644 --- a/quartz/plugins/pageTypes/dispatcher.ts +++ b/quartz/plugins/pageTypes/dispatcher.ts @@ -7,6 +7,9 @@ import { ProcessedContent, defaultProcessedContent } from "../vfile" import { write } from "../emitters/helpers" import { BuildCtx, trieFromAllFiles } from "../../util/ctx" import { StaticResources } from "../../util/resources" +import { render } from "preact-render-to-string" +import { fromHtml } from "hast-util-from-html" +import { Root as HtmlRoot } from "hast" function getPageTypes(ctx: BuildCtx): QuartzPageTypePluginInstance[] { return (ctx.cfg.plugins.pageTypes ?? []) as unknown as QuartzPageTypePluginInstance[] @@ -89,6 +92,47 @@ async function emitPage( }) } +/** + * Render each virtual page's Body component to HTML and parse it to a hast tree, + * populating both the ProcessedContent tree and vfile.data.htmlAst so that + * transclusion (e.g. ![[file.canvas]]) can inline the virtual page's content. + */ +function populateVirtualPageHtmlAst( + virtualEntries: Array<{ + tree: ProcessedContent[0] + vfile: ProcessedContent[1] + layout: FullPageLayout + vpSlug: FullSlug + }>, + ctx: BuildCtx, + allFiles: ProcessedContent[1]["data"][], + resources: StaticResources, +) { + const cfg = ctx.cfg.configuration + for (const ve of virtualEntries) { + const BodyComponent = ve.layout.pageBody + const externalResources = pageResources(pathToRoot(ve.vpSlug), resources) + const componentData: QuartzComponentProps = { + ctx, + fileData: ve.vfile.data, + externalResources, + cfg, + children: [], + tree: ve.tree, + allFiles, + } + try { + const htmlString = render(BodyComponent(componentData)) + const htmlAst = fromHtml(htmlString, { fragment: true }) as HtmlRoot + ve.tree.children = htmlAst.children + ve.vfile.data.htmlAst = htmlAst + } catch { + // Body rendering failed — leave htmlAst empty so transclusion falls + // back to the default title-only display. + } + } +} + export const PageTypeDispatcher: QuartzEmitterPlugin> = (userOpts) => { const defaults = userOpts?.defaults ?? {} const byPageType = userOpts?.byPageType ?? {} @@ -135,6 +179,9 @@ export const PageTypeDispatcher: QuartzEmitterPlugin> } } + // Render Body components to populate htmlAst for transclusion + populateVirtualPageHtmlAst(virtualEntries, ctx, allFiles, resources) + // Merge virtual page data into allFiles so renderPage can resolve transcludes const allFilesWithVirtual = [...allFiles, ...virtualEntries.map((ve) => ve.vfile.data)] @@ -207,6 +254,9 @@ export const PageTypeDispatcher: QuartzEmitterPlugin> } } + // Render Body components to populate htmlAst for transclusion + populateVirtualPageHtmlAst(virtualEntries, ctx, allFiles, resources) + // Merge virtual page data into allFiles for transclude resolution const allFilesWithVirtual = [...allFiles, ...virtualEntries.map((ve) => ve.vfile.data)]