Skip to content
Quilium Docs
Back to site
Explanation

Templating philosophy and practices

Understand the thinking behind a good Quilium template, so the ones you write stay easy to edit, easy to reuse, and hard to break.

What this page is about

This page is the why, not the how. It covers the principles a Quilium template rests on and the trade-offs behind them, so you can make good calls when the syntax leaves you a choice. For the mechanics, see the how-tos: Build a template (Liquid) and Reuse blocks (partials).

The single idea underneath everything below: a template is a contract between a designer and an editor. The designer decides how things look; the editor decides what they say. A good template keeps that line clean. Most template pain, and most editor confusion, comes from a template that quietly crosses it.

Content and presentation are separate, on purpose

The first principle is the oldest one in web publishing, and Quilium leans on it hard: the template renders content, it never holds content.

A template is structure, layout, and logic. The words, the images, the links, the prices live in content — content-block fields (content.custom), page-type queries (page.custom), globals, or a collection. The template reads them; it does not contain them.

{%- comment -%} Good: the template renders a field, it doesn't carry the copy. {%- endcomment -%}
{% assign c = content.custom %}
<h2>{{ c.title }}</h2>
<div class="prose">{{ c.body }}</div>
{%- comment -%} Bad: copy hard-coded in the template. The editor can never touch it. {%- endcomment -%}
<h2>Discover our spring collection</h2>

The payoff. The moment a sentence lives in the template, it is invisible to the editor, invisible to the CMS, untranslatable through the dictionary, and a code change away from every fix. Keep copy in content and the same template serves a hundred pages, an editor reworks them all without a deploy, and the multilingual model can carry each string into another locale. The template stops being a thing only a developer can edit.

The one honest exception. Fixed UI labels that are part of the design, not the editorial copy, do belong in the template, but wrapped in the translate filter so they stay translatable: {{ 'S\'abonner' | _ }}. The msgid is the source string. Everything an editor would ever want to change still lives in content.

Model for the editor's form, not for the render

A Quilium schema does two jobs at once. It defines the data your template reads, and it generates the form the editor fills in. The second job is the one people forget, and it is where most modelling mistakes start.

The principle: name and shape fields for what they mean to the editor, not for how they happen to render. The editor sees field labels and a form, never your Liquid. If the field is called ctaTarget and grouped under a clear tab, the form explains itself. If it is called href2 and sits next to nine unrelated toggles, the editor guesses, and a guessing editor produces broken pages.

A few things follow from taking the editor's form seriously:

  • Name fields by meaning. backgroundImage, ctaText, publishedAt, not img2 or field3. The label the editor reads is derived from the field; a meaningful key gives a meaningful form.
  • The first field of a repeat or a choice variant is the item's label in the admin list. Make it the descriptive one (title, question, quote), never an icon or a color. Otherwise the editor manages a list that reads Item 1 / Item 2 / Item 3.
  • Split a long form into tabsets once it crosses roughly eight fields or mixes concerns: editorial content in one tab, display toggles in another. A wall of twelve fields is a form nobody reads to the bottom.
  • Add an info: hint wherever the format is not obvious from the label: a FontAwesome class, a URL convention, a hex colour. The hint is the difference between a field that is safe and one that is a trap.

The payoff. A schema modelled for the form makes the right thing the easy thing for the editor. They cannot drop a value in the wrong place because the form does not offer a wrong place. You spend the modelling effort once; every editing session after that is faster and safer. A schema modelled only for the render saves you ten minutes today and costs the editor an hour a week forever.

Pick the field that fits the meaning. A styled bullet list or a paragraph-with-a-button is one wysiwyg field, not a repeat. A repeat earns its place only when each item is a typed mini-record the editor could not express in the rich-text they type (icon + title + image). Modelling for meaning is also modelling for the right field type.

Factor a partial when it earns its keep, not before

Quilium templates compose through include. A partial is a fragment you pull out so more than one place can share it. The judgement call is when to factor one out, and both extremes cost you.

The principle: inline until repetition or a clear seam justifies a partial. Premature partials scatter a single page's logic across a dozen tiny files you must open in sequence to read one screen. The opposite, a thousand-line layout that inlines everything, is unreadable and impossible to reuse. Neither is virtuous; the right cut is in between.

Good reasons to factor a partial:

  • The same fragment renders in two or more places (an article card on the home page and on a tag page). Duplicated markup drifts; a shared partial does not.
  • There is a genuine seam the design already implies (site header, footer, locale switcher, an article's byline). These are nouns in the design, so they are natural partials.
  • A content-type needs its own element template. Each content-type points its template: at one element partial, and the _contents dispatcher renders each block by resolving that path. That is not a judgement call; it is how blocks render.
{%- comment -%} The dispatcher: one partial renders every block in a section. {%- endcomment -%}
{% include '_contents', section: page.contents['main'] %}

Weak reasons: a fragment used exactly once, or a split made only because the file felt long. Length alone is not a seam.

The payoff. Partials cut at real seams give you reuse without fragmentation, each file is a unit you can read and reason about on its own, and a change to the byline lands everywhere it shows. Partials cut at imaginary seams give you the indirection tax with none of the reuse. Factor for sharing, not for tidiness.

Naming and the shared-scope trap

Naming is not cosmetic in Quilium; it is part of the contract, and one naming-adjacent behaviour is a genuine footgun. Two rules and one trap.

camelCase, consistently, everywhere. Field names, section names, content-type and page-type keys are camelCase, strict: fluxArticles, backgroundImage, newsletterBande. Match it in every YAML and JSON snippet. The engine also accepts other styles, but a site that mixes them reads as assembled by several hands. Consistency is what makes a template feel authored.

Partials carry a leading underscore. _header, _articleCard, _localeUrl. The convention marks a file as a fragment meant to be included, not a page rendered on its own. It is a signal to the next reader, including future you.

Now the trap. include shares the caller's scope. That is deliberate and useful: a partial sees content, page, globals, locale without you re-passing them. The cost is that a variable set inside one include can leak into a later include on the same page. Lean on a value a previous include happened to set and you get a bug that only appears in a specific order, the worst kind to chase.

{%- comment -%} Pass every input explicitly, on every call. Never rely on a prior include having set it. {%- endcomment -%}
{% include '_articleMeta', article: a, withByline: true %}
{% include '_langSwitcher', variant: 'dark' %}

The discipline is simple: pass each input a partial needs explicitly, on every call — variant: '' here, variant: 'dark' there — and never assume a value carried over. Treat each include as if its inputs start empty, even though scope says otherwise.

The payoff. Explicit params make a partial's behaviour depend only on what you handed it at the call site, so it renders the same wherever it appears, in any order, and you can read one include without tracing every include before it. The shared scope stays a convenience instead of becoming a source of order-dependent bugs.

Scope leaks are silent. A leaked variable does not raise an error; it renders the wrong thing, only sometimes, depending on include order. The habit of always passing params explicitly is what keeps it from ever happening.

The thread through all of it

The four principles are one principle seen from four sides. Separate content from presentation so each side owns what it should. Model for the editor's form so the editor's side is obvious and safe. Factor partials at real seams so the designer's side stays readable and reusable. Name with discipline and pass params explicitly so neither side surprises the other later. A template that holds the line on all four is the one you can hand to an editor, reuse on the next page, and open again in six months without flinching.

Where to go next


Last updated September 6, 2026.