Statac

Publish

Feeds, sitemaps and other files

Statac builds a feed for your lists, and files such as a sitemap, with templates of yours, so what a reader sees is yours to word.

Which lists have a feed?

List Has a feed
The site’s own list Unless its page says feed: false
Each folder at the top of content/ Unless its page says feed: false
Any other list: a folder inside another, a tag, a series, a list made with posts: Only where its page asks
On a site in several languages, each language’s own list and the folders at its top As the site’s list and a top folder do: lists in each language

A feed is published as feed.xml beside its list’s page: /feed.xml for the site, /posts/feed.xml for content/posts/. To ask for a tag’s feed, in content/tags/rust.md:

---
title: Rust
feed: true
---

On a list’s page, feed: says where its feed goes:

feed: What it does
true A feed, at feed.xml beside the list’s page.
false No feed.
atom.xml The feed’s file, beside the list’s page: /posts/atom.xml.
/index.rss The feed’s own address, from the top of the site.

pages.feed: false in statac.yaml gives the site no feeds. From a folder’s index.md, pages.feed: reaches the lists inside it, not the folder’s own.

A list with no dated posts has no feed. A feed also needs the site’s title, url and feed_key: see when a line is missing.

Write the feed’s template

An Atom feed of a list’s newest 20 posts, whole, in templates/feed.xml:

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>{{ site.title }}</title>
  <link rel="self" href="{{ list.feed | absolute }}"/>
  <link rel="alternate" href="{{ list.url | absolute }}"/>
  <id>{{ feed_id(page) }}</id>
  <updated>{{ posts | newest_date | moment }}</updated>
  {% for post in posts[:20] %}
  <entry>
    <title>{{ headline(post) }}</title>
    <link rel="alternate" href="{{ post.url | absolute }}"/>
    <id>{{ feed_id(post) }}</id>
    <updated>{{ post.date | moment }}</updated>
    <content type="html">{{ post.content | absolute }}</content>
  </entry>
  {% endfor %}
</feed>
Name What the feed’s template is handed
list The list, as any template sees it, with its title, url and feed.
posts The list’s dated posts, newest first, whatever order the list keeps.
page The page that speaks for the list.
site The site, as on any page.

An entry must have a title, and headline(post) gives one to a post with none. absolute makes each address in post.content whole, so links and pictures work in a feed reader.

Statac ships no feed of its own. A feed at /feed.json is built with templates/feed.json where there is one, and with templates/feed.xml otherwise.

site.feed and a list’s feed hold the address of the feed published, or nothing where none is, so a page links its feed inside {% if list.feed %}.

What a new site’s feed carries

In the feed Taken from
Its title The site’s title, after the list’s own on a list’s feed: Notes - Site
<subtitle> tagline in statac.yaml
Who wrote it author in statac.yaml, or else the site’s title
Each entry’s title called(post): the post’s title, or a note’s date and time, as 4 Oct 2026, 09:12
When each post was published Its date
When it last changed Its updated:, where it has one
A <category> for each tag The post’s tags

A new site with a url has three feeds: /feed.xml, /notes/feed.xml and /photos/feed.xml.

Entry ids

<id>{{ feed_id(post) }}</id>
<id>urn:uuid:1174090d-113e-8f97-957f-75aa65842541</id>

A feed reader knows each post by its id, and shows a post with an id it hasn’t seen as new. feed_id gives a page’s id, made from the site’s feed_key and the page’s address, or its id: where it writes one. It is the same on every build.

In statac.yaml:

feed_key: 0b6d0c4e-2f7a-8d51-9a8e-3c1f5e7a9b20

statac new writes a key made fresh for the site.

Warning:

Once a feed is published, never change feed_key. Every id changes with it, and feed readers show every post again as new.

