Structured Document Templating
Documents are templated via fragments of Liquid (via Fluid) that are stored in hidden sections of the document itself. The document editor can create template sections to customize a specific document, or they will be automatically added based on the document’s path.
Document Template
The base template for the document is stored in a section called _template:body
. The templating engine looks for this section, and passes the document into it.
In its simplest form, it does this:
{{ document | render }}
That template (the render
filter, specifically) iterates every section in the document and calls the render
filter on each one. That filter will then try to find a template for each section based on its name, and process it individually.
The resulting document will therefore be the combined template rendering of each section, in a series. This means that the “document” doesn’t really template – all the templating is delegated to the individual sections. So long as they all template themselves and that output gets glued together, then that’s the document.
You can do something different if you want:
<h1>This is My Document</h1>
<p>
Let's do history first
</p>
{{ document.get_section("history") | render }}
<p>
And now everything else:
</p>
{{ document.get_sections("-history") | render }}
You can ever dig down into the sections:
<h1>This is My Document</h1>
<p>
From: {{ document.get_section("history").meta.start_date }}
</p>
<p>
To: {{ document.get_section("history").meta.end_date }}
</p>
{{ document.get_sections("-history") | render }}
You can do whatever you want in _template:body
– it gets handed the entire document with all its information available.
Section Templates
The document can contain templates for specific section names. When processing a section, the engine will look for the first section called _template:SECTIONNAME
. If it finds one, it will pass that specific section to it (and the entire document, secondarily), and capture the output.
In Fluid:
section
: The section itselfdoc
: The entire document
For example:
[[ _template:movie ]]
<p>
<strong>{{ section.meta.title }}</strong> ({{ section.Meta.year }})
<br/>
{{ section.Content | markdown }}
<p>
<p>
There are {{ doc.FilteredSections.size }} movies in this document.
</p>
This template would be located and applied to any section with the name “movie.”
Remember that the engine will locate the first template with that name. This means that the _template:movie
template can be overridden by another section called _template:movie
higher up in the document.
Example
Consider this document:
title: My Document
This is my document.
[[ author ]]
name: Deane Barker
date: 2022-01-29
[[ _template:body ]]
<h1>{{ doc.Title }}</h1>
{{ document | render }}
[[ _template:author ]]
<p class="byline">
By {{ section.Meta.name }} on {{ section.Meta.date | format_date:"MMMM d, yyyy" }}
<p>
[[ _template:* ]]
{{ section. content | markdown }}
Remember, we added these to the bottom of the document, so they exist in the document itself.
When the templating engine processes the document, it will execute the Liquid code in _template:body
. This will print the document title in an H1
tag, then begin to iterate the sections. (Remember, the title
of the document is just taken from the first section. )
The first section has an implied name of text
(remember, the first section gets the default section name). The engine looks for a section named _template:text
. Not finding one, it falls back to _template:*
(the catchall for everything not otherwise found), and outputs the section content processed by Markdown. (In this case, it will just put P
tags around the content.)
The next section has a name of author
, so the engine finds the section called _template:author
and passes both the section and the document itself to it (you’ll sometimes use the document in template, but not often – it’s just there if you need it). This prints a paragraph of text with the author’s name and the date.
The result would look like this:
<h1>My Document</h1>
<p>This is my document.</p>
<p class="byline">
By Deane Barker on January 29, 2022
</p>
To clarify the template naming:
_template:body
: this is called for the document itself; this is the “master” or “outer” template_template:movie
: this is called for sections with the name “movie” (or whatever else is supplied)_template:*
: this is called for any section for which the engine can’t find a named template (as explained above)
Special Templates
_template:SECTIONNAME:before-series
: This will execute before any series of one or more named sections. So, if you have fourmovie
sections in a row,_template:movie:before-series
will execute, then_template:movie
will execute for each of the movie sections. This template is not bound to an actual section, so thesection
variable will benull
_template:SECTIONNAME:after-series
: As above, but after the series._template:document-first
: Renders once before the first section. Note that this renders within the_template:body
. Any call to therender
filter on section or document will render this before the section if that section is the first in the document. If you never use therender
filter (so, you don’t do declarative templating), then this will never run._template:document-last
: As above, but renders once after the last section.
Reordering Sections
…don’t. Not from templating code, anyway. (ApplyStructure
works from C#.)
There’s no functionality for this. Not because it can’t be done, but just that I haven’t had a need to do it. In general, order the document’s sections in the order you want them to be output.
The KiwiDocument format is not meant to be a database (though you can use it that way, if you want). It’s really just a collection of document pieces with a specified, editorial order.
Technically, the sections are a collection (see How does the system's API support the model?) which turns into an ArrayValue
in Fluid. So they can be ordered in a limited fashion, like this:
{% for section in doc.filteredsections | sort:"name" %}
{{ section | render }}
{% endfor %}
However, there’s no way to order by meta values or anything more sophisticated.
So, again, order the sections in the document in the order they’re supposed to be output. If you need the sections dynamically sorted at runtime, then this might not be the right tool for the job.
The Default _template:body Section
Here is the actual contents of _template:body
on this website (it was simplified above).
<article>
{{ render("content/header") }}
{% if doc.start != nil %}
{{ doc.start | render }}
{% endif %}
{{ doc.filteredsections | render }}
{% if doc.end != nil %}
{{ doc.end | render }}
{% endif %}
</article>
The header
include is a fairly complex template that builds the top of the document (the title, subtitle, author, date, and tags, if they exist).
The next section renders the section called _start
(if found) using the template _template:start
(if found).
Then the all the sections in the document are iterated and processed by their template if found, and by the catchall _template:*
if not.
The next section renders the section called _end
(if found) using the template _template:end
(if found).
The Catchall Template
If not template is found for a section, then it’s assumed to be general text with no meta, and is rendered by a template called _template:*
.
{{ section.content | markdown }}