Skip to content

Commit a6e5c7c

Browse files
committed
feat: add optional tag-based RSS feed generation
- Add 'rssTags' option to ContentIndex plugin for flexible tag-based RSS feeds - Implement RSS feed generation for specific tags - Add deduplication and slugification of tags - Output feeds to 'tags/<tag>/index.xml'
1 parent f346a01 commit a6e5c7c

1 file changed

Lines changed: 71 additions & 1 deletion

File tree

quartz/plugins/emitters/contentIndex.tsx

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ import { Root } from "hast"
22
import { GlobalConfiguration } from "../../cfg"
33
import { getDate } from "../../components/Date"
44
import { escapeHTML } from "../../util/escape"
5-
import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path"
5+
import {
6+
FilePath,
7+
FullSlug,
8+
SimpleSlug,
9+
getAllSegmentPrefixes,
10+
joinSegments,
11+
simplifySlug,
12+
slugTag,
13+
} from "../../util/path"
614
import { QuartzEmitterPlugin } from "../types"
715
import { toHtml } from "hast-util-to-html"
816
import { write } from "./helpers"
@@ -28,6 +36,9 @@ interface Options {
2836
rssFullHtml: boolean
2937
rssSlug: string
3038
includeEmptyFiles: boolean
39+
includeTags: boolean
40+
rssTagsLimit: number
41+
rssTags: string[]
3142
}
3243

3344
const defaultOptions: Options = {
@@ -37,6 +48,9 @@ const defaultOptions: Options = {
3748
rssFullHtml: false,
3849
rssSlug: "index",
3950
includeEmptyFiles: true,
51+
includeTags: false,
52+
rssTagsLimit: 15,
53+
rssTags: [],
4054
}
4155

4256
function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndexMap): string {
@@ -135,6 +149,62 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
135149
slug: (opts?.rssSlug ?? "index") as FullSlug,
136150
ext: ".xml",
137151
})
152+
153+
if (opts?.includeTags) {
154+
// Optimization: Build a map of tag -> content list once (reverse index)
155+
const tagsInput: Map<string, ContentDetails[]> = new Map()
156+
157+
// Iterate over the content index once to populate the reverse index
158+
for (const [_, content] of linkIndex) {
159+
const tags = content.tags.flatMap(getAllSegmentPrefixes)
160+
for (const tag of new Set(tags)) {
161+
// Use Set to avoid double counting per file
162+
if (!tagsInput.has(tag)) {
163+
tagsInput.set(tag, [])
164+
}
165+
tagsInput.get(tag)!.push(content)
166+
}
167+
}
168+
169+
let sortedTags: string[] = []
170+
171+
if (opts.rssTags && opts.rssTags.length > 0) {
172+
// Deduplicate and slugify user-provided tags
173+
const userTags = new Set(opts.rssTags.map((tag) => slugTag(tag)))
174+
175+
// Filter user tags to only those that exist in the content
176+
sortedTags = Array.from(userTags).filter((tag) => tagsInput.has(tag))
177+
} else if ((opts.rssTagsLimit ?? 0) > 0) {
178+
// Sort available tags by frequency (number of content items)
179+
sortedTags = Array.from(tagsInput.entries())
180+
.sort((a, b) => b[1].length - a[1].length) // Sort by frequency descending
181+
.slice(0, opts.rssTagsLimit)
182+
.map(([tag]) => tag)
183+
}
184+
185+
if (sortedTags.length === 0) {
186+
console.warn(
187+
"[contentIndex] includeTags is enabled, but no tag-based RSS feeds will be generated. " +
188+
"Either provide non-empty `rssTags` matching content tags or set `rssTagsLimit` to a positive number.",
189+
)
190+
}
191+
192+
for (const tag of sortedTags) {
193+
const tagContent = tagsInput.get(tag)
194+
if (!tagContent) continue // Should not happen given logic above
195+
196+
// Reconstruct a map for generateRSSFeed (it expects a ContentIndexMap)
197+
// We can optimize this by making generateRSSFeed accept an array, but for now we conform to the interface
198+
const tagFilteredIndex = new Map(tagContent.map((content) => [content.slug, content]))
199+
200+
yield write({
201+
ctx,
202+
content: generateRSSFeed(cfg, tagFilteredIndex, opts.rssLimit),
203+
slug: joinSegments("tags", tag, "index") as FullSlug,
204+
ext: ".xml",
205+
})
206+
}
207+
}
138208
}
139209

140210
const fp = joinSegments("static", "contentIndex") as FullSlug

0 commit comments

Comments
 (0)