Pagedeck

Feeds

build.feed publishes one collection as an RSS feed at /rss.xml, and links it from the <head> of every page so a reader finds it without being told.

build: {
  outDir: "./site",
  origin: "https://example.com",
  feed: {
    collection: "posts",
    title: "My Site",
    description: "Recent posts",
    item: { title: "title", description: "summary", pubDate: "date" },
  },
  // ...
}

A site that declares no feed gets exactly the build it got before the field existed. No file is written, no manifest row appears, and no page carries a link.

What goes in it

The items are the pages of that collection, not the entries of it. An entry the build routed no page for has no address to point a reader at, so it is not an item — which is what makes three things true for free:

  • An entry outside its publication window is absent, because it is not a page either.
  • An experiment arm is absent, because an arm is not a page.
  • Every item's <link> and <guid> is the page's own absolute URL, composed the same way its canonical is.

A page a locale got from its fallback chain is skipped as well. It is a copy of a post already in the feed at another URL, and a reader subscribing would see the same article once per locale that fell back to it. A locale that really translated the post has its own entry, its own page and its own item.

Every locale's items share one feed, and the feed says nothing about language. There is no <language> element on the channel and no xml:lang on an item, so a site that translates its posts publishes one file in which the translations sit interleaved, and a reader has no way to subscribe to one language. If that is not what you want, the answer today is to declare the feed over a collection only one locale publishes.

Items are newest first, by the field item.pubDate names. A collection that declares no date field gets a feed ordered by URL, which is deterministic and of no use to a reader — declare the date.

The mapping

item names the fields of an entry, not values:

key what it names
title the field holding the item's title. Required.
description the field holding its summary. Optional.
pubDate the field holding its publication instant. Optional.

Store the instant as ISO-8601 in UTC (2026-06-01T09:00:00Z) — the same format a publication window wants. The build converts it to the RFC-822 form RSS asks for.

A stamp with no timezone designator is read as UTC, not in the timezone of whatever machine runs the build, so 2026-06-01T09:00:00 and 2026-06-01T09:00:00Z are the same instant here and one store writes one feed everywhere. An offset is honoured where you write one (2026-06-01T11:00:00+02:00), and items are ordered by the instant a stamp names rather than by how it is spelled.

A field an individual entry does not carry writes no element for that entry rather than an empty one, and a pubDate value that is not an ISO-8601 date or date-time writes no <pubDate>. Which fields an entry holds is content; if a field is required, say so in the collection's schema, where the failure names the entry.

origin is required

Every <link> and <guid> in a feed is an absolute URL — RSS has no relative form, and a reader has no base to resolve one against — so feed without origin is refused when the config loads rather than quietly writing a file nobody can follow. The same rule sitemap and xDefault follow, for the same reason.

Every page of a site that declares a feed carries one element, in the <head> beside the canonical:

<link rel="alternate" type="application/rss+xml" title="My Site" href="https://example.com/rss.xml">

title is the channel title you declared. It is what a browser extension or a reader shows when it offers to subscribe from a page.

One feed, at one address

/rss.xml is fixed, and there is one feed per site rather than one per locale or per output tree. The link every page carries is absolute, so a locale served from its own domain points at the same file and a reader subscribing from either tree gets the same feed.

The one file is written into the output tree that serves your origin — the locale whose domain is the origin's host, or the default tree when no locale claims it. So on a site whose locales all sit on domains of their own, the feed is inside the origin's tree and not beside them.

That also means a page of your own cannot be published at /rss.xml: the build refuses the collision rather than overwrite one file with the other, and the fix is to move the page.

Atom and JSON Feed are not emitted, and neither are per-tag or per-category feeds. One format and one feed is the whole of what this field does.

Incremental builds

pagedeck build --incremental composes and writes the feed on every run, unlike a sitemap, which a partial build can keep. The file is a pure function of the collection's pages and their entries, so a rewritten one holds the bytes it already held — it costs one small write and never goes stale.