From 673f51c13d092c8b9eb41ae7cc19918cbceaa2b0 Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Thu, 26 Feb 2026 20:16:59 +0100 Subject: [PATCH] fix: include virtual pages in content index for explorer visibility --- quartz/build.ts | 33 +++++++++- quartz/plugins/pageTypes/dispatcher.ts | 6 ++ quartz/processors/emit.ts | 88 +++++++++++++++++--------- quartz/util/ctx.ts | 4 +- 4 files changed, 100 insertions(+), 31 deletions(-) diff --git a/quartz/build.ts b/quartz/build.ts index 0794b8a28..4518fa1d8 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -50,6 +50,7 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { allSlugs: [], allFiles: [], incremental: false, + virtualPages: [], } const perf = new PerfTimer() @@ -264,10 +265,40 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD ) let emittedFiles = 0 + + // Phase 1: Run PageTypeDispatcher first so it populates ctx.virtualPages + const dispatcher = cfg.plugins.emitters.find((e) => e.name === "PageTypeDispatcher") + if (dispatcher) { + ctx.virtualPages = [] + const emitFn = dispatcher.partialEmit ?? dispatcher.emit + const emitted = await emitFn(ctx, processedFiles, staticResources, changeEvents) + if (emitted !== null) { + if (Symbol.asyncIterator in emitted) { + for await (const file of emitted) { + emittedFiles++ + if (ctx.argv.verbose) { + console.log(`[emit:${dispatcher.name}] ${file}`) + } + } + } else { + emittedFiles += emitted.length + if (ctx.argv.verbose) { + for (const file of emitted) { + console.log(`[emit:${dispatcher.name}] ${file}`) + } + } + } + } + } + + // Phase 2: Run all other emitters with content extended by virtual pages + const contentWithVirtual = + ctx.virtualPages.length > 0 ? [...processedFiles, ...ctx.virtualPages] : processedFiles for (const emitter of cfg.plugins.emitters) { + if (emitter.name === "PageTypeDispatcher") continue // Try to use partialEmit if available, otherwise assume the output is static const emitFn = emitter.partialEmit ?? emitter.emit - const emitted = await emitFn(ctx, processedFiles, staticResources, changeEvents) + const emitted = await emitFn(ctx, contentWithVirtual, staticResources, changeEvents) if (emitted === null) { continue } diff --git a/quartz/plugins/pageTypes/dispatcher.ts b/quartz/plugins/pageTypes/dispatcher.ts index f739b8c45..e41967994 100644 --- a/quartz/plugins/pageTypes/dispatcher.ts +++ b/quartz/plugins/pageTypes/dispatcher.ts @@ -134,6 +134,9 @@ export const PageTypeDispatcher: QuartzEmitterPlugin> ...vp.data, }) + // Expose virtual pages on ctx so other emitters (e.g. ContentIndex) can include them + ctx.virtualPages.push([tree, vfile]) + yield emitPage(ctx, vpSlug, tree, vfile.data, allFiles, layout, resources) } } @@ -182,6 +185,9 @@ export const PageTypeDispatcher: QuartzEmitterPlugin> ...vp.data, }) + // Expose virtual pages on ctx so other emitters (e.g. ContentIndex) can include them + ctx.virtualPages.push([tree, vfile]) + yield emitPage(ctx, vpSlug, tree, vfile.data, allFiles, layout, resources) } } diff --git a/quartz/processors/emit.ts b/quartz/processors/emit.ts index 78702959c..0c3204d46 100644 --- a/quartz/processors/emit.ts +++ b/quartz/processors/emit.ts @@ -1,11 +1,50 @@ import { PerfTimer } from "../util/perf" import { getStaticResourcesFromPlugins } from "../plugins" import { ProcessedContent } from "../plugins/vfile" +import { QuartzEmitterPluginInstance } from "../plugins/types" import { QuartzLogger } from "../util/log" import { trace } from "../util/trace" import { BuildCtx } from "../util/ctx" +import { StaticResources } from "../util/resources" import { styleText } from "util" +async function runEmitter( + emitter: QuartzEmitterPluginInstance, + ctx: BuildCtx, + content: ProcessedContent[], + resources: StaticResources, + log: QuartzLogger, +): Promise { + let count = 0 + try { + const emitted = await emitter.emit(ctx, content, resources) + if (Symbol.asyncIterator in emitted) { + // Async generator case + for await (const file of emitted) { + count++ + if (ctx.argv.verbose) { + console.log(`[emit:${emitter.name}] ${file}`) + } else { + log.updateText(`${emitter.name} -> ${styleText("gray", file)}`) + } + } + } else { + // Array case + count += emitted.length + for (const file of emitted) { + if (ctx.argv.verbose) { + console.log(`[emit:${emitter.name}] ${file}`) + } else { + log.updateText(`${emitter.name} -> ${styleText("gray", file)}`) + } + } + } + } catch (err) { + trace(`Failed to emit from plugin \`${emitter.name}\``, err as Error) + } + return count +} + export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) { const { argv, cfg } = ctx const perf = new PerfTimer() @@ -15,36 +54,27 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) { let emittedFiles = 0 const staticResources = getStaticResourcesFromPlugins(ctx) - await Promise.all( - cfg.plugins.emitters.map(async (emitter) => { - try { - const emitted = await emitter.emit(ctx, content, staticResources) - if (Symbol.asyncIterator in emitted) { - // Async generator case - for await (const file of emitted) { - emittedFiles++ - if (ctx.argv.verbose) { - console.log(`[emit:${emitter.name}] ${file}`) - } else { - log.updateText(`${emitter.name} -> ${styleText("gray", file)}`) - } - } - } else { - // Array case - emittedFiles += emitted.length - for (const file of emitted) { - if (ctx.argv.verbose) { - console.log(`[emit:${emitter.name}] ${file}`) - } else { - log.updateText(`${emitter.name} -> ${styleText("gray", file)}`) - } - } - } - } catch (err) { - trace(`Failed to emit from plugin \`${emitter.name}\``, err as Error) - } - }), + + // Phase 1: Run PageTypeDispatcher first so it populates ctx.virtualPages + // with pages generated by page type plugins (tag pages, folder pages, bases pages, etc.) + const dispatcher = cfg.plugins.emitters.find((e) => e.name === "PageTypeDispatcher") + if (dispatcher) { + ctx.virtualPages = [] + emittedFiles += await runEmitter(dispatcher, ctx, content, staticResources, log) + } + + // Phase 2: Run all other emitters with content extended by virtual pages. + // This ensures emitters like ContentIndex include virtual pages in their output + // (e.g. sitemap, RSS, contentIndex.json used by the explorer sidebar). + const contentWithVirtual = + ctx.virtualPages.length > 0 ? [...content, ...ctx.virtualPages] : content + const otherEmitters = cfg.plugins.emitters.filter((e) => e.name !== "PageTypeDispatcher") + const counts = await Promise.all( + otherEmitters.map((emitter) => + runEmitter(emitter, ctx, contentWithVirtual, staticResources, log), + ), ) + emittedFiles += counts.reduce((sum, c) => sum + c, 0) log.end(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince()}`) } diff --git a/quartz/util/ctx.ts b/quartz/util/ctx.ts index 80115ec27..83167f378 100644 --- a/quartz/util/ctx.ts +++ b/quartz/util/ctx.ts @@ -1,5 +1,5 @@ import { QuartzConfig } from "../cfg" -import { QuartzPluginData } from "../plugins/vfile" +import { ProcessedContent, QuartzPluginData } from "../plugins/vfile" import { FileTrieNode } from "./fileTrie" import { FilePath, FullSlug } from "./path" @@ -29,6 +29,8 @@ export interface BuildCtx { allFiles: FilePath[] trie?: FileTrieNode incremental: boolean + /** Virtual pages generated by page type plugins (e.g. tag pages, folder pages, bases pages) */ + virtualPages: ProcessedContent[] } export function trieFromAllFiles(allFiles: QuartzPluginData[]): FileTrieNode {