Data contract · part 4
theme.yaml
The fourth part of the data contract: theme.yaml, the theme's passport. What the theme can draw, what it expects from the site and how it links pages together.
This page is for designers and theme developers. Someone who only writes texts does not need to read it.
In plain words
If the content is the text of a book, the theme is its layout: fonts, colours, where the pictures go. theme.yaml is the theme's ID card. It lists which kinds of pages the theme can draw (a home page, a service page, a blog post) and which sections each kind expects.
This split is what lets you change the design without rewriting a single page: a new theme reads the same files and draws them its own way.
Example. A theme that knows two kinds of pages:
name: my-theme
types:
home:
uses: [intro, "services:checklist", "faq:faq"]
service:
uses: [intro, "what:list", "prices:table", "faq:faq"]
A page of type service with sections what, prices and faq gets exactly the blocks the designer made for them. A section the theme does not know is reported by the build, so a typo in an anchor never disappears silently.
When you need this page. When you make or change a theme. If you only write texts, you can skip it.
The theme passport
theme.yaml serves as documentation and as the basis of the heron check report, not as a strict validator.
name: main
version: "1.0"
title: Site theme
# what the theme expects from site.yaml. HERON checks presence without going into meaning
requires:
- contact.phone
- organization.name
# forms the theme draws. Their description must be in site.yaml
forms: [callback]
modules:
- breadcrumbs
- prose
- facts
- list
- alert-list
- steps
- timeline
- faq
- cards
- quote
- table
- gallery
- author
types:
home:
uses: [intro, about, services, faq]
jsonld: [WebSite]
category:
uses: [intro, outro]
jsonld: [CollectionPage]
service:
uses: [intro, quick-facts, what, who, how, faq]
jsonld: [Service]
article:
uses: [intro]
jsonld: [Article]
page:
uses: [intro]
# links between pages. HERON finds them, checks them and builds the reverse link
# without knowing what the relation means
links:
- field: related
type: service
back: mentioned_in
- field: topic # field in the front matter of the source page
type: topic # type of the pages it points to (optional)
on: service # pages of which type should have the field
required: 1 # minimum number of links
# ratios and widths HERON fits the masters into
images:
widths: [400, 800, 1200, 1600]
ratios: ["1:1", "16:9", "4:3"] # what templates may ask for; only what is asked for gets cut
formats: [avif, webp]
fallback_width: 800 # fallback PNG/JPEG for a plain image: one width
A page type is a template
A page type is implemented by a templates/TYPE.html file, an ordinary Jinja2 template where the order of blocks reads top to bottom:
{% extends "base.html" %}
{% block content %}
{{ mod.breadcrumbs() }}
{{ mod.hero() }}
{{ mod.facts('quick-facts') }}
{{ mod.prose('what') }}
{{ mod.prose('who') }}
{{ mod.list('contra') }}
{{ mod.steps('how') }}
{{ mod.timeline('recovery') }}
{{ mod.prose('price') }}
{{ mod.faq('faq') }}
{% for p in page.related.get('related', []) %}
{{ p.h1 }} → {{ p.url }} {# a link to the related page #}
{% endfor %}
{{ mod.call() }}
{{ mod.author() }}
{% endblock %}
mod.prose('what') takes the what section; if there is none, it outputs nothing — no heading, no empty frame. No conditions around each module are needed.
How sections get their form and what reaches a module is in section forms; how it looks on the site is in the forms reference.
Links are not sections
mod.MODULE('id') looks for a section with that anchor in the page text. Linked pages are not in the text: HERON collects them from link fields into page.related.FIELD (and reverse ones into page.related.BACK). The theme loops over this list itself, as in the example above, or calls its own module without an argument that reads page.related. Calling mod.cards('related') outputs nothing: the page has no related section.
Reverse links
back: NAME: HERON builds a reverse list. If a.md points to b with the related field, b gets page.related.NAME with page a. The theme draws it the same way as a direct one.
- links within one type are allowed: a service points to services (
field: related, type: service, back: related_from), and the reverse list is built as usual; - a page linking to itself is skipped with a warning in the "links" group. It goes into neither the direct nor the reverse list;
- one reverse name across several declarations (for example,
topicfrom services and from articles with the sameback: mentioned_in) gives one mixed list: pages of all types together, without duplicates; - the order of both direct and reverse lists is by
order, then byh1, ignoring case. The order in which links are written in front matter is not kept: the target pages'ordersets it; - filtering by type is up to the theme: a list element has a
type, like any page:{% for p in page.related.mentioned_in if p.type == 'article' %}; ondoes not affect building links: a link is built for any page where the field is filled.ononly says for which page type an empty field is a reason for a warning (below).
Required links: required
required: N: the theme expects at least N links in this field. It is a warning, not an error: unfinished internal linking is no reason not to publish a site, but you need to see it. It goes to the "links" group of the build report and heron check:
- the field is filled but has fewer than
Nlinks: a warning on any page that has the field: "related: 0 links, the theme expects at least 1"; - the field is empty or missing: a warning only on pages of the
ontype: "field 'topic' is not filled, the theme expects at least 1". Withouton, HERON does not know which pages should have the field and says nothing about empty ones; - a link to a page that does not exist is still error
E006, regardless ofrequired.
The on key is written without quotes: YAML reads a bare on as "true", and HERON gives it its name back itself.
Links with captions
A link field takes two forms, a list of names and a list of maps; you can mix them in one list:
services:
- slug: express-review
note: The main way when time is short
- full-review
In links a page is named by its file name without extension, not by its address. The map key is historically called slug, but its value is that same file name.
An address does not fit: it is set by the slug field and differs between languages, so linking by address would mean an English page and its Russian translation are linked to different things, although a link describes a subject, not an address. Renaming an address does not affect links at all.
The name is looked up within its own language. Two files with the same name in different folders of one language is error E006 about ambiguity: HERON does not guess which one was meant.
If no caption is set, HERON uses the target page's promise, and if there is none, its description. A name with no matching page is a build error, as before.
Source
This page mirrors section 6 of docs/spec/20-data-contract.md on the prod branch. The repository is updated first: open the file on GitHub.