Skip to content
Quilium Docs
Back to site
How-to guides

Reuse blocks (partials)

Factor repeated markup into partials, pass parameters explicitly, and dispatch a page's content blocks with a single include.

Liquid has no macros. Reuse in Quilium is {% include %} plus a naming convention.

Live example: Lisière. Every snippet on this page comes from Lisière, the demo site of an independent long-form magazine. Its partials, _contents, _articleMeta, _localeUrl, are the ones shown here.
Quilium ships no partial library. Every partial in your site is one you wrote. The names used across this documentation (_contents, _localeUrl) are conventions from the example site, not built-ins. See Built-in partials and helpers for what the engine actually provides.

1. Create a partial

Put it at the site root, prefix the filename with an underscore, and include it by name without the extension.

{%- comment -%} _articleMeta.liquid — arg: article, withByline (boolean) {%- endcomment -%}
<p class="meta">
  <time datetime="{{ article.date }}">{{ article.date | date: 'D MMMM YYYY' }}</time>
  {% if withByline and article.author != blank %}<span>{{ article.author }}</span>{% endif %}
  {% if article.readingTime != blank %}<span>{{ article.readingTime }} min</span>{% endif %}
</p>

Include it with named parameters:

{% include '_articleMeta', article: art, withByline: true %}

Open every partial with a comment naming its arguments. It is the only signature Liquid gives you.

2. Know that include shares scope

include runs the partial in the caller's scope. The partial sees content, page, globals, locale and everything the caller assigned, on top of the parameters you passed. That is deliberate: it is what lets an element partial read the current block without threading it through.

The cost is that assignments leak in both directions.

Pass every input on every call. A variable set inside a partial stays set for the includes that follow it on the same page. If _langSwitcher reads variant, pass variant: '' explicitly on the calls that want the default, rather than relying on nobody having set it earlier.
{% include '_langSwitcher', variant: 'dark' %}
...
{% include '_langSwitcher', variant: '' %}   {# not just {% include '_langSwitcher' %} #}

3. Dispatch a page's content blocks

The one partial every site needs: it walks a section and renders each block through its own content-type template.

{%- comment -%} _contents.liquid — arg: section (array of blocks) {%- endcomment -%}
{%- for content in section -%}
  {%- if content.template != blank -%}
    {%- assign _file = content.template | replace: 'elements/', '' | prepend: 'elements/' -%}
    {% include _file %}
  {%- endif -%}
{%- endfor -%}

Call it once per drop zone, from the page-type layout:

{% include '_contents', section: page.contents['main'] %}

Two things make it work. {% include %} accepts a variable path, so the content-type's own template key decides which element renders. And because scope is shared, each element sees content set to the block being rendered, with no parameter to pass.

The replace then prepend pair makes the elements/ prefix idempotent, so the dispatcher works whether a content-type declares 'aLaUne.liquid' or 'elements/aLaUne.liquid'.

4. Write a helper partial

A partial does not have to render markup. It can compute a value, which is how you get helper functions in a language that has no function calls. Include it where the value belongs, and its output lands there.

{%- comment -%} _localeUrl.liquid — arg: path (a fixed internal path starting with /) {%- endcomment -%}
{%- assign _first = path | truncate: 1, '' -%}
{%- if path != blank and _first == '/' -%}
  /{{ locale.slug }}{%- unless path == '/' -%}{{ path }}{%- endunless -%}
{%- else -%}
  {{ path }}
{%- endif -%}
<a href="{% include '_localeUrl', path: '/newsletter' %}">{{ 'Subscribe' | _ }}</a>

Use tight whitespace control ({%- and -%}) throughout a helper like this one. A stray newline ends up inside your href.

Not | slice: 0 for the first character. In Quilium, slice is overridden — it chunks an array into groups, and handed a string it returns empty. This exact helper shipped with slice on five demo sites: the test fell through to the else branch every time, the partial returned the bare path, and no internal link carried its locale prefix — on the English site they all pointed at the French pages, in 200, for months. truncate: 1, '' gives the first character. The general lesson is in Liquid filters: a Quilium filter can shadow the LiquidJS one you have in mind, and the failure is an empty string, not an error.

5. Choose between a partial and a content-type

They solve different problems. Getting this wrong produces either an unusable backoffice or a template full of hardcoded copy.

Use When
Partial the markup repeats but its data comes from the caller: a card, a byline, a URL helper, site chrome
Content-type an editor needs to place it, order it, and fill its fields, on a page of their choosing
Element that reads page data the block is placed by an editor but its content comes from a page-type query, so it carries no fields of its own

The third row is the one people miss. A detail page's title block, body block and related-items block are content-types with no fields: the editor decides where they sit, and each one renders part of page.custom.* resolved once at page level.

Related


Last updated September 6, 2026.