---
title: Reuse blocks (partials)
description: Factor repeated markup into partials, pass parameters explicitly, and dispatch a page's content blocks with a single include.
updated: 2026-07-25
prev: build-a-template
next: multilingual-site
---

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

<div class="q-callout q-callout-note"><i class="fa-solid fa-flask lead"></i>
  <div><b>Live example: Lisière.</b> Every snippet on this page comes from <b>Lisière</b>, the demo site of an independent long-form magazine. Its partials, <code>_contents</code>, <code>_articleMeta</code>, <code>_localeUrl</code>, are the ones shown here.</div></div>

<div class="q-callout q-callout-note"><i class="fa-solid fa-circle-info lead"></i>
  <div><b>Quilium ships no partial library.</b> Every partial in your site is one you wrote. The names used across this documentation (<code>_contents</code>, <code>_localeUrl</code>) are conventions from the example site, not built-ins. See <a href="/en/docs/partials-helpers">Built-in partials and helpers</a> for what the engine actually provides.</div></div>

## 1. Create a partial

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

```liquid
{%- 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:

```liquid
{% 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.

<div class="q-callout q-callout-warn"><i class="fa-solid fa-triangle-exclamation lead"></i>
  <div><b>Pass every input on every call.</b> A variable set inside a partial stays set for the includes that follow it on the same page. If <code>_langSwitcher</code> reads <code>variant</code>, pass <code>variant: ''</code> explicitly on the calls that want the default, rather than relying on nobody having set it earlier.</div></div>

```liquid
{% 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.

```liquid
{%- 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:

```liquid
{% 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.

```liquid
{%- 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 -%}
```

```liquid
<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`.

<div class="q-callout q-callout-warn"><i class="fa-solid fa-triangle-exclamation lead"></i>
  <div><b>Not <code>| slice: 0</code> for the first character.</b> In Quilium, <code>slice</code> is <b>overridden</b> — it chunks an <i>array</i> into groups, and handed a string it returns empty. This exact helper shipped with <code>slice</code> on five demo sites: the test fell through to the <code>else</code> branch every time, the partial returned the bare path, and <b>no internal link carried its locale prefix</b> — on the English site they all pointed at the French pages, in 200, for months. <code>truncate: 1, ''</code> gives the first character. The general lesson is in <a href="/en/docs/liquid-filters">Liquid filters</a>: a Quilium filter can shadow the LiquidJS one you have in mind, and the failure is an empty string, not an error.</div></div>

## 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

<div class="q-cards">
  <a class="q-card reco" href="/en/docs/partials-helpers"><span class="k">Recommended</span><span class="t">Built-in partials and helpers</span><span class="d">What the engine ships, and what you have to write.</span></a>
  <a class="q-card" href="/en/docs/build-a-template"><span class="t">Build a template</span><span class="d">Layouts, blocks and elements end to end.</span></a>
  <a class="q-card" href="/en/docs/content-types"><span class="t">Content-type schema</span><span class="d">The <code>template</code> key the dispatcher reads.</span></a>
</div>
