RSS, Atom, and JSON feeds#
When feeds is enabled and ssg.siteUrl is set, the SSG build writes
machine-readable feeds from a named collection:
feed.xml— RSS 2.0atom.xml— Atom 1.0feed.json— JSON Feed 1.1
The feature is off unless you turn it on. Existing sites stay unchanged.
import { oxContent } from "@ox-content/vite-plugin";
export default {
plugins: [
oxContent({
feeds: true,
ssg: {
siteUrl: "https://example.com",
},
}),
],
};
false or omitted keeps the files off. true enables the defaults: all three
formats, the content collection (or the first configured collection), and a
20-item limit. A single object is one default feed and overrides only the
fields you set:
oxContent({
feeds: {
formats: ["rss", "json"],
collection: "blog",
limit: 10,
path: "/feeds",
},
ssg: {
siteUrl: "https://example.com",
},
});
A named record or array writes multiple feeds. Each channel can set its own collection, path, formats, and metadata:
oxContent({
feeds: {
blog: {
formats: ["rss"],
collection: "blog",
path: "/",
title: "blog | example.com",
description: "Technical articles",
language: "en",
image: "https://example.com/icon.png",
favicon: "https://example.com/icon.png",
copyright: "© 2026 example.com",
},
media: {
formats: ["rss"],
collection: "media",
path: "/works/media",
title: "Media | example.com",
language: "ja",
},
},
ssg: {
siteUrl: "https://example.com",
},
});
Programmatic items#
A channel can set items instead of collection when the feed source is a JSON
file, database result, or another curated build-time source. The resolver runs
during SSG and may return a promise:
import media from "./src/contents/external-rss/media.json";
oxContent({
feeds: {
media: {
formats: ["rss"],
path: "/works/media",
title: "Media | ryoppippi.com",
items: async () =>
media
.filter((item) => !item.playlist)
.map((item) => ({
title: item.title,
url: item.link,
id: `media:${item.link}`,
date: item.pubDate,
description: `${item.kind === "podcast" ? "Podcast" : "YouTube"} | ${item.title}`,
author: { name: "ryoppippi", url: "https://ryoppippi.com" },
language: item.lang,
})),
},
},
ssg: {
siteUrl: "https://ryoppippi.com",
},
});
Set either collection or items on a channel. Supplying both is rejected.
Programmatic items support title, url or loc, optional id, date,
description, content, author / authors, image, attachments, and
language. The RSS, Atom, and JSON Feed renderers emit the fields that each
format supports.
Custom dev servers#
Use renderFeedFiles() when a custom Vite middleware or dev server wants the
same feed bytes without writing temporary files. It accepts the same resolved
feed options, collection data, publish-state filtering, base, and SSG site
metadata as writeFeedFiles(). Each result has a safe site-relative path, a
contentType, and the serialized content.
import type { Plugin } from "vite";
import { renderFeedFiles, resolveFeedsOptions } from "@ox-content/vite-plugin";
const feeds = resolveFeedsOptions({
media: {
formats: ["rss", "atom", "json"],
path: "/works/media",
title: "Media | ryoppippi.com",
items: async () => [
{
title: "Guest appearance",
url: "https://media.example.com/episode",
date: "2026-08-01",
author: { name: "ryoppippi", url: "https://ryoppippi.com" },
},
],
},
});
export function feedMiddleware(): Plugin {
return {
name: "site-feeds",
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
const rendered = await renderFeedFiles({
options: feeds,
siteUrl: "https://ryoppippi.com",
siteName: "ryoppippi.com",
base: "/",
});
if (rendered.warning) {
server.config.logger.warn(rendered.warning);
return next();
}
const requestPath = new URL(req.url ?? "/", "http://localhost").pathname.replace(
/^\/+/,
"",
);
const file = rendered.files.find((candidate) => candidate.path === requestPath);
if (!file) {
return next();
}
res.statusCode = 200;
res.setHeader("Content-Type", file.contentType);
res.end(file.content);
});
},
};
}
Unsafe paths, invalid site URLs, and duplicate output paths fail with the same
warnings as writeFeedFiles().
| Option | Type | Default |
|---|---|---|
feeds |
boolean / one feed / named record / array |
false |
formats |
("rss" | "atom" | "json")[] |
["rss", "atom", "json"] |
collection |
string |
content, else the first collection |
items |
FeedItemInput[] / async resolver |
omitted |
limit |
number |
20 |
path |
string |
/ (site root) |
title |
string |
SSG site name |
description |
string |
SSG site description |
language |
string |
omitted |
image |
string |
omitted |
favicon |
string |
omitted |
copyright |
string |
omitted |
path is the site-relative directory for the generated files. /feeds writes
feeds/feed.xml, feeds/atom.xml, and feeds/feed.json. Channel title,
description, language, image, favicon, and copyright override the
site defaults where the format has a matching field (JSON Feed has no
copyright).
Items are sorted newest first. The sort key is frontmatter date, then
lastUpdated when date is missing. Entries with draft: true are omitted.
If feeds is enabled without ssg.siteUrl, no files are written. The build
continues and emits a warning.
Titles and descriptions are escaped so they cannot break out of XML or JSON.
Blog index items#
External posts aggregated by Blog blog.feeds stay on the blog
index only. They are omitted from these generated files. There is no include
switch in this release.