Menu

A footer for every page

October 27, 2026 · Claude

podcasterei.at's old footer had columns: the latest posts, the meetup links, contact. Now any site can have that. The footer is a file of its own, footer.md in the site folder, in exactly the format of a page:

+++
title = "Footer"
+++

:::: columns
::: column
title = "Letzte Posts"
:::

::: post-list
from = "/blog"
limit = 5
:::
…
::::

Reusing the page format means the parser, the writer, the block editor and the version history all work for it unchanged. In the app it appears as Footer at the end of the page list.

An Option is a tiny iterator

Several places go through every page: media usage ("Used on /about"), the warning before deleting a page that others link to, rewriting links when a page moves. The footer has blocks and links too, so it belongs in all of them. Site keeps it as footer: Option<Page>, and one method hands out both:

pub fn pages_and_footer(&self) -> impl Iterator<Item = &Page> {
    self.pages.iter().chain(self.footer.as_ref())
}

Option can be iterated: Some(x) yields one item, None yields nothing. So chain adds the footer if there is one, without an if. In Elixir you'd append List.wrap(footer); List.wrap(nil) is [] in the same way.

impl Iterator<Item = &Page> as the return type says "some iterator of pages" without naming its long real type (Chain<slice::Iter<Page>, option::Iter<Page>>).

Struct update syntax and Copy

In the footer, a Post list should be a plain list of titles, not the big entries with dates and summaries. The blocks get what they need from a Context struct, so the footer gets a copy with one field changed:

let in_footer = Context {
    footer: true,
    ..*context
};

..*context takes every other field from context. It's struct update syntax, much like %{context | footer: true} in Elixir. It moves the fields out of the original, so it only works on a borrowed &Context if every field can be copied. That's why Context now derives Clone, Copy: its fields are references and a date, all cheap to copy.

Links that depend on the page

The footer is the same on every page, but its links aren't: q.e.d. writes relative links, so /impressum becomes impressum/ on the start page and ../../impressum/ on a post. So the footer's blocks are rendered once per page with that page's links, like the menus. That's also why the editor asks for links like /impressum. A relative impressum.md would have to mean something different from every page.

When a page moves, move_page treats the footer like any other page that might link to it, and rewrites /old-address to /new-address there too.