krz/orgo
Lightning fast org-mode static site generator.
clone: git clone https://gitbay.org/krz/orgo.git
v0.21.0: docs/guide/04-templates.org · raw
1#+TITLE: Templates
2#+DESCRIPTION: Layouts, inheritance, every variable and filter available to a template.
3#+LEDE: minijinja layouts loaded from disk, hashed into the cache, replaceable entirely.
4
5Templates live in the directory named by =[templates] dir=, default =templates/=. They
6are [[https://docs.rs/minijinja][minijinja]] templates — Jinja2 syntax — loaded at build
7time, so editing one and rebuilding is the whole edit cycle.
8
9* base.html replaces the layout
10
11A file called =base.html= becomes the page layout. Without one, a built-in layout is used
12— which is what makes a bare directory of org files build into a real site.
13
14#+BEGIN_SRC html
15<!DOCTYPE html>
16<html lang="{{ site.language }}">
17<head>
18 <meta charset="utf-8">
19 <meta name="viewport" content="width=device-width, initial-scale=1">
20 <title>{{ page.title }} — {{ site.title }}</title>
21 <link rel="stylesheet" href="{{ root }}style.css">
22 {% if stylesheet %}<link rel="stylesheet" href="{{ stylesheet }}">{% endif %}
23</head>
24<body>
25 <nav>{% for item in nav %}<a href="{{ item.url }}">{{ item.title }}</a>{% endfor %}</nav>
26 <main>
27 <h1>{{ page.title }}</h1>
28 {{ body | safe }}
29 </main>
30</body>
31</html>
32#+END_SRC
33
34A template that does not compile is a *build error*, not a fallback to the default —
35someone editing a layout should see the mistake, not output that looks like their edit
36did nothing.
37
38* Pages can render through a different layout
39
40=base.html= is the default, not the only option. A =[[pages]]= rule gives a section its
41own layout, and =#+TEMPLATE:= gives one page its own:
42
43#+BEGIN_SRC toml
44[[pages]]
45match = "blog"
46template = "post.html"
47#+END_SRC
48
49#+BEGIN_SRC org
50,#+TEMPLATE: wide.html
51#+END_SRC
52
53The page's own keyword wins over any rule, and the most specific rule wins over a broader
54one. A second layout almost always wants the first one's chrome, so it extends it:
55
56#+BEGIN_SRC html
57{% extends "base.html" %}
58{% block content %}
59{{ body | safe }}
60<p><a href="mailto:you@example.com">Reply by email →</a></p>
61{% endblock %}
62#+END_SRC
63
64Full rules in [[file:02-configuration.org][Configuration]].
65
66* Names are full filenames
67
68Templates are registered under their full relative filename: =base.html=,
69=partials/head.html=, =feed.xml=. That is what ={% extends "base.html" %}= names, and it
70is why a template can have any extension — which is how an RSS feed is just a listing
71page.
72
73#+BEGIN_SRC html
74{% extends "base.html" %}
75{% block content %}<p>Only this part differs.</p>{% endblock %}
76#+END_SRC
77
78Subdirectories work, so ={% include "partials/header.html" %}= does what you expect.
79
80** Partials keep a snippet out of a layout
81
82A block of content that is not really layout — an invitation to reply, a licence line, a
83donation ask — is better as its own file than as a line inside =base.html=:
84
85#+BEGIN_SRC html
86{% extends "base.html" %}
87{% block content %}
88{{ body | safe }}
89{% include "reply.html" %}
90{% endblock %}
91#+END_SRC
92
93Emptying =reply.html= removes it everywhere; editing it re-renders exactly the pages that
94include it, because the render key follows includes. And because the *page* chooses its
95layout, an individual page opts in with =#+TEMPLATE: post.html= or out with
96=#+TEMPLATE: base.html=, without a rule deciding for a whole directory.
97
98* Variables
99
100** body
101
102The rendered page HTML. Always use ={{ body | safe }}= — it is already HTML, and
103escaping it would print tags at the reader.
104
105Empty on generated pages, which build their content from =pages= or =groups= instead.
106
107** page
108
109| Field | Meaning |
110|-------+---------|
111| =title= | =#+TITLE:=, or the filename stem. |
112| =url= | Output path relative to the site root, e.g. =blog/post.html=. |
113| =source= | Source path relative to the source root, e.g. =blog/post.org=. |
114| =date= | =#+DATE:= verbatim, in whatever org syntax was written. |
115| =date_iso= | The =YYYY-MM-DD= inside it, or =none=. |
116| =year= | The year from that date, for grouping a listing. |
117| =tags= | =#+FILETAGS:=, split. |
118| =excerpt= | =#+DESCRIPTION:=, or the first paragraph. |
119| =word_count= | Words of prose, excluding code blocks. |
120| =reading_time= | Minutes at 200 wpm, rounded up. |
121| =toc= | The heading tree. See below. |
122| =keywords= | *Every* =#+KEYWORD:=, by lowercased name. |
123
124=page.keywords= is the escape hatch: =#+CUSTOM_THING: x= is
125={{ page.keywords.custom_thing }}=, so your own metadata works without orgo knowing it
126exists.
127
128** site
129
130=site.title=, =site.base_url=, =site.description=, =site.language= — straight from
131=[site]= in the config.
132
133** nav
134
135A list of ={title, url}=, with URLs relative to the current page.
136
137** root
138
139The =../= prefix back to the site root from this page: empty at the top level, =../= one
140level down. Prefix it to any site-root-relative path so the same template works at any
141depth:
142
143#+BEGIN_SRC html
144<link rel="stylesheet" href="{{ root }}style.css">
145<a href="{{ root }}{{ post.url }}">{{ post.title }}</a>
146#+END_SRC
147
148** stylesheet
149
150URL of the generated =syntax.css=, relative to this page. Link it or code blocks are
151unstyled.
152
153** theme
154
155URL of =theme.css=, relative to this page — the [[file:02-configuration.org][built-in theme]] named by
156=site.theme=. Empty when there is none, which is the default, so guard it and link it
157*before* =stylesheet= or the theme's code colours would override the highlighter's:
158
159#+BEGIN_SRC html
160{% if theme %}<link rel="stylesheet" href="{{ theme }}">{% endif %}
161{% if stylesheet %}<link rel="stylesheet" href="{{ stylesheet }}">{% endif %}
162#+END_SRC
163
164A layout that ignores it is a layout with its own CSS, which is the point at which you
165have outgrown the setting.
166
167** pages, group, groups, paginator
168
169Present on generated pages; see [[file:03-collections.org][Collections]]. =pages= is also
170available on every page when =[templates] expose_page_list = true=.
171
172** page.toc
173
174The page's headings as a *tree* — a table of contents is one, and rebuilding a tree from
175a flat list of levels inside a template is what Jinja is worst at.
176
177Each entry has =title=, =anchor=, =level=, =number= and =children=:
178
179#+BEGIN_SRC html
180{% macro toc_list(entries) %}
181<ul>{% for e in entries %}
182 <li><a href="#{{ e.anchor }}">{{ e.number }} {{ e.title }}</a>
183 {%- if e.children %}{{ toc_list(e.children) }}{% endif %}</li>
184{% endfor %}</ul>
185{% endmacro %}
186
187{% if page.toc | length > 1 %}{{ toc_list(page.toc) }}{% endif %}
188#+END_SRC
189
190=number= is the section number — =1.=, =3.1.= — always computed and printed only if you
191ask for it. Print it when =[html] section_numbers= is on, or the contents will number what
192the headings do not.
193
194Anchors come from the same function that emits heading =id= attributes, so a TOC link
195cannot drift from the heading it points at. The tree is empty when the page has no
196headings, when =[html] toc = false=, or when the document says =#+OPTIONS: toc:nil=.
197
198* Filters
199
200Beyond minijinja's built-ins:
201
202| Filter | Does |
203|--------+------|
204| =absolute= | Site-root-relative path → absolute URL, using =site.base_url=. |
205| =rfc822= | Any org or ISO date → the format RSS =pubDate= requires. |
206| =truncate(n)= | Shorten to at most /n/ characters on a word boundary, with an ellipsis. |
207
208** absolute
209
210#+BEGIN_SRC html
211<link>{{ post.url | absolute }}</link>
212#+END_SRC
213
214Apply it to the *site-root-relative* values — =page.url=, =pages[].url=, =group.url= —
215and not to =nav[].url=, =paginator.*_url=, =stylesheet= or =root=, which are relative to
216the page carrying them and already correct there.
217
218An already-absolute URL passes through, so a template can apply it uniformly to internal
219paths and external links. With no =base_url= it is an *error* naming the setting, rather
220than a relative URL that would make a feed invalid.
221
222** truncate
223
224minijinja ships no truncate, and an excerpt is usually a whole paragraph — so without one
225a listing's only options are the full paragraph or nothing.
226
227* Escaping
228
229Output is HTML-escaped by default, because titles and text are author content.
230={{ body | safe }}= is the deliberate exception.
231
232Unlike stock minijinja, =/= is *not* escaped. Escaping it is a defence for values
233interpolated into JavaScript, and since =<= is escaped anyway it buys nothing in an HTML
234document — while making every URL read =../index.html=. Templates emit a lot of
235URLs.
236
237* Templates are a cache input
238
239Every template's source is hashed, so editing a layout re-renders the pages that use it.
240A design change never leaves a site half-updated.