Feed Subscription NEW
About 984 words
Overview
The theme can generate RSS 2.0, Atom 1.0 and JSON Feed 1.1 files during a production build, and inject <link rel="alternate"> into the page <head> so that readers and automation tools can discover the subscription URLs automatically.
Unlike the common "one feed per site" setup, feeds are aligned with the collections model and support splitting channels by locale × collection:
.vuepress/dist
feed.xml
atom.xml
feed.json
blog
feed.xml
notes
feed.xml
en
feed.xml
blog
feed.xml
Quick Start
Configure hostname and feed:
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
theme: plumeTheme({
hostname: 'https://example.com',
feed: true, // only outputs /feed.xml
}),
})hostname is required
Every link in a feed must be absolute. Without hostname the theme skips generation and logs a warning. base must also match the deployment path of the site, otherwise subscription URLs point to the wrong place.
Production build only
Feeds are generated in the onGenerated hook, i.e. only during vuepress build. The dev server (vuepress dev) generates no feed.
Multiple Collections and Locales
Collection channels
Only the global channel is generated by default. Sites with multiple collections can enable perCollection to emit a dedicated channel for every eligible collection:
export default defineUserConfig({
theme: plumeTheme({
hostname: 'https://example.com',
feed: {
perCollection: true,
formats: ['rss', 'atom', 'json'],
},
locales: {
'/': {
collections: [
{ type: 'post', dir: 'blog', title: 'Blog', link: '/blog/' },
{ type: 'post', dir: 'notes', title: 'Notes', link: '/notes/' },
],
},
},
}),
})Output:
| Channel | File | Public URL |
|---|---|---|
| Global | dist/feed.xml | https://example.com/feed.xml |
| Blog | dist/blog/feed.xml | https://example.com/blog/feed.xml |
| Notes | dist/notes/feed.xml | https://example.com/notes/feed.xml |
An article may appear in both the global channel and a collection channel. This is expected.
Collection level switches
feed on a collection controls whether it is included and whether it owns a dedicated channel:
// `doc` collections are excluded by default, `feed: true` opts in explicitly
const doc = { type: 'doc', dir: 'guide', title: 'Guide', sidebar: 'auto', feed: true }// Disable subscriptions for a collection
const post = { type: 'post', dir: 'notes', title: 'Notes', feed: false }feed: true is equivalent to feed: {}: the collection is included in feeds and gets a dedicated channel, without being limited by the theme level perCollection / includeDocCollections.
See collection options for every field.
Locales
Each locale outputs at least one global channel, and the output path follows the locale prefix:
dist/feed.xml → https://example.com/feed.xml
dist/en/feed.xml → https://example.com/en/feed.xmlchannel.link and collection.feed.link are locale relative (e.g. /blog/). The theme prepends the current locale prefix automatically, so do not write /en/ yourself.
Custom filenames and directories
export default defineUserConfig({
theme: plumeTheme({
feed: {
filename: {
rss: 'rss.xml',
atom: 'atom.xml',
json: 'feed.json',
},
channel: {
link: '/subscribe/', // writes the global channel to dist/subscribe/
title: 'Subscribe',
author: { name: 'pengzhanbo', email: 'i@example.com' },
ttl: 60,
},
}
})
})Full Content and Size Trade-offs
content defaults to 'excerpt', which only outputs the summary. Set it to 'full' to output the rendered HTML of the page.
Vue components are not rendered in full mode
The feed content comes from page.contentRendered at build time, where Vue components provided by the theme and vuepress-plugin-md-power (<VPBadge>, <VPCard>, REPL, charts…) are not yet parsed by a browser. Their tags are stripped and only their inner text is kept. Readers should visit the original page for interactive content.
Full mode increases file size significantly. A single channel larger than 10 MB triggers a warning, and you should switch to content: 'excerpt' or lower limit.
Security and Privacy
The following pages and content never enter any feed:
| Case | Behaviour |
|---|---|
Page with password | Excluded |
Matched by encrypt.rules | Excluded |
encrypt.global | The whole feature is disabled with a warning |
frontmatter.draft: true | Excluded |
frontmatter.article: false | Excluded |
frontmatter.feed: false | Excluded (highest priority) |
| Home / post list / tag pages and other non-content pages | Excluded |
::: encrypt snippet | The corresponding tag is removed from the content |
Exclude a single page:
---
title: Internal draft ideas
feed: false
---Override the metadata of a single entry:
---
title: Article title
feed:
title: Title shown in the reader
summary: Custom summary
guid: /custom-id/
---Auto Discovery
During the build, every page that belongs to a generated channel receives the matching link in <head>:
<link rel="alternate" type="application/rss+xml" title="Blog" href="https://example.com/blog/feed.xml">- One link per enabled format;
- Nothing is injected when the page already declares an
alternatelink of the same type; - Set
linkTag: falseto disable it.
Readers only need to paste the site domain into Feedly / Inoreader / NetNewsWire to discover the feeds.
Validation
- RSS / Atom: https://validator.w3.org/feed/
- JSON Feed: https://validator.jsonfeed.org/
Verify determinism locally:
pnpm docs:build
shasum dist/feed.xml dist/atom.xml dist/feed.json
# build again and compare, the results must be identicalFAQ
Make sure hostname is a valid site origin (e.g. https://example.com). If the site is deployed under a sub path, use base (e.g. /docs/) instead of appending it to hostname.
The output path is the channel home route plus the filename. Two collections sharing the same link conflict: the first channel wins and a warning is logged. Use collections[].feed.link to give them different channel links.
Feeds use "local time + timezone offset", e.g. 2025-06-10T08:00:00+08:00. Building in a different timezone produces a different string for the same instant. CI sets TZ=Etc/UTC, so artifacts are reproducible.
A feed entry needs a date: frontmatter.createTime → frontmatter.date → git.createdTime → git.updatedTime. Pages without any date are skipped and counted in the build log. Also check whether the page is encrypted, a draft, or marked with article: false / feed: false.
onGenerated only runs during a production build. Run vuepress build and preview the output with a static server.
