Templates¶
A template element generates child elements from a
Jinja2 template instead of
hand-writing them. Two motivating cases:
- Repetition in place — a 4×3 thumbnail grid is one loop, not twelve hand-positioned elements.
- Reuse with parameters — a banner/title/subtitle scaffold lives in one file and is instantiated wherever needed, varying only a few declared parameters.
Plain .slide.json and .deck.json files are never
Jinja-processed — templating lives only in places that announce it.
Inline templates¶
For short, one-off generation, put the Jinja text directly in the
element's template field. It must render to a JSON5 array of
elements:
{
template: "[ {% for i in range(4) %} { image: \"thumbs/t{{ i }}.png\", position: [{{ 5 + i * 24 }}, 10], width: 20, height: \"auto\" }, {% endfor %} ]",
}
JSON5's tolerance of trailing commas makes loop-generated arrays painless. Inline templates are one JSON5 string, so anything long or reused belongs in a template file instead.
Template files¶
A reusable template lives in a *.elements.json.j2 file and may open
with a front-matter block — a JSON5 object between two --- lines
— declaring its parameter contract. Note the syntax: unlike the
----delimited front-matter convention elsewhere, this block is
JSON5, not YAML (one syntax across the whole format); writing YAML
here is caught with a pointed error:
---
{
description: "Colored banner with title and optional subtitle.",
params: {
title: { type: "string", required: true },
subtitle: { type: "string", default: "" },
accent: { type: "string", default: "#5EB083" },
},
}
---
[
{ name: "banner", html: "<div style=\"width:100%;height:100%;background:{{ accent }}\"></div>",
position: [0, 0], width: 100, height: 100 },
{ name: "title", markdown: "## {{ title }}", position: [3, 33], anchor: [0, 50],
width: 80, color: "#ffffff" },
{% if subtitle %}
{ name: "subtitle", markdown: "### {{ subtitle }}", position: [3, 66], anchor: [0, 50],
width: 80, color: "#ffffff" },
{% endif %}
]
Instantiate it with template_file + with:
{
name: "header",
template_file: "../templates/section_header.elements.json.j2",
with: { title: "Building the stack", subtitle: "Details (1/2)" },
position: [0, 5], width: 100, height: 15,
}
scrolly schema template <path> prints a template's contract; the
with: block is validated against it before rendering — missing
or misspelled params are precise errors, not mysteries in rendered
text.
Slide factories¶
Templating also works at whole-slide granularity. A factory is a
*.slide.json.j2 template rendering a complete slide object (title,
scroll behavior, elements); a template-slide stub is a regular
.slide.json file whose top level holds just the instantiation:
// slides/detail-1.slide.json — the whole file
{
template_file: "../factories/chapter.slide.json.j2",
with: { heading: "Building the stack", part: "1/2" },
}
The deck file never changes — it keeps pointing at .slide.json
sources and stays a pure navigation manifest. Layout lives once in the
factory; each stub holds only what varies. Front-matter params,
validation, and scrolly expand work exactly as for element
templates. A stub may contain nothing besides template_file and
with — every slide field comes from the factory. Suffixes declare
the render target: factories end in .slide.json.j2, element
templates in .elements.json.j2; mixing them up is a build error.
File references in a factory's output (asset paths, *_file fields)
resolve against the factory's directory.
How an instantiation behaves¶
An expanded template becomes a container
holding the rendered children: the element's box re-coordinates the
children (defaults: the full slide), its substrate fields animate the
group as a unit, and a name dot-prefixes the children's names so the
same template can be instantiated twice on one slide.
Variables and parameters¶
A template's variables come from its with: block plus front-matter
defaults — nothing else. Values can be any JSON value, including
nested arrays and objects; Jinja iterates them naturally
({% for team in teams %} … {{ team.name }}). Declared type:s check
the top level only (string, number, integer, boolean, array,
object); structure inside array / object values is the caller's
responsibility — a mismatch surfaces as a strict-undefined render
error naming the template.
Debugging¶
Rendering is strict and the errors are numbered: Jinja syntax (E801,
which also reminds you of {% raw %}…{% endraw %} for literal {{),
undefined variables (E802), invalid rendered output (E803, with an
excerpt), param mismatches (E804), other render failures (E805).
When the rendered text itself is the question, expand it:
scrolly expand deck.deck.json --slide detail-1
This renders each template instantiation on the slide with its actual
with: values and prints the post-Jinja, pre-validation text. There
is no hypothetical-render mode: to explore a template, write a small
example slide and expand it.
Paths¶
template_file paths resolve relative to the file they appear in;
{% include %} / {% import %} inside a template file resolve within
that file's directory tree. Asset and *_file references inside the
rendered output resolve relative to the template file (inline
templates: the slide file). Everything stays subject to the deck-root
confinement rule (see Working with assets).