feat(contentIndex): add optional tag-based RSS feed generation

- Add 'includeTags' and 'rssTagsLimit' options to ContentIndex plugin
- Implement logic to generate RSS feeds for unique tags
- Output feeds to 'tags/<tag>/index.xml'
- Support limiting the number of generated tag feeds by frequency
This commit is contained in:
Suryaansh Rai 2026-01-17 21:53:18 +05:30
parent f346a01296
commit 47433a9068

View File

@ -2,7 +2,7 @@ import { Root } from "hast"
import { GlobalConfiguration } from "../../cfg" import { GlobalConfiguration } from "../../cfg"
import { getDate } from "../../components/Date" import { getDate } from "../../components/Date"
import { escapeHTML } from "../../util/escape" import { escapeHTML } from "../../util/escape"
import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path" import { FilePath, FullSlug, SimpleSlug, getAllSegmentPrefixes, joinSegments, simplifySlug } from "../../util/path"
import { QuartzEmitterPlugin } from "../types" import { QuartzEmitterPlugin } from "../types"
import { toHtml } from "hast-util-to-html" import { toHtml } from "hast-util-to-html"
import { write } from "./helpers" import { write } from "./helpers"
@ -28,6 +28,8 @@ interface Options {
rssFullHtml: boolean rssFullHtml: boolean
rssSlug: string rssSlug: string
includeEmptyFiles: boolean includeEmptyFiles: boolean
includeTags: boolean
rssTagsLimit: number
} }
const defaultOptions: Options = { const defaultOptions: Options = {
@ -37,6 +39,8 @@ const defaultOptions: Options = {
rssFullHtml: false, rssFullHtml: false,
rssSlug: "index", rssSlug: "index",
includeEmptyFiles: true, includeEmptyFiles: true,
includeTags: false,
rssTagsLimit: 15,
} }
function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndexMap): string { function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndexMap): string {
@ -135,6 +139,39 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
slug: (opts?.rssSlug ?? "index") as FullSlug, slug: (opts?.rssSlug ?? "index") as FullSlug,
ext: ".xml", ext: ".xml",
}) })
if (opts?.includeTags && (opts.rssTagsLimit ?? 0) > 0) {
const tagCounts: Map<string, number> = new Map()
// Count tags from all non-empty files (unless includeEmptyFiles is true)
for (const [_, content] of linkIndex) {
const tags = content.tags.flatMap(getAllSegmentPrefixes)
for (const tag of new Set(tags)) { // Use Set to avoid double counting per file
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1)
}
}
const sortedTags = Array.from(tagCounts.entries())
.sort((a, b) => b[1] - a[1]) // Sort by frequency descending
.slice(0, opts.rssTagsLimit)
.map(([tag]) => tag)
for (const tag of sortedTags) {
const tagFilteredIndex = new Map(
Array.from(linkIndex).filter(([_, content]) => {
const fileTags = new Set(content.tags.flatMap(getAllSegmentPrefixes))
return fileTags.has(tag)
})
)
yield write({
ctx,
content: generateRSSFeed(cfg, tagFilteredIndex, opts.rssLimit),
slug: joinSegments("tags", tag, "index") as FullSlug,
ext: ".xml",
})
}
}
} }
const fp = joinSegments("static", "contentIndex") as FullSlug const fp = joinSegments("static", "contentIndex") as FullSlug