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]
> 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.

View File

@ -19,7 +19,7 @@ import DepGraph from "../../depgraph"
import chalk from "chalk"
import { ProcessedContent } from "../vfile"
export type ContentIndexMap = Map<FullSlug, ContentDetails>
type ContentIndex = Tree<TreeNode>
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 => `
<url>
@ -69,7 +69,7 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndexMap): string
</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 feedTitle = opts.titlePattern!(cfg, entries.dir, entries.dirIndex)
const limit = opts?.rssLimit ?? entries.raw.length
@ -165,24 +165,26 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (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<FilePath>[] = []
var indexTree = new Tree<TreeNode>(defaultFeed(), compareTreeNodes)
// ProcessedContent[] -> Tree<TreeNode>
// bfahrenfort: If I could finagle a Visitor pattern to cross
// different datatypes (TransformVisitor<T, K>?), 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<Partial<Options>> = (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",
}),
)