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 < |
.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.
- A search index of your own goes by its own mark: see keep a page out of the index.
- A preview carries no mark, so a search engine that can reach it may list it.
Where next?
- Lists, tags and series: every list, and the page that speaks for each
- Templates:
feed_id,headline, and theabsolute,momentandnewest_datefilters - Building and publishing: a preview’s feeds and sitemap
- Arriving from another tool: an old site’s feed ids and addresses
Something unclear or wrong? Open an issue on GitHub.