Themes

Writing a theme

6 min read

Layout #

themes/mytheme/
├── theme.yml
├── templates/
│   ├── base.html
│   ├── index.html        home page
│   ├── page.html         a content page
│   ├── section.html      a section listing
│   ├── taxonomy.html     all terms of a taxonomy      (only if you use taxonomies)
│   ├── term.html         pages for one term           (only if you use taxonomies)
│   ├── 404.html          optional
│   └── shortcodes/       one file per shortcode
└── static/               copied to the site root

Link the generated code stylesheet from your base.html, next to your own:

<link rel="stylesheet" href="{{ url(path='highlight.css') }}">

Meteorite always generates highlight.css when highlighting is on. Without the link, code blocks are plain monospace text.

Templates use Tera (Jinja-like). Output is HTML-escaped by default; use | safe for page.content, page.summary and shortcode body.

theme.yml #

name: mytheme            # letters, digits, - and _
version: 1.0.0
description: What it looks like
author: You
meteorite_api: 1         # template context version, see below
params:
  accent:
    default: "#5b4de0"
    description: Link colour
  tagline:
    required: true       # the build fails with a clear message if unset

meteorite_api is the version of the template context and file conventions the theme was written for. Meteorite refuses to load a theme that targets a different version, so an incompatible theme fails up front instead of rendering wrongly.

Template context (API v1) #

Available in every template:

NameTypeNotes
configobjectThe merged site config. config.params includes theme defaults.
dataobjectContents of data/*.yml, keyed by file name.
meteorite.versionstring
themeobjectThe active theme’s manifest: name, version, skins, params.
chat_widget()functionRenders the optional chat launcher and loader, or nothing when chat is off. Call it once before </body> as {{ chat_widget() | safe }}. See Add a chat assistant.
pageobjectThe current page (always set; synthetic for listings).

Functions:

FunctionReturns
url(path="/blog/")The path joined to base_url. Always use this for links so sub-path hosting works.
get_pages(section="", limit=N, recursive=true)Pages (no body), newest first.
get_section(path="blog")A section object including its pages.
resize_image(path="a.jpg", width=400)URL of a resized copy (needs --features images).
identicon(seed="Ada Lovelace")Inline SVG default avatar, a symmetric 5x5 pattern derived from the seed. Add | safe. The background has the class identicon-bg.

page #

FieldNotes
metaFront matter: title, description, tags, weight, plus any custom keys.
url, permalinkRoot-relative URL and absolute URL.
contentRendered HTML body.
summaryHTML before <!-- more -->, or the first paragraph.
dateYYYY-MM-DD or null.
word_count, reading_timeIntegers.
tocList of {level, id, title}.
sectionPath of the containing section.
prev, next{title, url} or null, following the section’s order.

Per template #

TemplateExtra variables
page.htmlsection = {path, url, title} of the parent section
index.html, section.htmlsection (below), paginator, page = the _index.md page
taxonomy.htmltaxonomy = {name, url}, terms = [{name, slug, url, count}]
term.htmltaxonomy, term = {name, slug, url, count}, pages
404.htmlnothing extra

section: path, url, permalink, title, description, meta, content, toc, subsections ({path, url, title, description}), pages (this page of the listing, bodies omitted) and total_pages.

paginator: number, total_pages, per_page, total_items, current_url, first_url, last_url, prev_url and next_url (null at the ends). It is always present; a section without pagination has total_pages == 1.

Shortcodes #

templates/shortcodes/note.html is invoked as {{< note kind="warn" >}}...{{< /note >}}. Arguments arrive as top-level variables and the Markdown body, already rendered, as body.

Listings that carry the whole post #

Listings leave out each page’s body to stay fast. A section whose _index.md sets full_content: true is the exception: its pages carry content in section.pages, get_pages() and term pages, so a theme can show whole posts in a stream (a micro-blog). It is off by default; turn it on only for sections of short pages.

Skins #

A theme can offer alternative looks that share its markup. Declare them in theme.yml:

skins:
  - { name: sunrise, label: Sunrise, description: "Warm oranges" }
  - { name: slate, label: Slate }
params:
  skin: { default: sunrise }
  skin_switcher: { default: true }

Meteorite makes the list available to templates as theme.skins (name, label, description), and the manifest itself as theme.name, theme.version. The theme is then responsible for three things, all shown in the bundled themes:

  1. Put data-skin="{{ config.params.skin }}" on <html>.
  2. Style each skin with :root[data-skin="sunrise"] { ... }, overriding the same CSS variables you use for light and dark. Skin rules must come after the mode rules in the stylesheet.
  3. Optionally render a switcher that sets data-skin and stores the choice in localStorage (app.js in the bundled themes does this for any element with data-skin-set="<name>").

Skin names are lowercase letters, digits, - and _, and must be unique.

Light and dark mode #

Both bundled themes are good starting points. The pattern they use:

  1. Define colours as CSS variables on :root for light.
  2. Override them for dark under @media (prefers-color-scheme: dark), guarded with :root:not([data-theme="light"]), and again under :root[data-theme="dark"].
  3. Set data-theme on <html> from JavaScript when the visitor presses a toggle, and store the choice in localStorage. Read it from a tiny inline script in <head> so there is no flash.
  4. Let the site pin a starting mode with a color_scheme parameter (auto, light, dark).

highlight.css follows the same data-theme convention, so code blocks switch with your page.

Tips #

  • Link with url(path=...), never a hard-coded /.
  • For the home page’s latest posts use get_pages(limit=5); don’t loop over everything.
  • Keep a static/ stylesheet name unique to your theme to avoid clashing with a site’s files.
  • Test with meteorite check: it catches links your templates build wrongly.