@@ -2,7 +2,15 @@ import { Root } from "hast"
22import { GlobalConfiguration } from "../../cfg"
33import { getDate } from "../../components/Date"
44import { 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"
614import { QuartzEmitterPlugin } from "../types"
715import { toHtml } from "hast-util-to-html"
816import { 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
3344const 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
4256function 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