Gatsby RSS Feed 生成实战:以 gatsby-plugin-feed 与 Markdown 博客内容为例
Gatsby RSS Feed 生成实战:以 gatsby-plugin-feed 与 Markdown 博客内容为例
导读
本文以 Gatsby 官方仓库中的 feed 示例站点 及其示例文章 posts/2017-03-09-first-post.md 为主线,完整讲解如何基于 gatsby-source-filesystem + gatsby-transformer-remark 把 Markdown 文章变成 RSS Feed,并深入 gatsby-plugin-feed 的源码,讲清 serialize、query、output、match、link 等核心配置的作用原理。读完本文,你将能在自己的 Gatsby 站点中从零搭建一套可复用的 RSS 输出流水线。
示例文章的角色定位:RSS 条目的数据源头
examples/feed/posts/2017-03-09-first-post.md 表面看是一篇占位文章(正文为 Lorem ipsum 测试文本),但它的真实价值在于为整个 RSS 示例提供“标准化的 Markdown 数据模型”。其 frontmatter 结构如下:
---
title: First Post
date: "2017-03-09"
draft: false
---
这正是后续 GraphQL 查询要消费的字段集合:title 与 date 会被 gatsby-plugin-feed 默认查询读取并写入 RSS 条目的标题与发布时间。同目录下的 2017-05-22-second-post.md 与之结构完全一致(标题同为 "First Post",日期为 2017-05-22),两篇文章共同验证了「多篇文章如何按日期排序、逐条生成 RSS item」这一核心链路。
需要特别说明:示例文章只是数据样例,gatsby-transformer-remark 并不要求 frontmatter 必须包含 draft 字段。该字段在这里仅为演示站点所使用,是否参与 RSS 过滤完全取决于你在 serialize 或查询中如何编写逻辑。
示例站点的整体架构:数据如何流动
该示例的最小实现由四份文件构成,构成一条完整的数据链路:
| 文件 | 职责 |
|---|---|
| gatsby-config.js | 声明 siteMetadata 与插件配置,接入文件系统、Markdown 转换与 RSS 生成 |
| posts/*.md | Markdown 内容源,提供 frontmatter 元数据与正文 |
| gatsby-node.js | 在 onCreateNode 中为每篇 Markdown 文章计算并写入 slug 字段 |
| src/pages/index.js | 页面入口,通过 <a href="/rss.xml"> 暴露生成的订阅地址 |
数据流向可概括为:
Markdown 文件
→ gatsby-source-filesystem(把文件变为 File 节点)
→ gatsby-transformer-remark(把 Markdown 变为 MarkdownRemark 节点)
→ gatsby-node.js 的 onCreateNode(写入 slug 字段)
→ gatsby build 完成后的 onPostBuild(gatsby-plugin-feed 执行 GraphQL 查询并输出 XML)
→ public/rss.xml
其中 gatsby-node.js 对 RSS 链路有一个关键贡献:它基于文件名生成带前后斜杠的 slug(如 /2017-03-09-first-post/),该 slug 随后会被 feed 插件的 serialize 拼接成文章的完整 URL。这是整个示例中唯一需要“自写逻辑”的部分,也直接决定了 RSS 条目里链接的形态。
gatsby-config.js 解析:站点元信息与插件装配
gatsby-config.js 中 siteMetadata 的三项内容 title、description、siteUrl 是 RSS 生成的“元信息底座”:
module.exports = {
siteMetadata: {
title: `GatsbyJS RSS`,
description: `A blog with RSS powered by GatsbyJS.`,
siteUrl: `https://www.gatsbyjs.com`,
},
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
name: `posts`,
path: `${__dirname}/posts`,
},
},
`gatsby-transformer-remark`,
{
resolve: `gatsby-plugin-feed`,
options: {},
},
],
}
三个插件各司其职:
- gatsby-source-filesystem:把
posts目录下的.md文件作为数据源载入; - gatsby-transformer-remark:将 Markdown 内容转换为可查询的
MarkdownRemark节点,解析 frontmatter、生成excerpt与html; - gatsby-plugin-feed:在构建结束(
onPostBuild)阶段读取 GraphQL 查询结果,输出 RSS XML。
注意此处 gatsby-plugin-feed 的 options 是一个空对象——这说明该插件自带一整套合理的默认配置,零配置也能直接生成默认的 rss.xml。这一点在阅读 internals.js 源码时可以得到印证:插件内置了默认 query、默认 feeds 数组、默认 generator 标签(GatsbyJS)以及默认的 setup 函数,用于把 siteMetadata 与 feed 配置合并成 RSS 的 feedOptions。
从零配置到自定义:feed 的核心配置模型
官方文档 gatsby-plugin-feed README 给出了完整的自定义写法。插件的顶层选项只有两个:
| 顶层选项 | 必填 | 说明 |
|---|---|---|
query |
否 | 一条 GraphQL 查询,默认查询 siteMetadata(title、description、siteUrl)作为站点级上下文 |
feeds |
是 | 要生成的 RSS 流数组,每个 feed 是一个对象 |
每个 feed 对象中的必填项为:
| feed 键 | 类型 | 说明 |
|---|---|---|
title |
string | RSS 流的标题 |
output |
string | XML 文件的输出路径(相对 public/) |
query |
string | 获取 RSS 条目内容的 GraphQL 查询 |
serialize |
function | 接收查询结果,返回符合 rss 包 itemOptions 的对象数组 |
典型自定义配置(来自 README 示例)如下:
module.exports = {
siteMetadata: {
title: `Your site title`,
description: `Your site description`,
site_url: `https://your-site-url.com`,
},
plugins: [
{
resolve: `gatsby-plugin-feed`,
options: {
feeds: [
{
serialize: ({ query: { site, allMarkdownRemark } }) => {
return allMarkdownRemark.nodes.map(node => {
return Object.assign({}, node.frontmatter, {
description: node.excerpt,
date: node.frontmatter.date,
url: site.siteMetadata.siteUrl + node.fields.slug,
guid: site.siteMetadata.siteUrl + node.fields.slug,
custom_elements: [{ "content:encoded": node.html }],
})
})
},
query: `
{
allMarkdownRemark(
sort: { order: DESC, fields: [frontmatter___date] },
) {
nodes {
excerpt
html
fields {
slug
}
frontmatter {
title
date
}
}
}
}
`,
output: "/rss.xml",
title: "Your Site's RSS Feed",
},
],
},
},
],
}
serialize 是整个配置的核心:它为每篇文章返回一个 RSS item 对象,其中 url/guid 由 siteMetadata.siteUrl 与示例站点 gatsby-node.js 生成的 node.fields.slug 拼接而成,custom_elements 中的 content:encoded 则把 node.html(完整 HTML 正文)注入 RSS 条目——这正是“全文订阅”效果的关键所在。
进阶选项:match 与 link
除必填项外,feed 还支持两个插件专属的可选配置(详见 README):
match:控制哪些页面会被插入 feed 引用标签。不配置时,所有页面都会自动加入 feed 的<link>引用;配置为字符串(如"^/blog/")时,该字符串会被编译为正则表达式,只有pathname匹配的页面才会被插入引用。link:覆盖由output推导出的默认 RSS 链接,适用于接入第三方分发服务(如 FeedBurner)的场景。
所有额外选项都会透传给 rss 包的 feedOptions,因此你可以直接写入 language、custom_namespaces(如扩展媒体命名空间)等标准 RSS 字段:
{
serialize: () => { /* ... */ },
query: `/* ... */`,
output: "/rss.xml",
title: "Your Site's RSS Feed",
match: "^/blog/",
link: "https://feeds.feedburner.com/gatsby/blog",
custom_namespaces: {
media: 'http://search.yahoo.com/mrss/',
},
language: `en-US`,
}
源码级原理:onPostBuild 阶段发生了什么
gatsby-plugin-feed 的生成时机是生产构建,而非开发模式。README 明确指出:插件只在 production 模式下生成 XML 文件,要本地验证需运行 gatsby build && gatsby serve。
生成逻辑集中在 src/gatsby-node.js 的 onPostBuild 钩子中,其流程可以拆解为四步:
- 合并默认选项:
options = { ...defaultOptions, ...pluginOptions },将 internals.js 中的默认配置与用户在gatsby-config中传入的配置合并; - 执行站点级查询:调用
runQuery(graphql, options.query)获取siteMetadata,作为所有 feed 的共享上下文; - 遍历每个 feed:若 feed 自带
query,则执行它并借助merge({}, baseQuery, result)把站点级数据与 feed 级数据合并;随后调用feed.serialize(locals)得到 item 数组,通过new RSS(setup(locals))创建 RSS 实例并逐条merged.item(item); - 写出 XML:
outputPath = path.join('./public', feed.output),自动创建缺失目录后写入rssFeed.xml()字符串。
两个值得注意的实现细节:
setup是默认的 feedOptions 装配函数,它把siteMetadata展开后与 feed 自身配置合并,这正是 README 中“把title、description、siteUrl直接透传给 rss feedOptions”这一描述的源码实现;- 若
serialize不是合法函数,插件会通过reporter.warn输出警告并跳过该 feed 的生成,且runQuery在 GraphQL 返回errors时会直接抛出异常——这两点共同保证了配置错误能尽早暴露。
配置校验:pluginOptionsSchema
src/plugin-options.js 使用 Gatsby 的 Joi 校验机制对插件选项做运行时校验。feed 级对象的必填项(output、query、title、serialize)以及可选的 match、link 都必须在 schema 内声明,其余字段通过 .unknown(true) 放行以便透传给 rss 包。更关键的是,顶层与 feed 级 query 都经过 .external() 钩子调用 parse(query) 做 GraphQL 语法解析,非法查询会在构建早期抛出明确错误信息("query" must be a valid GraphQL query...)。
内置默认 query 与默认 feed
如果你像示例站点一样使用空 options,插件会启用 internals.js 中的默认行为:
- 站点级查询自动读取
siteMetadata的title、description、siteUrl(含site_url别名); - 默认 feed 查询
allMarkdownRemark,按frontmatter.date降序取前 1000 条,输出到rss.xml。
这意味着:只要你的站点遵循「Markdown 文章 + siteMetadata 三项元信息」这一约定,就能在零配置下得到一份可用的 RSS。但要注意,默认查询不会为条目拼接 url 等链接字段,因此生产站点通常仍需要自定义 serialize 与 query 来生成带完整 URL 的条目。
构建验证与页面展示
package.json 中的脚本与插件要求完全对应:
{
"scripts": {
"develop": "gatsby develop",
"build": "gatsby build",
"start": "npm run develop"
}
}
验证步骤:
npm install(依赖gatsby、gatsby-plugin-feed、gatsby-source-filesystem、gatsby-transformer-remark、react);npm run build触发生产构建,onPostBuild生成public/rss.xml;npm run develop或gatsby serve访问站点。
页面端 src/pages/index.js 给出了最简单的订阅入口呈现方式——直接渲染一个指向 /rss.xml 的普通链接:
import React from "react"
const IndexRoute = () => (
<div>
<p> Welcome to the GatsbyJS RSS Demo.</p>
<p>
<a href="/rss.xml">Click here</a> to see the generated RSS Feed.
</p>
</div>
)
export default IndexRoute
生产实践中,可以在此基础上结合 feed 配置里的 match 选项(如 "^/blog/"),让页面在构建时自动注入 RSS 的 <link rel="alternate" type="application/rss+xml"> 引用,从而让订阅器自动发现你的 feed。
测试与回归保障
仓库为插件提供了完整的测试支撑:src/tests/gatsby-node.js 与对应的快照文件 gatsby-node.js.snap、gatsby-ssr.js.snap。这些测试覆盖了 onPostBuild 的 XML 输出形状以及 SSR 阶段 feed 引用标签的注入逻辑,是理解插件输出格式与行为边界的直接参考:当你自定义 serialize 的返回字段时,可通过比对快照中的 XML 结构来确认 RSS 的合法性。
小结
以 posts/2017-03-09-first-post.md 为代表的示例文章,加上 gatsby-config.js、gatsby-node.js 与页面入口,构成了一条从 Markdown 到 RSS 的完整参考实现。其背后是 gatsby-plugin-feed 在 onPostBuild 阶段执行 GraphQL 查询、合并默认配置、逐条序列化并写出 XML 的稳健机制。若要为你的博客接入 RSS,遵循三步即可:在 siteMetadata 中补齐站点元信息、通过 gatsby-transformer-remark 暴露文章的 title/date/excerpt/html、再用 serialize + query 定义条目与输出路径。若需自定义多 feed、按路径匹配注入订阅引用或对接第三方聚合服务,则进一步使用 match 与 link 选项即可。