#+TITLE: Templates #+DESCRIPTION: Layouts, inheritance, every variable and filter available to a template. #+LEDE: minijinja layouts loaded from disk, hashed into the cache, replaceable entirely. Templates live in the directory named by =[templates] dir=, default =templates/=. They are [[https://docs.rs/minijinja][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. #+BEGIN_SRC html {{ page.title }} — {{ site.title }} {% if stylesheet %}{% endif %}

{{ page.title }}

{{ body | safe }}
#+END_SRC 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: #+BEGIN_SRC toml [[pages]] match = "blog" template = "post.html" #+END_SRC #+BEGIN_SRC org ,#+TEMPLATE: wide.html #+END_SRC 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: #+BEGIN_SRC html {% extends "base.html" %} {% block content %} {{ body | safe }}

Reply by email →

{% endblock %} #+END_SRC Full rules in [[file:02-configuration.org][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. #+BEGIN_SRC html {% extends "base.html" %} {% block content %}

Only this part differs.

{% endblock %} #+END_SRC 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=: #+BEGIN_SRC html {% extends "base.html" %} {% block content %} {{ body | safe }} {% include "reply.html" %} {% endblock %} #+END_SRC 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: #+BEGIN_SRC html {{ post.title }} #+END_SRC ** 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 [[file:02-configuration.org][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: #+BEGIN_SRC html {% if theme %}{% endif %} {% if stylesheet %}{% endif %} #+END_SRC 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 [[file:03-collections.org][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=: #+BEGIN_SRC html {% macro toc_list(entries) %} {% endmacro %} {% if page.toc | length > 1 %}{{ toc_list(page.toc) }}{% endif %} #+END_SRC =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 #+BEGIN_SRC html {{ post.url | absolute }} #+END_SRC 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.