diff --git a/docs/features/RSS Feed.md b/docs/features/RSS Feed.md index 4b1a1bb3e..5fb609ee4 100644 --- a/docs/features/RSS Feed.md +++ b/docs/features/RSS Feed.md @@ -1,10 +1,16 @@ -Quartz emits an RSS feed for all the content on your site by generating an `index.xml` file that RSS readers can subscribe to. Because of the RSS spec, this requires the `baseUrl` property in your [[configuration]] to be set properly for RSS readers to pick it up properly. +Quartz emits an RSS feed for all the content on your site by generating an file that RSS readers can subscribe to. Because of the RSS spec, this requires the `baseUrl` property in your [[configuration]] to be set properly for RSS readers to pick it up properly. > [!info] > After deploying, the generated RSS link will be available at `https://${baseUrl}/index.xml` by default. > > The `index.xml` path can be customized by passing the `rssSlug` option to the [[ContentIndex]] plugin. +Quartz also generates RSS feeds for all subdirectories on your site. Add `.rss` to the end of the directory link to download an RSS file limited to the content in that directory and its subdirectories. + +- Subdirectories containing an `index.md` file with `noRSS: true` in the frontmatter will not generate an RSS feed. + - The entries in that subdirectory will still be present in the default feed. +- You can hide a file from all RSS feeds by putting `noRSS: true` in that file's frontmatter. + ## Configuration This functionality is provided by the [[ContentIndex]] plugin. See the plugin page for customization options. diff --git a/quartz/plugins/emitters/contentIndex.tsx b/quartz/plugins/emitters/contentIndex.tsx index a7686b12d..f94b7f1c0 100644 --- a/quartz/plugins/emitters/contentIndex.tsx +++ b/quartz/plugins/emitters/contentIndex.tsx @@ -19,7 +19,7 @@ import DepGraph from "../../depgraph" import chalk from "chalk" import { ProcessedContent } from "../vfile" -export type ContentIndexMap = Map +type ContentIndex = Tree export type ContentDetails = { slug: FullSlug filePath: FilePath @@ -40,7 +40,7 @@ interface Options { bypassIndexCheck: boolean rssLimit?: number rssFullHtml: boolean - rssSlug: string + rssSlug: FullSlug includeEmptyFiles: boolean titlePattern?: (cfg: GlobalConfiguration, dir: FullSlug, dirIndex?: ContentDetails) => string } @@ -51,13 +51,13 @@ const defaultOptions: Options = { enableRSS: true, rssLimit: 10, rssFullHtml: false, - rssSlug: "index", + rssSlug: "index" as FullSlug, includeEmptyFiles: true, titlePattern: (cfg, dir, dirIndex) => `${cfg.pageTitle} - ${dirIndex != null ? dirIndex.title : dir.split("/").pop()}`, } -function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndexMap): string { +function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string { const base = cfg.baseUrl ?? "" const createURLEntry = (content: ContentDetails): string => ` @@ -69,7 +69,7 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndexMap): string ` } -function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndexMap, limit?: number): string { +function finishRSSFeed(cfg: GlobalConfiguration, opts: Partial, entries: Feed): string { const base = cfg.baseUrl ?? "" const feedTitle = opts.titlePattern!(cfg, entries.dir, entries.dirIndex) const limit = opts?.rssLimit ?? entries.raw.length @@ -165,24 +165,26 @@ export const ContentIndex: QuartzEmitterPlugin> = (opts) => { } const cfg = ctx.cfg.configuration - const linkIndex: ContentIndexMap = new Map() - for (const [tree, file] of content) { - const slug = file.data.slug! - const date = getDate(ctx.cfg.configuration, file.data) ?? new Date() - if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { - linkIndex.set(slug, { - slug, - filePath: file.data.relativePath!, - title: file.data.frontmatter?.title!, - links: file.data.links ?? [], - tags: file.data.frontmatter?.tags ?? [], - content: file.data.text ?? "", - richContent: opts?.rssFullHtml - ? escapeHTML(toHtml(tree as Root, { allowDangerousHtml: true })) - : undefined, - date: date, - description: file.data.description ?? "", - }) + const emitted: Promise[] = [] + var indexTree = new Tree(defaultFeed(), compareTreeNodes) + + // ProcessedContent[] -> Tree + // bfahrenfort: If I could finagle a Visitor pattern to cross + // different datatypes (TransformVisitor?), half of this pass could be + // folded into the FeedGenerator postorder accept + const detailsOf = ([tree, file]: ProcessedContent): ContentDetails => { + return { + title: file.data.frontmatter?.title!, + links: file.data.links ?? [], + tags: file.data.frontmatter?.tags ?? [], + content: file.data.text ?? "", + richContent: opts?.rssFullHtml + ? escapeHTML(toHtml(tree as Root, { allowDangerousHtml: true })) + : undefined, + date: getDate(ctx.cfg.configuration, file.data) ?? new Date(), + description: file.data.description ?? "", + slug: slugifyFilePath(file.data.relativePath!, true), + noRSS: file.data.frontmatter?.noRSS ?? false, } } for (const [tree, file] of content) { @@ -241,8 +243,8 @@ export const ContentIndex: QuartzEmitterPlugin> = (opts) => { emitted.push( write({ ctx, - content: generateRSSFeed(cfg, linkIndex, opts.rssLimit), - slug: (opts?.rssSlug ?? "index") as FullSlug, + content: finishRSSFeed(cfg, opts, feedTree.data as Feed), + slug: opts.rssSlug!, // Safety: defaults to "index" ext: ".xml", }), )