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

A rich, flexible article body

Let editors build a rich body out of typed blocks, paragraph, quote, image, text-with-image, using a single choice field.

A choice field holds an ordered list of blocks, where each block is one of several typed variants the editor picks from. It is how you get an article-style body whose pieces are structured data rather than one lump of HTML.

Live example: Méridienne. Every snippet on this page comes from Méridienne, the demo site of a neighbourhood coffee roaster. Its cafe collection stores each entry's story in a choice field named histoire, with four variants.

1. Check that you need one

choice is the heaviest of the three "rich body" options. Reach for it last.

Need Use
Styled prose: headings, bold, links, bullet lists, a pull quote from the style palette wysiwyg
One repeating shape, always the same fields repeat
Several different shapes, interleaved in an order the editor chooses choice

The test is heterogeneity. A body that is prose with the occasional inline image is a wysiwyg. A body that alternates paragraphs, full-width images with captions, pull quotes and side-by-side text-plus-image is a choice.

2. Declare the variants

Each entry under config.choices is one variant: a key, a name shown in the editor, and its own fields.

histoire:
  type: choice
  label: Histoire
  config:
    choices:
      paragraph:
        name: Texte
        fields:
          text:
            type: wysiwyg
            label: Texte
      textImage:
        name: Texte + image
        fields:
          text:
            type: wysiwyg
            label: Texte
          image:
            type: image
            label: Image
            config:
              versions:
                wide:
                  label: Large
                  width: 1200
                  height: 900
          side:
            type: select
            label: Côté de l'image
            config:
              options:
                gauche: Gauche
                droite: Droite
      quote:
        name: Citation
        fields:
          text:
            type: textarea
            label: Citation
          author:
            type: text
            label: Auteur
      image:
        name: Image
        fields:
          image:
            type: image
            label: Image
            config:
              versions:
                wide:
                  label: Large
                  width: 1400
                  height: 900
          caption:
            type: text
            label: Légende

Variants are independent: two of them may declare a field with the same name and different types, and nothing is shared between them. Keep the set small, four or five variants is a comfortable ceiling, and give each one a name an editor understands without training.

Give it its own tab. Méridienne puts histoire in a dedicated histoire tabset, away from the entry's factual fields. A body builder next to a price field makes for a crowded form.

3. Know the discriminator

Every stored block carries a _choice key holding the variant key it was created from. That value is how a template knows what it is looking at.

[
  { "_choice": "paragraph", "text": "<p>…</p>" },
  { "_choice": "quote", "text": "…", "author": "Ana Ruiz" },
  { "_choice": "textImage", "text": "<p>…</p>", "image": [{ "…": "…" }], "side": "droite" }
]

The key is _choice, not _type.

4. Write one template per variant

Put them under a folder of their own, one file per variant key, and let each read the block it was handed:

{%- comment -%} elements/blocs/quote.liquid — arg: bloc {%- endcomment -%}
<blockquote>
  <p>« {{ bloc.text }} »</p>
  {% if bloc.author != blank %}<footer>— {{ bloc.author }}</footer>{% endif %}
</blockquote>
{%- comment -%} elements/blocs/textImage.liquid — arg: bloc {%- endcomment -%}
{% assign img = bloc.image[0].versions.wide.url %}
{% assign imgRight = false %}
{% if bloc.side == 'droite' %}{% assign imgRight = true %}{% endif %}
<div class="grid md:grid-cols-2">
  {% if img %}<div class="{% if imgRight %}md:order-2{% endif %}" style="background-image:url('{{ img }}')"></div>{% endif %}
  {% if bloc.text != blank %}<div class="{% if imgRight %}md:order-1{% endif %}">{{ bloc.text }}</div>{% endif %}
</div>

5. Dispatch on the discriminator

One small partial turns the variant key into a filename. It is the same trick the content-block dispatcher uses, applied one level down:

{%- comment -%} _choice.liquid — arg: blocs (the choice field's value) {%- endcomment -%}
{% for bloc in blocs %}
  {% if bloc._choice != blank %}
    {% assign blocFile = 'elements/blocs/' | append: bloc._choice | append: '.liquid' %}
    {% include blocFile %}
  {% endif %}
{% endfor %}

Because include shares scope, each variant template sees bloc without any further plumbing. Call it once, wherever the body belongs:

{% if cafe.histoire != blank %}
  <div class="prose-histoire">
    {% include '_choice', blocs: cafe.histoire %}
  </div>
{% endif %}

Adding a fifth variant is then two steps: a new entry under choices, a new file under elements/blocs/. The dispatcher does not change.

Pitfalls

  • A missing variant template renders nothing, silently. The dispatcher builds a filename from data. If an editor picks a variant you have not written a template for, that block disappears from the page with no error. Add the template at the same time as the variant.
  • Do not use choice for standard prose. A body that is only paragraphs and headings belongs in a wysiwyg, where the style palette already provides lead paragraphs, quotes and drop caps. Reaching for choice there gives the editor a builder for something they could just type.
  • Guard bloc.image[0]. A variant's image field is an array like any other, and an editor can save a block before uploading the image.

Related


Last updated September 6, 2026.