Shape the site
Templates
A template is a file in templates/ that pages are built in: HTML, with
places for each page’s values.
Write a template
In templates/page.html:
<!doctype html>
<html lang="{{ page.language }}" dir="{{ page.direction }}">
<title>{{ headline(page) }} - {{ site.title }}</title>
{% if page.title %}<h1>{{ page.title }}</h1>{% endif %}
{{ page.content }}
</html>
{{ ... }} shows a value. {% if ... %} shows a part only where
something is so, and {% for ... %} shows it once for each item. The
language is Jinja, as MiniJinja reads it.
A value is written with <, > and & made safe. page.content is
HTML already and goes in as it is, as does a value passed through
safe.
Two things on a built page are Statac’s own until a file of yours takes their place:
| What | Statac writes | Yours in its place |
|---|---|---|
| The colours for code | code.css, published with each build |
assets/code.css |
| Six pieces of the writing: headings, links, code blocks, inline code, a footnote’s marker and the notes at the foot | Plain HTML | A file for the piece in snippets/markdown/ |
Which template builds a page?
A page is built with the first of these:
- the template its own
layout:names, aslayout: post; - the template its
type:names, wheretemplates/has one; - a
layout:handed down from its folder orstatac.yaml, aspages.layout: post, unless the page speaks for a list; homefor the home page,folderfor any other page that speaks for a list, andpagefor the rest.
The pages that speak for a list are a folder’s index.md, a tag’s or a
series’ page, a page with posts: or each:, and the later pages of
each: see the page at a list’s address.
On a page with each:, layout: names the template of the pages it
makes, not its own: see
make a page for each item.
A template is named by its file’s name without .html: post is
templates/post.html. One for another kind of file keeps its ending, as
layout: sitemap.xml, and what it shows is made safe for that kind: as
markup in HTML and XML, as JSON values in .json, and not at all in
.txt.
Statac also looks for two templates by name:
| Template | Builds |
|---|---|
templates/feed.xml |
Each list’s feed. A feed at another ending, such as /feed.json, is built with the template of that name, templates/feed.json. |
templates/redirect.html |
The small page at each old address, and each page with redirect_to. |
Share a frame with extends
In templates/post.html:
{% extends "base" %}
{% block main %}
<article>
{{ page.content }}
</article>
{% endblock %}
templates/base.html holds the whole page, with
{% block main %}{% endblock %} where each page’s own part goes. Each
block takes the place of the block of the same name there.
Statac has no include, import or from. For a piece used in many
templates, such as a post in a list, write a snippet and
call it: {{ card(post) }}.
What a template is handed
| Template | Is handed |
|---|---|
| Any page’s | page, the page being built, and site, the same on every page. |
| A feed’s | list, the list; posts, its dated posts, newest first, whatever order the list keeps; page, the page speaking for the list; and site. |
redirect.html |
to, where readers are sent; title; page, the page writing the redirect; and site. See a redirect page. |
| A snippet | What its call hands it, and page and site. |
Every page a template meets has the names page has, so post.url
works as page.url does. In a snippet too, page is the page being
built, not the post a call hands it.
Serve’s panel shows every name a template can write on a page, with its value there.
The site’s words
{% set words = site.data.words[page.language] %}
<a href="#content">{{ words.skip }}</a>
Statac hands a template nothing called words. A new site keeps the
words its pages show in data/words.yaml, under each language’s code,
and each template that shows one reads them with the set line above.
The line in base.html doesn’t reach a template that extends it. See
the words your pages show.
When a page has no such value
{% if page.summary %}
<p>{{ page.summary }}</p>
{% endif %}
Where a template shows a value a page doesn’t have, Statac
stops rather than publish an empty space. Say what a
page without it shows, as above, or show nothing with
{{ page.summary | default("") }}. Defaults chain:
{{ page.description | default(page.summary) | default("") }}.
| A filter such as | Does with a page without the field |
|---|---|
selectattr("group", "eq", "a"), rejectattr("group", "eq", "a") |
selectattr leaves it out, and rejectattr keeps it. |
sort(attribute="rank") |
Puts it after the rest, in the order it came. |
groupby("rank"), unique(attribute="rank"), map(attribute="rank") |
Stops. Leave such pages out first with selectattr("rank", "defined"). |
A test such as {% if page.subtitle is defined %} never stops: it may
ask about a field no page has yet.
How the built page’s source is laid out
<main>
{{ page.content }}
</main>
builds as
<main>
<p>The first paragraph.</p>
<ul>
<li>One</li>
<li>Two</li>
</ul>
</main>
Statac adds no indentation and no blank line of its own, and takes none of yours away.
- A line holding only a template tag, such as
{% if %}or{% for %}, leaves nothing behind. - A value on a line of its own, such as
{{ page.content }}or a snippet call, has each of its lines indented as that line is. Where it comes to nothing, the line isn’t written. - Lines inside
<pre>,<textarea>and an attribute’s value are left exactly as they are.
What a template sees on page
| Name | What it gives | Example |
|---|---|---|
page.title |
The title in the page’s front matter. A page Statac makes for a list has none. |
<h1>{{ page.title }}</h1> |
page.summary |
The summary in its front matter. |
{{ page.summary | default("") }} |
page.date, page.updated |
Its dates, shown in the site’s date_format, each with its year, month, day and weekday. |
<time datetime="{{ page.date | moment }}">{{ page.date }}</time> |
page.content |
The writing, built into HTML. Shown on another page, as in a list or a feed, its relative addresses are written out in full: see links. | {{ page.content }} |
page.excerpt |
The start of the writing, built into HTML: up to its <!--more--> line, or else its first paragraph. A page with neither has none. See excerpts. |
{{ post.excerpt }} |
page.excerpt_marked |
true where the excerpt ends at a <!--more--> line. |
{% if post.excerpt_marked %} |
page.opening |
The words of the first paragraph, as text with no markup. A page with no paragraph has none. | {{ page.summary | default(page.opening) | default("") }} |
page.words |
How many words a reader reads, code blocks and footnote markers left out. In Chinese and Japanese, each character is one. | {{ [1, page.words // 200] | max }} min read |
page.maths |
true where the writing holds maths. A list showing excerpts asks each post, post.maths. |
{% if page.maths %}{{ css("maths.css") }}{% endif %} |
page.toc |
The page’s headings, each with title, id, url (a link to it on the page), level, and the headings beneath it as headings. |
{% for heading in page.toc %} |
page.url |
Where the page is published, the site’s path included: /posts/post-1/. |
<a href="{{ post.url }}"> |
page.slug |
The name in the page’s address, its last part: post-1. |
<article id="{{ page.slug }}"> |
page.source |
The file the page was read from: content/posts/post-1.md. |
{{ page.source }} |
page.language, page.direction |
The page’s language, by its code, and ltr or rtl. |
<html lang="{{ page.language }}" dir="{{ page.direction }}"> |
page.translations |
The same page in the site’s other languages, the main language first. Empty where it has none. See translations. | {% for other in page.translations %} |
page.tags, page.series, page.folder |
Its tags, its series, and the folder it is filed in. | {% if "Rust" in page.tags %} |
page.list, page.posts, page.number |
On a page that speaks for a list: the list, the posts this page of it shows, and which of its pages this is, from 1. See lists in templates. | {% for post in page.posts %} |
page.previous, page.next |
The pages either side of this one. See previous and next. | {% if page.next %} |
page.post |
On a page made for a post by each: site.posts, the post. See a page for each post. |
{{ headline(page.post) }} |
page.key |
On a page made from a record in data/, the record’s name. See make a page for each item. |
{{ page.key }} |
page.card, page.og_title |
The picture a link preview shows, with its url, width, height and alt, and what the page is called where it is shared. A page with no card has no page.card. |
{% if page.card is defined %} |
page.webmentions |
What other sites said of the page: said, the replies, mentions and bookmarks, and faces, the likes and reposts, each oldest first. Every page has it, empty where nothing was said. See what a mention gives. |
{% for mention in page.webmentions.said %} |
page.search_box() |
A search box: id, the id its element takes, and first, whether it is the first on the page. |
{% set box = page.search_box() %} |
page.<name> |
Any field of your own. | {{ page.book.author }} |
Note:
page.title has no value on a page with no title:. Where a
template names pages, as a list or a <title> does, write
headline(post): it names every page.
Pictures and files beside the page
| Name | What it gives | Example |
|---|---|---|
page.photo("photo-1.jpg") |
A picture: src, avif, fallback, sizes, full, width, height, alt, caption, name, and each line of the picture’s own .yaml file. It takes sizes, full=true and decorative=true. |
<img src="{{ page.photo("photo-1.jpg").src }}"> |
page.photos(["photo-1.jpg", "photo-2.jpg"]), page.photos() |
Several pictures, each as page.photo gives it: those named, in order, or every photograph beside a page with a folder of its own, by file name. |
{% for picture in page.photos() %} |
page.file("notes-1.pdf") |
Any file: url, name, size in bytes, text, code and language. |
<a href="{{ page.file("notes-1.pdf").url }}"> |
page.video("video-1.mp4") |
A video: src and type. |
<source src="{{ page.video("video-1.mp4").src }}"> |
page.audio("recording-1.m4a") |
A recording: src and type. |
<source src="{{ page.audio("recording-1.m4a").src }}"> |
page.youtube(address), page.vimeo(address) |
A video kept on YouTube or Vimeo, from its address or its id alone: id, start, embed and still. |
{{ page.youtube("abcdefghijk").embed }} |
A file gives url, the address to link to. A picture, a video and a
recording give src, for the element that shows them. A name is found
as the writing finds it, and any page can be asked, as
post.photo(post.cover).
Pictures in templates
and files and videos in a template
say what each field holds.
What a mention gives
| Name | What it gives |
|---|---|
kind |
replied, liked, reposted, bookmarked or mentioned. |
url |
The sender’s page. |
author, author_url |
Who sent it, and their own page. |
published |
When, as a date. |
words |
What they wrote. |
face |
The sender’s picture, as page.photo gives one. |
Each is plain text, never HTML, and each but kind and url may be
missing, so ask first. Show words as they are: markdown or safe
would put another site’s HTML into your page. See
webmentions.
What a template sees on site
| Name | What it gives | Example |
|---|---|---|
site.title, site.language |
As in the settings. On a site in several languages, those of the page being built. | {{ site.title }} |
site.url |
The site’s url, with no / at its end, or the one --url gives for one build. |
{{ site.url }} |
site.host |
The host in the site’s url: example.org for https://example.org/blog/. A site with no url has none. |
{% if site.host == "example.org" %} |
site.today |
The day the site is built on: today in the site’s timezone, or the day --today names. |
{{ site.today | date("2025") }} |
site.languages |
Each language’s code, the main one first. Asked by a code, that language’s home page. A site that names no language has en alone. |
{{ site.languages.en.url }} |
site.posts |
Every post, newest first. | {% for post in site.posts[:5] %} |
site.pages |
Every page the site publishes, in the order of their addresses: a list’s later pages, the 404 page, sitemap.xml and robots.txt among them, but no redirects. |
{% for entry in site.pages %} |
site.tags, site.series |
Every tag and every series, A to Z. | {% for tag in site.tags %} |
site.folders |
Every folder with a page of its own, in the order of their addresses. | {% for folder in site.folders %} |
site.feed |
The address of the site’s own feed, or nothing where none is published. A list’s is list.feed. |
{% if site.feed %} |
site.data.<name> |
A file in data/, by its name. See data files. |
{% for link in site.data.links %} |
site.<name> |
Any value of your own in statac.yaml. |
{{ site.tagline }} |
On a site in several languages, site.posts, site.folders,
site.tags, site.series and site.feed are those of the page’s
language, and site.pages is every language’s: see
each language in templates.
Filters
Jinja’s usual filters are there, such as default, upper, lower,
join, replace, length and safe. Statac adds these:
| Name | What it gives | Example |
|---|---|---|
date |
A date in a style of its own, written by example as date_format is. See how dates look. |
{{ page.date | date("Wed 31 Dec") }} |
moment |
A date as a machine reads it, 2026-10-04T15:04:00+01:00, for <time datetime>. moment("rss") gives the form RSS uses. |
{{ page.date | moment }} |
markdown |
Text read as Markdown, such as a summary with emphasis in it: what it builds. |
{{ page.summary | markdown }} |
highlight |
Text as coloured code, for the inside of a <code>, the language named as a code block names it. The page needs the code stylesheet. |
<pre class="code"><code>{{ page.example | highlight("rust") }}</code></pre> |
plain |
The words of some HTML as one line, its markup taken off. | {{ post.excerpt | plain }} |
truncate |
Text cut at a word to fit a number of characters, with an ending. | {{ page.summary | truncate(160, "…") }} |
digits |
A number, or text with numbers in it, in the digits of the page’s language: ۱۲ for 12 on a Persian page. Only 0 to 9 change. Handed HTML, it gives the words alone, as plain does. |
{{ "Page 3 of 10" | digits }} gives Page ۳ of ۱۰ |
absolute |
An address starting with / made whole, with the start of the site’s url in front. Handed HTML, such as post.content, it does this to each address in an href, src, srcset or poster. Text joined to HTML with ~ is text, so nothing in it is made whole. It needs the site’s url. |
{{ page.url | absolute }} gives https://example.org/posts/post-1/ |
json |
A value written as JSON, safe inside a <script>, and inside an attribute in single quotes, never double. |
data-posts='{{ posts | json }}' |
tagged, in, where |
The pages of a list carrying a tag, filed in a folder, or whose field holds a value. See lists made with posts:. |
{{ site.posts | in("blog") | tagged("Rust") }} |
newest_first |
A list of pages in date order, newest first. | {% for post in related(page) | newest_first %} |
newest_date |
The newest date among a list of pages. A list with no dated page has none. | {{ posts | newest_date | moment }} |
urlencode |
Text made fit to be part of an address. | ?text={{ headline(page) | urlencode }} |
What markdown builds
| In the text | Built |
|---|---|
| Maths | As the writing’s is. |
| A snippet call | Shown as written. |
| A link, a code block, inline code, a callout | By the site’s own piece for it, where it has one. A code block isn’t coloured. |
| A heading, a picture, a footnote | As Statac builds them: a heading has no id, a picture’s address is left as written, and footnotes are numbered from 1 in each value, with the ids note-1 and back-1. |
Warning:
Two values with footnotes on one page, or one beside the page’s own
footnotes, repeat the ids note-1 and back-1, and Statac says
nothing of it.
Functions
| Name | What it gives | Example |
|---|---|---|
css("site.css") |
A stylesheet’s <link>, to a file in assets/ by its own name. See assets. |
{{ css("site.css") }} |
js("app.js") |
A script’s <script>, to a file in assets/. |
{{ js("app.js") }} |
asset("fonts/font-1.woff2") |
The address a file in assets/ is published at, for an element you write yourself. |
href="{{ asset("fonts/font-1.woff2") }}" |
url("/about/") |
An address inside the site, wherever it is published: /blog/about/ on a site at https://example.org/blog/, and /about/ on one at the top of its domain. It takes an address starting with one /, and keeps what follows a ? or a #. A link written without it is left as written. In the writing, the same is @url(/about/). |
<a href="{{ url("/about/") }}"> |
headline(page) |
A page’s name, as text with no markup: see what headline calls a page. |
<title>{{ headline(page) }}</title> |
related(page) |
The posts sharing a tag with the page, those sharing most first. | {% for post in related(page) %} |
feed_id(page) |
Any page’s id in a feed: urn:uuid: and an id made from the site’s feed_key and the page’s address, or its own id:. For a list’s page, its feed’s own id. It needs the site’s feed_key. See entry ids. |
<id>{{ feed_id(post) }}</id> |
webmention() |
The line naming where other sites send the site’s webmentions, for the <head>. Without webmentions: true in the settings it writes nothing. See webmentions. |
{{ webmention() }} |
Each snippet in snippets/ is called the same way, by its file’s name:
{{ card(post) }} places snippets/card.html. See
snippets.
What headline calls a page
headline(page) gives the first there is of:
- its
title:, as written; - for the home page, the site’s
title; - the
# headingits writing opens with, as a reader reads it, its punctuation set and its Markdown gone:# Using `grep`givesUsing grep; - for a page that speaks for a list, such as a folder’s or a tag’s, the list’s name, a tag as it is shown;
- its
summary, or else the words of its opening paragraph, cut at a word to 70 characters with…after; - its date, written as the site writes dates;
- its address.
A list’s later pages are called what its first page is. A page marked
untitled is called by its date before its summary: see
pages without titles.
A template that calls such a page something else asks whether it has a
title: {% if page.title is defined %}.
Where next?
- Snippets: pieces of HTML a template or the writing calls by name
- Lists, tags and series: what every list gives a template, and a list’s pages
- Front matter: the fields Statac reads, and fields of your own
- The starter: the templates a new site has
Something unclear or wrong? Open an issue on GitHub.