fix: include virtual pages in content index for explorer visibility

This commit is contained in:
saberzero1 2026-02-26 20:16:59 +01:00
parent 360dfca18e
commit 673f51c13d
No known key found for this signature in database
4 changed files with 100 additions and 31 deletions

View File

@ -50,6 +50,7 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
allSlugs: [], allSlugs: [],
allFiles: [], allFiles: [],
incremental: false, incremental: false,
virtualPages: [],
} }
const perf = new PerfTimer() const perf = new PerfTimer()
@ -264,10 +265,40 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
) )
let emittedFiles = 0 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) { for (const emitter of cfg.plugins.emitters) {
if (emitter.name === "PageTypeDispatcher") continue
// Try to use partialEmit if available, otherwise assume the output is static // Try to use partialEmit if available, otherwise assume the output is static
const emitFn = emitter.partialEmit ?? emitter.emit 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) { if (emitted === null) {
continue continue
} }

View File

@ -134,6 +134,9 @@ export const PageTypeDispatcher: QuartzEmitterPlugin<Partial<DispatcherOptions>>
...vp.data, ...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) yield emitPage(ctx, vpSlug, tree, vfile.data, allFiles, layout, resources)
} }
} }
@ -182,6 +185,9 @@ export const PageTypeDispatcher: QuartzEmitterPlugin<Partial<DispatcherOptions>>
...vp.data, ...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) yield emitPage(ctx, vpSlug, tree, vfile.data, allFiles, layout, resources)
} }
} }

View File

@ -1,11 +1,50 @@
import { PerfTimer } from "../util/perf" import { PerfTimer } from "../util/perf"
import { getStaticResourcesFromPlugins } from "../plugins" import { getStaticResourcesFromPlugins } from "../plugins"
import { ProcessedContent } from "../plugins/vfile" import { ProcessedContent } from "../plugins/vfile"
import { QuartzEmitterPluginInstance } from "../plugins/types"
import { QuartzLogger } from "../util/log" import { QuartzLogger } from "../util/log"
import { trace } from "../util/trace" import { trace } from "../util/trace"
import { BuildCtx } from "../util/ctx" import { BuildCtx } from "../util/ctx"
import { StaticResources } from "../util/resources"
import { styleText } from "util" import { styleText } from "util"
async function runEmitter(
emitter: QuartzEmitterPluginInstance,
ctx: BuildCtx,
content: ProcessedContent[],
resources: StaticResources,
log: QuartzLogger,
): Promise<number> {
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[]) { export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
const { argv, cfg } = ctx const { argv, cfg } = ctx
const perf = new PerfTimer() const perf = new PerfTimer()
@ -15,36 +54,27 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
let emittedFiles = 0 let emittedFiles = 0
const staticResources = getStaticResourcesFromPlugins(ctx) const staticResources = getStaticResourcesFromPlugins(ctx)
await Promise.all(
cfg.plugins.emitters.map(async (emitter) => { // Phase 1: Run PageTypeDispatcher first so it populates ctx.virtualPages
try { // with pages generated by page type plugins (tag pages, folder pages, bases pages, etc.)
const emitted = await emitter.emit(ctx, content, staticResources) const dispatcher = cfg.plugins.emitters.find((e) => e.name === "PageTypeDispatcher")
if (Symbol.asyncIterator in emitted) { if (dispatcher) {
// Async generator case ctx.virtualPages = []
for await (const file of emitted) { emittedFiles += await runEmitter(dispatcher, ctx, content, staticResources, log)
emittedFiles++ }
if (ctx.argv.verbose) {
console.log(`[emit:${emitter.name}] ${file}`) // Phase 2: Run all other emitters with content extended by virtual pages.
} else { // This ensures emitters like ContentIndex include virtual pages in their output
log.updateText(`${emitter.name} -> ${styleText("gray", file)}`) // (e.g. sitemap, RSS, contentIndex.json used by the explorer sidebar).
} const contentWithVirtual =
} ctx.virtualPages.length > 0 ? [...content, ...ctx.virtualPages] : content
} else { const otherEmitters = cfg.plugins.emitters.filter((e) => e.name !== "PageTypeDispatcher")
// Array case const counts = await Promise.all(
emittedFiles += emitted.length otherEmitters.map((emitter) =>
for (const file of emitted) { runEmitter(emitter, ctx, contentWithVirtual, staticResources, log),
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)
}
}),
) )
emittedFiles += counts.reduce((sum, c) => sum + c, 0)
log.end(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince()}`) log.end(`Emitted ${emittedFiles} files to \`${argv.output}\` in ${perf.timeSince()}`)
} }

View File

@ -1,5 +1,5 @@
import { QuartzConfig } from "../cfg" import { QuartzConfig } from "../cfg"
import { QuartzPluginData } from "../plugins/vfile" import { ProcessedContent, QuartzPluginData } from "../plugins/vfile"
import { FileTrieNode } from "./fileTrie" import { FileTrieNode } from "./fileTrie"
import { FilePath, FullSlug } from "./path" import { FilePath, FullSlug } from "./path"
@ -29,6 +29,8 @@ export interface BuildCtx {
allFiles: FilePath[] allFiles: FilePath[]
trie?: FileTrieNode<BuildTimeTrieData> trie?: FileTrieNode<BuildTimeTrieData>
incremental: boolean 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<BuildTimeTrieData> { export function trieFromAllFiles(allFiles: QuartzPluginData[]): FileTrieNode<BuildTimeTrieData> {