Generated from
cat-harness/skills/ui/ui-core/liquid-templates.md— do not edit here.
Liquid templates — layout only, every fact from the graph
Two engines run Liquid here, and they produce different kinds of output. Know which one you are writing for before you write a line:
| README templates | site templates | |
|---|---|---|
| engine | liquidjs, run by bootstrap-tools’ subgraph-readmes (jekyllInclude: true, strictFilters: true) | Jekyll, run by docs-site.yml with the pinned just-the-docs remote_theme |
| lives in | templates/readme/ beside the writer, in the tools repository (bootstrap-tools/scripts/templates/readme/) | cat-harness/docs/_includes/ — just-the-docs’ extension points (head_custom.html, footer_custom.html, nav_footer_custom.html) plus our own |
| output | <directory>/README.md, committed, read on GitHub where no Liquid runs | _site/, never committed, published to Pages |
| data | only what the generator passes: subgraph, instance, kg, files, subdirs, summary | site, page, include, and _data/*.json written by the pipeline |
| verified by | bun run cat readme:subgraphs:check — the diff is the output | building the site: rendered-verification, bun run cat preview:site |
Where a template lives
Beside the writer that renders it, in the tools repository. The README
templates are part of bootstrap-tools’ scripts/ subgraph, at
bootstrap-tools/scripts/templates/readme/, and the writer finds them with
TEMPLATES, a path relative to its own file. They moved there with the
writer (bean xsqm, owner 2026-09-29: one copy of the README writers, in the
tools repository): a template is part of the toolset, and somebody who wants
different READMEs uses a different toolset over the same content. Until then
they sat in cat-harness’s tools/ directory, found through its id, and a
harness that wants its own layout still passes its own templates directory to
plan(). They are files OF the scripts/ subgraph, not a directory of their
own: a declared directory nested inside another is refused by
check:layout-norms. Never an inline template string in a .ts file and
never a .liquid file outside a declared directory: a template nobody
declared is one no checker looks at, and its README row cannot say what it is.
What a template may read — and what it must not compose
A template arranges facts; it never states one. Every word that is not
layout comes from a variable the generator read from the Knowledge Graph: the
directory’s declared title and description, each file’s own account of
itself, a relation a diagram records. No count, URL, name or claim typed into
a template — each is a second answer free to drift from the first, which is
readme-sections §”Resolve links; never compose them”
one level down.
- Computation belongs in the generator, where it is tested: counting,
grouping, choosing a link target, collapsing a long list. The template sees
results (
subdirs[].count,summary), not inputs to derive them from. - A missing value is shown as missing, never papered over:
{{ subgraph.title | default: subgraph.id }}and “No description is declared for …” are the pattern. The generator records the gap as a QA finding intest/results/subgraph-readmes.qa-results.json(a derived result, stored on theqa-reportsbranch byqa-publish); the template must not make it look filled. - A new variable is a generator change first. Add it to the object passed
to
renderFile, list it in the template’s comment, then use it.
Versions are variables, never numbers
A version typed into a template is stale on the next bump. Read it:
- README templates get
release:version(every instance declares one), and — for an instance declaring aniriBase—major,agent(<iriBase><version>/, for identifiers) andhuman(<iriBase>v<major>/, for pages). Empty strings where not declared: test with!= "", and show nothing rather than a guess. - Site templates read
site.data.harness.releases.<instance>— the same four fields for every instance, written bysync-docs-harnessinto_data/harness.jsonunder thedocs:harness:checkgate.
Link a person to human, never agent: the full-version address is for
programs, which pin exactly (instance-publication
§”Release IRIs”).
The leading comment IS the file’s description
Open every template with a {%- comment -%} block. Its first sentence is
what the directory README’s “what it is” column shows for the file
(describe() in bootstrap-tools/scripts/readme-graph-sections.ts), so write it for
a reader of that table — “The file table: files directly in the directory,
then its subdirectories.” — and put the variables it reads in the lines after.
No comment and the row says only “a template”.
Includes
jekyllInclude: true gives Jekyll’s syntax: {% include files.liquid %}, the
file name unquoted and with its extension. An include with no parameters sees
the including template’s variables (files.liquid reads files directly); a
parameter passed as {% include x.liquid key=value %} is read as
include.key, as in Jekyll. Split a template when a part is reusable or when
a reader would want its own row for it — not to hide logic.
Whitespace — check the output, not the template
{%- and -%} strip the whitespace on that side of the tag, and that is how a
template emits clean Markdown. It is also how a separator the output needs
disappears: one {% endif -%} in _includes/landing.html glued two HTML
attributes together and the live landing page printed its own markup as text,
past every gate (continual-progress §”A template is
not a page”). Strip freely inside a tag’s own lines; think before stripping
next to a table row’s newline, the blank line before a heading or table, or
an attribute boundary. subgraph-readmes also collapses three or more
newlines to two, so do not fight blank lines with stripping.
Escaping
strictFilters: true— an unknown filter is an error, not a silent pass-through. No custom filter is registered.- A value bound for a table cell is escaped before it reaches the
template — the generator’s
cell()turns|into\|and folds newlines. A new cell-bound field goes through it; a raw|from a file’s description splits the row. - Liquid syntax in prose that Jekyll will build must sit inside a
rawblock.gen-skill-docswraps every skill page in one, so a skill may quote{{ }}and{% %}freely — but never the closingendrawtag itself, which ends the block early. A generated README that sits under a site source (cat-harness/docs/, rootdocs/) may be read by the site build too; if a KG value there can carry Liquid, build the site and look before assuming either way.
An undefined variable is a message, not silence
Jekyll renders an undefined variable as the empty string and says nothing;
strict_variables says something by FAILING the build and by changing what
renders (the exception aborts the whole tag, else branch included).
docs/_plugins/liquid-undefined-warn.rb does neither: every lookup runs as
Liquid runs it, and at the end of the build each path that was ABSENT is
printed as a warning with its count and pages, site.data.* first (owner,
2026-10-09, bean 8pyz: “jekyll strict (if not error out, just message)”).
A lookup inside a condition or under | default is not reported — there the
template asked — nor an include’s own parameter (include.x, optional by
convention), a field of a variable that is there and nil (its cause is reported
once, where it went undefined), or a path liquid_undefined_ignore in
_config.yml lists: a THEME’s optional settings, which site-chrome/_config.yml
names for just-the-docs. On the immunizations IG those rules took 399 reports
to the 2 real gaps. FOLIO_LIQUID_UNDEFINED_REPORT=<file> writes the same record
as JSON; liquid_undefined: off in _config.yml turns it off. It runs only
where plugins do (bundle exec jekyll, as an IG repository’s own site and
preview:site build); the github-pages build is safe mode and loads none.
Markers — the platform owns the region, the folio owns the file
subgraph-readmes writes only between <!-- kg:subgraph:begin --> and
<!-- kg:subgraph:end -->, creates a README holding just that region when
there is none, and leaves an unmarked README alone and reported. The rules,
and why the whole-file generator they replaced was wrong, are in
readme-sections; do not restate them in a template.
Regenerate, commit, and the gate
bun run cat readme:subgraphs # render every declared directory's README + the QA sidecar
bun run cat readme:subgraphs:check # fail on a stale README or sidecar — CI and the pre-commit hook
Changing a template changes every README it renders; commit them with the
template, in one commit. So does adding or removing a file in any declared
directory — that directory’s table changed. readme:subgraphs:check runs in
code-quality-gates.yml and in cat-harness-tools/scripts/git-hooks/pre-commit; it
is part of bun run cat gates.
On the site specifically
- Pages need front matter to be pages —
title, andparent/nav_orderfor just-the-docs’ sidebar, which it builds from front matter at build time. baseurlis/folio-assistant; a link or asset path goes throughrelative_url, never a literal/….- Override just-the-docs through its extension-point includes, not by copying
a theme layout — a copied layout silently stops tracking the pinned version
(
upstream-version-adoption). - A generated reference directory (
reference/skill-instructions/,reference/skills/,auto-docs/) is never hand-edited; change its generator.