Page Its id
A post Made from feed_key and the post’s address.
A post with an id: Made from feed_key and the id:, or the id: itself where it is shaped as fc370633-88f0-8092-9f96-7749dbfff0eb.
A list’s page, for the feed’s own <id> Made from feed_key and the page’s address, or its id:.
A post dated before feed_ids_before Its whole address.
The same, with feed_ids_from: jekyll The id Jekyll’s feed gave it: the folder of its address, then the name of its file.
A list’s page, where feed_ids_before is set The feed’s whole address.

feed_ids_before, a day, is for a site moving from another tool, so its readers see nothing new on the move. statac import writes it.

When a line is missing

error  No feed key: `statac.yaml` has no `feed_key`
  Add `feed_key: 90bd16ae-ec81-8d6f-889b-c7fec905d3e6` to `statac.yaml` and never change it: feed readers know each post by an id made from it.
  More: https://statac.dev/e/no-feed-key/

Wrote nothing: 1 error stopped the build.

A feed needs three lines in statac.yaml: title names it, url makes its links whole and feed_key makes its ids. Without a title or a url, the build goes on with no feeds. With a url and no feed_key, it stops, offering a key.

Have Statac write each id:

Statac writes no id: into a post unless you ask. In statac.yaml:

write_feed_ids: true
Wrote an `id` into `content/posts/post-2.md`.

With it, statac serve and statac stamp write an id: line into each post in a feed that statac build hasn’t yet published from this computer. statac build never writes one.

No line is written into a draft, a post waiting for its day, a post that has an id:, or a post published before the setting was turned on.

Wrote no feed ids: they start once `statac build` has published the site.

When a post moves

---
title: Post 1
date: 2026-01-01
permalink: /writing/post-1/
id: fc370633-88f0-8092-9f96-7749dbfff0eb
---

With no id:, a post’s id is made from its address, so a post that moves gets a new one and feed readers show it again as new. An id: stays with the post wherever it goes.

Statac remembers where each build published each post in a feed. Where one moves with no id:, it warns and offers the line above: the id the post had.

Note:

A post moved and edited at once, with no redirect_from:, is taken as new, and nothing is said. With its old address in redirect_from:, Statac knows it again.

What Statac remembers is in .statac/feeds, on the computer that builds the site. Without it, a post moved before the next build goes unnoticed.

Sitemaps, robots.txt and other files

In content/sitemap.md:

---
permalink: /sitemap.xml
layout: sitemap.xml
needs: url
---

A page is built as HTML unless its layout names another kind of file: layout: sitemap.xml builds this page with templates/sitemap.xml, at the file permalink names. What the template shows is made safe for that kind of file:

Layout ends What the template shows is written
.html, .xml As markup, so < is written &lt;
.json As JSON values, so the template puts no quotes round text
.txt As it is

needs: url keeps the page out until the site has a url.

A sitemap

In templates/sitemap.xml:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
  xmlns:xhtml="http://www.w3.org/1999/xhtml">
{% for entry in site.pages if entry.url is endingwith("/") %}  <url>
    <loc>{{ entry.url | absolute }}</loc>
{% if entry.translations %}    <xhtml:link rel="alternate" hreflang="{{ entry.language }}" href="{{ entry.url | absolute }}"/>
{% for other in entry.translations %}    <xhtml:link rel="alternate" hreflang="{{ other.language }}" href="{{ other.url | absolute }}"/>
{% endfor %}{% endif %}  </url>
{% endfor %}</urlset>

site.pages holds every page the site publishes. The test entry.url is endingwith("/") leaves out the 404 page, the sitemap and robots.txt.

Each page with translations lists every language it is in, itself among them.

robots.txt

robots.txt is built the same way, from a page with permalink: /robots.txt and layout: robots.txt, with templates/robots.txt.

Keep a page out of search engines

<meta name="robots" content="noindex">

This line, in a page’s <head>, asks search engines to leave the page out. Write it in the template that builds such a page.

Where next?

Something unclear or wrong? Open an issue on GitHub.