在生成 RSS 的时候,我发现 Quartz 并不能正确地生成 RSS,它会先按首字母顺序提取所有文档,然后按照设置的参数提取前 rssLimit
篇文章然后生成 RSS Feed。但很明显我们希望它能生成最新的文章的 Feed,因此我先去 issue 中看了一下,找到了一个最像的,简单留了个言就去看代码了。
这一看就觉得不大对劲,在渲染 RSS 的过程中,作者是这样提取文章的:
const items = Array.from(idx)
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
.slice(0, limit ?? idx.size)
.join("")
这样似乎没有对 RSS 按时间排序,但咱也不懂 typescript 啊,只好求助 ChatGPT 帮忙生成了一段按时间排序的版本:
const items = Array.from(idx)
.sort(([slugA, contentA], [slugB, contentB]) => {
const dateA = contentA.date ?? new Date(0);
const dateB = contentB.date ?? new Date(0);
return dateB.getTime() - dateA.getTime();
})
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
.slice(0, limit ?? idx.size)
.join("");
虽然不会写,但至少咱还是会读的,结合上下文,简单猜测这一段代码的意思就是看文章的属性中有没有 date 这个参数,有的话就使用,否则的话就使用当前时间进行比较。那么 date 是在哪获得的呢?简单往上跟一下能找到:
export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
opts = { ...defaultOptions, ...opts }
return {
name: "ContentIndex",
async emit(ctx, content, _resources, emit) {
... ...
for (const [tree, file] of content) {
const slug = file.data.slug!
const date = getDate(ctx.cfg.configuration, file.data) ?? new Date()
... ...
这里调用了 getDate
函数:
export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date | undefined {
... ...
return data.dates?.[cfg.defaultDateType]
}
这里 dates 是一个数据结构:
declare module "vfile" {
interface DataMap {
dates: {
created: Date
modified: Date
published: Date
}
}
}
简单来说就是选取文件三个属性中的一个,cfg.defaultDateType
默认的取值是:
const config: QuartzConfig = {
configuration: {
... ...
defaultDateType: "created",
... ...
那奇怪了,quartz 是在哪里识别到我 frontmatter 里面的 date 呢?找到:
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts }
return {
name: "CreatedModifiedDate",
markdownPlugins() {
return [
() => {
let repo: Repository | undefined = undefined
return async (_tree, file) => {
let created: MaybeDate = undefined
let modified: MaybeDate = undefined
let published: MaybeDate = undefined
const fp = file.data.filePath!
const fullFp = path.posix.join(file.cwd, fp)
for (const source of opts.priority) {
if (source === "filesystem") {
const st = await fs.promises.stat(fullFp)
created ||= st.birthtimeMs
modified ||= st.mtimeMs
} else if (source === "frontmatter" && file.data.frontmatter) {
created ||= file.data.frontmatter.date
modified ||= file.data.frontmatter.lastmod
modified ||= file.data.frontmatter.updated
modified ||= file.data.frontmatter["last-modified"]
published ||= file.data.frontmatter.publishDate
} else if (source === "git") {
if (!repo) {
repo = new Repository(file.cwd)
}
modified ||= await repo.getFileLatestModifiedDateAsync(file.data.filePath!)
}
}
file.data.dates = {
created: coerceDate(fp, created),
modified: coerceDate(fp, modified),
published: coerceDate(fp, published),
... ...
原来是在这里提取时间属性的,而且默认配置:
const defaultOptions: Options = {
priority: ["frontmatter", "git", "filesystem"],
}
默认的优先级就是 frontmatter 优先。
好了,虽然绕的有点远,但我还是发现了问题所在,于是把这一段代码加上就提了个 PR。代码没看懂就去交 PR 属实有点离谱,下次别这么干了。作者给了个意见更是没看懂,那就看看作者的操作吧:
diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts
index 69d0d37..2c70368 100644
--- a/quartz/plugins/emitters/contentIndex.ts
+++ b/quartz/plugins/emitters/contentIndex.ts
@@ -6,6 +6,7 @@ import { FilePath, FullSlug, SimpleSlug, simplifySlug } from "../../util/path"
import { QuartzEmitterPlugin } from "../types"
import { toHtml } from "hast-util-to-html"
import path from "path"
+import { byDateAndAlphabetical } from "../../components/PageList"
export type ContentIndex = Map<FullSlug, ContentDetails>
export type ContentDetails = {
@@ -59,6 +60,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu
</item>`
const items = Array.from(idx)
+ .sort((a, b) => byDateAndAlphabetical(cfg)(a[1], b[1]))
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
.slice(0, limit ?? idx.size)
.join("")
他导入了一个 byDateAndAlphabetical
然后用这个函数去 sort,但是并没有解决最开始提到的问题,RSS 生成时还是只生成按首字母排序的 feed。那为什么作者的 commit 不行呢?TS 小白抱着困惑去翻代码了。byDateAndAlphabetical
函数长这个样子:
export function byDateAndAlphabetical(
cfg: GlobalConfiguration,
): (f1: QuartzPluginData, f2: QuartzPluginData) => number {
return (f1, f2) => {
if (f1.dates && f2.dates) {
// sort descending
return getDate(cfg, f2)!.getTime() - getDate(cfg, f1)!.getTime()
} else if (f1.dates && !f2.dates) {
// prioritize files with dates
return -1
} else if (!f1.dates && f2.dates) {
return 1
}
// otherwise, sort lexographically by title
const f1Title = f1.frontmatter?.title.toLowerCase() ?? ""
const f2Title = f2.frontmatter?.title.toLowerCase() ?? ""
return f1Title.localeCompare(f2Title)
}
}
不对劲的地方出现在参数属性这里,在 byDateAndAlphabetical
函数中,函数参数的属性是 QuartzPluginData
,但实际上传入的参数属性是 ContentDetails
,这就导致这个函数没有效果了。还好作者最后的 commit 解决了问题:
const items = Array.from(idx)
- .sort((a, b) => byDateAndAlphabetical(cfg)(a[1], b[1]))
+ .sort(([_, f1], [__, f2]) => {
+ if (f1.date && f2.date) {
+ return f2.date.getTime() - f1.date.getTime()
+ } else if (f1.date && !f2.date) {
+ return -1
+ } else if (!f1.date && f2.date) {
+ return 1
+ }
+
+ return f1.title.localeCompare(f2.title)
+ })
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
.slice(0, limit ?? idx.size)
.join("")
作者的思路是先按照时间比较,不成功则按照 title 比较,但是实际上我们看之前的代码 date 属性是一定存在的:
export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
opts = { ...defaultOptions, ...opts }
return {
name: "ContentIndex",
async emit(ctx, content, _resources, emit) {
... ...
for (const [tree, file] of content) {
const slug = file.data.slug!
const date = getDate(ctx.cfg.configuration, file.data) ?? new Date()
... ...
所以最后加的那个 return f1.title.localeCompare(f2.title)
感觉有点画蛇添足。不过说不定也许以后真遇上了呢?
虽然不够优雅,但还是顺利解决啦,鼓掌鼓掌👏👏。