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.
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.
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
choicefor standard prose. A body that is only paragraphs and headings belongs in awysiwyg, where the style palette already provides lead paragraphs, quotes and drop caps. Reaching forchoicethere 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.