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 hosts that already load feed data while planning routes can return that
data from the normally build-only outputs(ctx) lifecycle instead of reloading it from
feeds.items. Return default items, named collections, and optional
collectionNames; Ox Content still uses the same writeFeedFiles() path and
publish-state filtering. See Custom host lifecycle.
Set dev.feedOutputs: true on oxContentCustomHost() to serve those coordinated
feed files during Vite development without adding duplicate feed routes.
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.