docs/guide/04-templates.org
240 lines · 8445 bytes
Templates
- base.html replaces the layout
- Pages can render through a different layout
- Names are full filenames
- Variables
- Filters
- Escaping
- Templates are a cache input
Templates live in the directory named by [templates] dir, default templates/. They
are minijinja templates — Jinja2 syntax — loaded at build
time, so editing one and rebuilding is the whole edit cycle.
base.html replaces the layout
A file called base.html becomes the page layout. Without one, a built-in layout is used
— which is what makes a bare directory of org files build into a real site.
<!DOCTYPE html>
<html lang="{{ site.language }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page.title }} — {{ site.title }}</title>
<link rel="stylesheet" href="{{ root }}style.css">
{% if stylesheet %}<link rel="stylesheet" href="{{ stylesheet }}">{% endif %}
</head>
<body>
<nav>{% for item in nav %}<a href="{{ item.url }}">{{ item.title }}</a>{% endfor %}</nav>
<main>
<h1>{{ page.title }}</h1>
{{ body | safe }}
</main>
</body>
</html>
A template that does not compile is a build error, not a fallback to the default — someone editing a layout should see the mistake, not output that looks like their edit did nothing.
Pages can render through a different layout
base.html is the default, not the only option. A [[pages]] rule gives a section its
own layout, and #+TEMPLATE: gives one page its own:
[[pages]]
match = "blog"
template = "post.html"
#+TEMPLATE: wide.html
The page's own keyword wins over any rule, and the most specific rule wins over a broader one. A second layout almost always wants the first one's chrome, so it extends it:
{% extends "base.html" %}
{% block content %}
{{ body | safe }}
<p><a href="mailto:you@example.com">Reply by email →</a></p>
{% endblock %}
Full rules in Configuration.
Names are full filenames
Templates are registered under their full relative filename: base.html,
partials/head.html, feed.xml. That is what {% extends "base.html" %} names, and it
is why a template can have any extension — which is how an RSS feed is just a listing
page.
{% extends "base.html" %}
{% block content %}<p>Only this part differs.</p>{% endblock %}
Subdirectories work, so {% include "partials/header.html" %} does what you expect.
Partials keep a snippet out of a layout
A block of content that is not really layout — an invitation to reply, a licence line, a
donation ask — is better as its own file than as a line inside base.html:
{% extends "base.html" %}
{% block content %}
{{ body | safe }}
{% include "reply.html" %}
{% endblock %}
Emptying reply.html removes it everywhere; editing it re-renders exactly the pages that
include it, because the render key follows includes. And because the page chooses its
layout, an individual page opts in with #+TEMPLATE: post.html or out with
#+TEMPLATE: base.html, without a rule deciding for a whole directory.
Variables
body
The rendered page HTML. Always use {{ body | safe }} — it is already HTML, and
escaping it would print tags at the reader.
Empty on generated pages, which build their content from pages or groups instead.
page
| Field | Meaning |
|---|---|
title |
#+TITLE:, or the filename stem. |
url |
Output path relative to the site root, e.g. blog/post.html. |
source |
Source path relative to the source root, e.g. blog/post.org. |
date |
#+DATE: verbatim, in whatever org syntax was written. |
date_iso |
The YYYY-MM-DD inside it, or none. |
year |
The year from that date, for grouping a listing. |
tags |
#+FILETAGS:, split. |
excerpt |
#+DESCRIPTION:, or the first paragraph. |
word_count |
Words of prose, excluding code blocks. |
reading_time |
Minutes at 200 wpm, rounded up. |
toc |
The heading tree. See below. |
keywords |
Every #+KEYWORD:, by lowercased name. |
page.keywords is the escape hatch: #+CUSTOM_THING: x is
{{ page.keywords.custom_thing }}, so your own metadata works without orgo knowing it
exists.
site
site.title, site.base_url, site.description, site.language — straight from
[site] in the config.
nav
A list of {title, url}, with URLs relative to the current page.
root
The ../ prefix back to the site root from this page: empty at the top level, ../ one
level down. Prefix it to any site-root-relative path so the same template works at any
depth:
<link rel="stylesheet" href="{{ root }}style.css">
<a href="{{ root }}{{ post.url }}">{{ post.title }}</a>
stylesheet
URL of the generated syntax.css, relative to this page. Link it or code blocks are
unstyled.
theme
URL of theme.css, relative to this page — the built-in theme named by
site.theme. Empty when there is none, which is the default, so guard it and link it
before stylesheet or the theme's code colours would override the highlighter's:
{% if theme %}<link rel="stylesheet" href="{{ theme }}">{% endif %}
{% if stylesheet %}<link rel="stylesheet" href="{{ stylesheet }}">{% endif %}
A layout that ignores it is a layout with its own CSS, which is the point at which you have outgrown the setting.
pages, group, groups, paginator
Present on generated pages; see Collections. pages is also
available on every page when [templates] expose_page_list = true.
page.toc
The page's headings as a tree — a table of contents is one, and rebuilding a tree from a flat list of levels inside a template is what Jinja is worst at.
Each entry has title, anchor, level, number and children:
{% macro toc_list(entries) %}
<ul>{% for e in entries %}
<li><a href="#{{ e.anchor }}">{{ e.number }} {{ e.title }}</a>
{%- if e.children %}{{ toc_list(e.children) }}{% endif %}</li>
{% endfor %}</ul>
{% endmacro %}
{% if page.toc | length > 1 %}{{ toc_list(page.toc) }}{% endif %}
number is the section number — 1., 3.1. — always computed and printed only if you
ask for it. Print it when [html] section_numbers is on, or the contents will number what
the headings do not.
Anchors come from the same function that emits heading id attributes, so a TOC link
cannot drift from the heading it points at. The tree is empty when the page has no
headings, when [html] toc = false, or when the document says #+OPTIONS: toc:nil.
Filters
Beyond minijinja's built-ins:
| Filter | Does |
|---|---|
absolute |
Site-root-relative path → absolute URL, using site.base_url. |
rfc822 |
Any org or ISO date → the format RSS pubDate requires. |
truncate(n) |
Shorten to at most n characters on a word boundary, with an ellipsis. |
absolute
<link>{{ post.url | absolute }}</link>
Apply it to the site-root-relative values — page.url, pages[].url, group.url —
and not to nav[].url, paginator.*_url, stylesheet or root, which are relative to
the page carrying them and already correct there.
An already-absolute URL passes through, so a template can apply it uniformly to internal
paths and external links. With no base_url it is an error naming the setting, rather
than a relative URL that would make a feed invalid.
truncate
minijinja ships no truncate, and an excerpt is usually a whole paragraph — so without one a listing's only options are the full paragraph or nothing.
Escaping
Output is HTML-escaped by default, because titles and text are author content.
{{ body | safe }} is the deliberate exception.
Unlike stock minijinja, / is not escaped. Escaping it is a defence for values
interpolated into JavaScript, and since < is escaped anyway it buys nothing in an HTML
document — while making every URL read ../index.html. Templates emit a lot of
URLs.
Templates are a cache input
Every template's source is hashed, so editing a layout re-renders the pages that use it. A design change never leaves a site half-updated.