feat(contentIndex): Per-folder RSS feeds

This commit is contained in:
bfahrenfort 2024-11-24 11:10:54 +11:00
parent cf7bb1fe83
commit c65e6836b4
2 changed files with 34 additions and 26 deletions

View File

@ -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] > [!info]
> After deploying, the generated RSS link will be available at `https://${baseUrl}/index.xml` by default. > 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. > 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 ## Configuration
This functionality is provided by the [[ContentIndex]] plugin. See the plugin page for customization options. This functionality is provided by the [[ContentIndex]] plugin. See the plugin page for customization options.

View File

@ -19,7 +19,7 @@ import DepGraph from "../../depgraph"
import chalk from "chalk" import chalk from "chalk"
import { ProcessedContent } from "../vfile" import { ProcessedContent } from "../vfile"
export type ContentIndexMap = Map<FullSlug, ContentDetails> type ContentIndex = Tree<TreeNode>
export type ContentDetails = { export type ContentDetails = {
slug: FullSlug slug: FullSlug
filePath: FilePath filePath: FilePath
@ -40,7 +40,7 @@ interface Options {
bypassIndexCheck: boolean bypassIndexCheck: boolean
rssLimit?: number rssLimit?: number
rssFullHtml: boolean rssFullHtml: boolean
rssSlug: string rssSlug: FullSlug
includeEmptyFiles: boolean includeEmptyFiles: boolean
titlePattern?: (cfg: GlobalConfiguration, dir: FullSlug, dirIndex?: ContentDetails) => string titlePattern?: (cfg: GlobalConfiguration, dir: FullSlug, dirIndex?: ContentDetails) => string
} }
@ -51,13 +51,13 @@ const defaultOptions: Options = {
enableRSS: true, enableRSS: true,
rssLimit: 10, rssLimit: 10,
rssFullHtml: false, rssFullHtml: false,
rssSlug: "index", rssSlug: "index" as FullSlug,
includeEmptyFiles: true, includeEmptyFiles: true,
titlePattern: (cfg, dir, dirIndex) => titlePattern: (cfg, dir, dirIndex) =>
`${cfg.pageTitle} - ${dirIndex != null ? dirIndex.title : dir.split("/").pop()}`, `${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 base = cfg.baseUrl ?? ""
const createURLEntry = (content: ContentDetails): string => ` const createURLEntry = (content: ContentDetails): string => `
<url> <url>
@ -69,7 +69,7 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndexMap): string
</urlset>` </urlset>`
} }
function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndexMap, limit?: number): string { function finishRSSFeed(cfg: GlobalConfiguration, opts: Partial<Options>, entries: Feed): string {
const base = cfg.baseUrl ?? "" const base = cfg.baseUrl ?? ""
const feedTitle = opts.titlePattern!(cfg, entries.dir, entries.dirIndex) const feedTitle = opts.titlePattern!(cfg, entries.dir, entries.dirIndex)
const limit = opts?.rssLimit ?? entries.raw.length const limit = opts?.rssLimit ?? entries.raw.length
@ -165,14 +165,15 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
} }
const cfg = ctx.cfg.configuration const cfg = ctx.cfg.configuration
const linkIndex: ContentIndexMap = new Map() const emitted: Promise<FilePath>[] = []
for (const [tree, file] of content) { var indexTree = new Tree<TreeNode>(defaultFeed(), compareTreeNodes)
const slug = file.data.slug!
const date = getDate(ctx.cfg.configuration, file.data) ?? new Date() // ProcessedContent[] -> Tree<TreeNode>
if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { // bfahrenfort: If I could finagle a Visitor pattern to cross
linkIndex.set(slug, { // different datatypes (TransformVisitor<T, K>?), half of this pass could be
slug, // folded into the FeedGenerator postorder accept
filePath: file.data.relativePath!, const detailsOf = ([tree, file]: ProcessedContent): ContentDetails => {
return {
title: file.data.frontmatter?.title!, title: file.data.frontmatter?.title!,
links: file.data.links ?? [], links: file.data.links ?? [],
tags: file.data.frontmatter?.tags ?? [], tags: file.data.frontmatter?.tags ?? [],
@ -180,9 +181,10 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
richContent: opts?.rssFullHtml richContent: opts?.rssFullHtml
? escapeHTML(toHtml(tree as Root, { allowDangerousHtml: true })) ? escapeHTML(toHtml(tree as Root, { allowDangerousHtml: true }))
: undefined, : undefined,
date: date, date: getDate(ctx.cfg.configuration, file.data) ?? new Date(),
description: file.data.description ?? "", description: file.data.description ?? "",
}) slug: slugifyFilePath(file.data.relativePath!, true),
noRSS: file.data.frontmatter?.noRSS ?? false,
} }
} }
for (const [tree, file] of content) { for (const [tree, file] of content) {
@ -241,8 +243,8 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
emitted.push( emitted.push(
write({ write({
ctx, ctx,
content: generateRSSFeed(cfg, linkIndex, opts.rssLimit), content: finishRSSFeed(cfg, opts, feedTree.data as Feed),
slug: (opts?.rssSlug ?? "index") as FullSlug, slug: opts.rssSlug!, // Safety: defaults to "index"
ext: ".xml", ext: ".xml",
}), }),
) )