#Objets
Voici tout ce qu’un thème peut lire. La liste est complète : ce qui n’y figure pas n’existe pas, et rendra une chaîne vide sans prévenir. Chaque champ est donné avec son type, un usage tiré d’un thème livré, et la valeur que le serveur rend réellement.
La source qui fait foi est src/lib/liquid/contexte.ts, complétée par le code
qui la remplit — src/lib/liquid/rendu-site.ts et le rendu public du site. Les
valeurs ci-dessous en sortent, pas d’une intention.
#La table des matières du contexte
| Objet | Forme | Disponible |
|---|---|---|
site | objet | partout |
page | objet | partout |
settings | objet libre | partout |
menu | tableau | partout |
articles | tableau | partout (vide sur les vues du blog) |
blog | objet | les vues du blog rendues par le thème |
article | objet | templates/article.json seulement |
annee | entier | partout |
theme_base | chaîne | partout |
section | objet | dans une section seulement |
content_for_layout | chaîne de HTML | dans layout/theme.liquid seulement |
Un snippet appelé par render ne voit rien de tout cela. Le tag isole
complètement la portée : seuls les paramètres passés explicitement entrent. Voir
les tags — c’est la surprise la plus fréquente, et de loin.
#site
L’entreprise à qui appartient le site. Deux champs, et le second n’est pas ce qu’on croit.
site.nomchaîneLe nom du site tel qu’il est saisi dans le CMS. C’est ce qu’on met dans la balise
title, dans le logo textuel de l’en-tête et dans la mention de copyright.
site.adressechaîneLe préfixe des liens internes, pas une adresse absolue. En production il est
vide : le site est servi sous son propre nom de domaine, et un lien interne
n’a besoin d’aucun préfixe. Il ne vaut /s/mon-site que sur l’hôte de
l’administration, c’est-à-dire en développement, où les sous-domaines ne
résolvent pas.
C’est pour cela qu’Origo écrit {{ site.adresse }}/ et non {{ site.adresse }} :
sans la barre oblique finale, le lien vers l’accueil serait une chaîne vide en
production.
<a class="marque" href="{{ site.adresse }}/">{{ site.nom }}</a>N’écris jamais <a href="{{ site.adresse }}"> seul, ni
{{ site.adresse }}/contact en croyant fabriquer une URL absolue. Le premier
produit href="" en production — le navigateur recharge la page courante ; le
second produit /contact, ce qui se trouve être juste, mais par accident. Pour
un lien interne, écris simplement /contact.
#page
La page en cours de rendu.
page.titrechaîneLe titre saisi dans le CMS — sur les vues du blog, celui de l’article ou du
blog affiché, ou « Articles ». Il sert dans la balise title de la coquille, et
plusieurs thèmes livrés le reprennent comme champ caché provenance du
formulaire de contact, pour savoir depuis quelle page la demande a été envoyée.
page.adressechaîne/ pour l’accueil, /le-slug pour les autres, /blog/… sur les vues du
blog. Sans préfixe : contrairement à
site.adresse et aux entrées de menu, ce champ n’est pas préfixé sur l’hôte
d’administration. Il sert à savoir où l’on est, pas à fabriquer un lien.
page.accueilbooléenVrai sur la page d’accueil. Utile pour une coquille qui met un h1 sur l’accueil
et un h2 ailleurs, ou pour n’afficher une barre d’annonce que sur la première
page.
page.demandechaîne"merci", "erreur", ou vide. C’est l’état du dernier envoi de formulaire, lu
depuis ?demande= dans l’adresse.
Ce champ mérite qu’on s’y arrête, parce qu’il porte à lui seul tout le retour
utilisateur d’un formulaire de contact. Le formulaire poste vers /api/contacts,
la route enregistre la demande puis redirige vers la page avec
?demande=merci (ou ?demande=erreur), et la page se rend à nouveau. La section
Contact peut donc remercier sans une ligne de JavaScript — ce qui tombe bien,
puisque assets/ n’accepte pas de .js.
{%- if page.demande == 'merci' -%}
<p class="message message-ok" role="status">
Merci, votre message est bien arrivé. Nous vous répondons vite.
</p>
{%- elsif page.demande == 'erreur' -%}
<p class="message message-ko" role="alert">
Le message n'a pas pu être envoyé. Vérifiez votre e-mail ou votre
téléphone, puis réessayez.
</p>
{%- endif -%}
<form class="formulaire" method="post" action="/api/contacts">
…
<input type="hidden" name="provenance" value="{{ page.titre }}">
</form>role="status" et role="alert" ne sont pas décoratifs : ils font annoncer le
message par un lecteur d’écran au moment où il apparaît. Un message de
confirmation qu’on ne peut pas entendre n’est pas un message de confirmation.
#settings
Les réglages globaux du thème — couleurs, largeur, typographie. Ils sont lus
dans config/settings_data.json, sous la clé current, et l’accès se fait par
identifiant : {{ settings.accent }} pour le réglage dont l’id est accent.
{
"current": {
"encre": "#15171c",
"papier": "#ffffff",
"accent": "#3a55cf",
"largeur": 1160,
"boutons": "pilule",
"sections": {
"entete": { "settings": { "cta_label": "Nous contacter" } }
}
}
}La clé sections est retirée de settings avant le rendu : elle ne contient
pas des réglages globaux mais ceux des sections posées par la coquille.
{{ settings.sections }} rend donc du vide, et c’est voulu.
Les valeurs par défaut déclarées dans config/settings_schema.json ne sont pas
fusionnées dans settings. Un réglage global absent de settings_data.json
rend du vide, même s’il a un "default" dans le schéma — contrairement aux
réglages de section, où la fusion a bien lieu.
Concrètement : ajouter un réglage global à un thème demande de l’ajouter aux
deux fichiers. Sinon --accent: ; part dans la feuille de style, la
déclaration est invalide, et toute la page perd sa couleur d’accent sans qu’une
seule erreur soit levée. Voir les réglages globaux.
#section
L’objet de la section en cours. Il n’existe que pendant le rendu d’un fichier
de sections/ — ni dans la coquille, ni dans un snippet appelé par render.
section.idchaîneL’identifiant de l’instance. Pour une section posée par un template, c’est la
clé qu’elle porte dans templates/*.json ; pour une section posée par la
coquille avec section, c’est son nom de fichier. Deux instances de la même
section dans une page ont le même type et des id différents — c’est ce qui
permet de fabriquer une ancre unique.
section.typechaîneLe nom du fichier, sans son extension : sections/prestations.liquid donne
prestations.
section.settingsobjetLes réglages de cette instance, accessibles par leur id. Ils sont fusionnés
avec les valeurs "default" du {% schema %} : ce qui n’a pas été renseigné
prend le défaut. Sans cette fusion, ajouter un réglage à un thème déjà installé
afficherait du vide partout, et l’auteur croirait son défaut ignoré.
section.blockstableauLes blocs répétables de la section, dans l’ordre défini par block_order. Vide
si la section n’en déclare pas. Chaque bloc porte id, type et settings.
<ol class="etapes-liste">
{%- for bloc in section.blocks -%}
<li>
<span class="etape-rang">{{ forloop.index }}</span>
<div>
<h3>{{ bloc.settings.titre }}</h3>
{%- if bloc.settings.texte != blank -%}<p>{{ bloc.settings.texte }}</p>{%- endif -%}
</div>
</li>
{%- endfor -%}
</ol>forloop.index vient de Liquid, pas de Webcosa : dans une boucle for, l’objet
forloop donne index (à partir de 1), index0, first, last, length et
rindex. Numéroter les étapes en CSS aurait été possible ; le faire ici permet
au numéro d’être stylé comme un élément à part entière.
Quand une section porte plusieurs types de blocs, on teste bloc.type :
{%- for bloc in section.blocks -%}
{%- if bloc.type == 'image' -%}
<img src="{{ bloc.settings.fichier }}" alt="{{ bloc.settings.legende | escape }}">
{%- else -%}
<p>{{ bloc.settings.texte }}</p>
{%- endif -%}
{%- endfor -%}Le détail de la déclaration est sur la page des blocs.
#menu
Le menu principal du site, tel qu’il est composé dans le CMS. C’est un tableau d’objets à deux champs, et rien d’autre : ni sous-menus, ni état actif, ni identifiant.
menu[].labelchaîneLe texte du lien.
menu[].adressechaîneL’adresse. Les liens internes sont déjà préfixés — contrairement à
page.adresse — donc on les écrit tels quels. Les liens externes sont laissés
intacts.
<nav class="menu" aria-label="Menu principal">
{%- for lien in menu -%}
<a href="{{ lien.adresse }}">{{ lien.label }}</a>
{%- endfor -%}
</nav>Pour marquer l’entrée courante, compare avec page.adresse — mais souviens-toi
que menu[].adresse est préfixé et page.adresse non. En production les deux
préfixes sont vides et la comparaison directe fonctionne ; sur l’hôte
d’administration, elle échoue silencieusement. Un thème qui en dépend paraîtra
cassé en développement seulement.
#articles
Les derniers articles publiés du site, tous blogs confondus, du plus récent au plus ancien. Le tableau est plafonné à 12 entrées, quelle que soit la valeur demandée par la section.
articles[].titrechaîneLe titre de l’article.
articles[].extraitchaîneLe chapeau saisi dans le CMS. Vide s’il n’y en a pas — d’où le test
!= blank avant de rendre le paragraphe.
articles[].adressechaîneL’adresse de l’article, de la forme /blog/<blog>/<slug>, déjà préfixée.
articles[].imagechaîneL’adresse de l’image de couverture, vide s’il n’y en a pas. C’est une URL
complète servie par la plateforme, pas un fichier d’assets/ : ne lui applique
pas asset_url.
articles[].datechaîneLa date de publication, destinée au filtre date_fr. Le rendu public la reporte
depuis published_at : {{ article.date | date_fr }} rend donc une date en
toutes lettres. Vide si l’article n’en porte aucune en base.
{%- if articles.size > 0 -%}
<div class="grille grille-3">
{%- for article in articles limit: section.settings.nombre -%}
<article class="carte">
{%- if article.image != blank -%}
<a href="{{ article.adresse }}"><img src="{{ article.image }}" alt="" loading="lazy"></a>
{%- endif -%}
<h3><a href="{{ article.adresse }}">{{ article.titre }}</a></h3>
{%- if article.extrait != blank -%}<p>{{ article.extrait }}</p>{%- endif -%}
<span class="date">{{ article.date | date_fr }}</span>
</article>
{%- endfor -%}
</div>
{%- endif -%}Trois habitudes à reprendre de cet extrait. Le if articles.size > 0 évite un
titre de section suivi du vide sur un site sans blog. Le limit: de la boucle
for prend sa valeur d’un réglage range, ce qui rend le nombre de cartes
réglable sans toucher au code. Et loading="lazy" sur les images n’est pas un
détail : une page de douze photos qui les charge toutes d’un coup perd deux
secondes sur la note de vitesse.
Sur les vues du blog rendues par le thème, articles est vide : la liste à
montrer est celle du blog affiché, dans blog.articles.
#blog
Le blog affiché, sur les vues du blog rendues par le thème :
templates/blog.json pour les listes, templates/article.json pour un
article. Voir Les templates.
Absent partout ailleurs — la clé n’existe pas, ce n’est pas un objet vide.
Sur une page ordinaire, {{ blog.titre }} rend du vide et {% if blog %} est
faux.
blog.titrechaîneLe titre du blog. « Articles » sur le sommaire (plusieurs blogs, aucun choisi)
et quand le site n’a aucun blog en base : ses articles non rattachés sont
servis à /blog sous ce titre.
blog.descriptionchaîneLa description saisie dans le CMS. Vide s’il n’y en a pas, et sur le sommaire.
blog.adressechaîne/blog/<blog>, ou /blog quand aucun blog n’est choisi. Déjà préfixée,
comme menu[].adresse : on l’écrit telle quelle dans un href.
blog.articlestableauLes articles publiés du blog, du plus récent au plus ancien, sans plafond
— contrairement à articles. Chaque entrée a les cinq champs de articles[] :
titre, extrait, adresse, image, date. Vide sur le sommaire.
Sur un article, c’est la liste de son blog, l’article ouvert compris :
compare adresse pour l’écarter d’un « À lire aussi ».
blog.blogstableauTous les blogs du site, dans l’ordre du CMS. Chacun porte titre,
description et adresse (déjà préfixée). C’est la liste que montre le
sommaire ; un thème peut aussi s’en servir pour une navigation entre blogs.
<h1>{{ blog.titre }}</h1>
{%- if blog.description != blank -%}<p class="chapeau">{{ blog.description }}</p>{%- endif -%}
{%- if page.adresse == '/blog' and blog.blogs.size > 1 -%}
{%- for b in blog.blogs -%}
<a class="carte" href="{{ b.adresse }}"><h2>{{ b.titre }}</h2></a>
{%- endfor -%}
{%- else -%}
{%- for a in blog.articles -%}
<article class="carte">
<h2><a href="{{ a.adresse }}">{{ a.titre }}</a></h2>
<time datetime="{{ a.date }}">{{ a.date | date_fr }}</time>
</article>
{%- else -%}
<p>Aucun article publié pour l’instant.</p>
{%- endfor -%}
{%- endif -%}Le premier test reconnaît le sommaire : /blog sur un site qui a plusieurs
blogs. page.adresse n’est pas préfixée, la comparaison vaut donc aussi sur
l’hôte d’administration. Le else d’une boucle for est du Liquid standard :
il s’affiche quand le tableau est vide.
#article
L’article ouvert, sur /blog/<blog>/<article> — rendu par
templates/article.json, et nulle part ailleurs. Ailleurs, la clé n’existe
pas.
article.titrechaîneLe titre de l’article. Il n’est pas dans article.contenu : c’est à la
section de le poser, en <h1>.
article.extraitchaîneLe chapeau saisi dans le CMS, vide s’il n’y en a pas.
article.adressechaîneL’adresse de l’article, de la forme /blog/<blog>/<slug>, déjà préfixée.
article.imagechaîneL’adresse complète de l’image de couverture, vide s’il n’y en a pas. Comme pour
articles[].image, pas d’asset_url.
article.datechaîneLa date de publication, au format ISO 8601 (2026-09-14T08:00:00+00:00) : elle
se passe telle quelle à date_fr, et convient à l’attribut datetime d’une
balise time. Vide si l’article n’en porte aucune.
article.contenuchaîne de HTMLLe corps de l’article, déjà rendu en HTML par la plateforme — la même
fonction qui rend sa propre page d’article. Les balises sortent d’une liste
fermée (p, h2 à h4, ul, ol, li, blockquote, pre, code, hr,
br, img, strong, em, s, a) et chaque texte est échappé : un
<script> écrit dans l’article ressort en texte visible.
article.blog.titrechaîneLe titre du blog de l’article — pour un lien de retour.
article.blog.adressechaîneL’adresse de ce blog, déjà préfixée.
N’applique aucun filtre de texte à article.contenu — ni escape, ni
lignes, ni truncate. C’est du HTML : escape afficherait les balises au
visiteur, truncate couperait une balise en deux. Pose-le tel quel, comme
content_for_layout, et mets-le en forme dans ta feuille de style
(.article-corps p, .article-corps h2…) : la plateforme n’y ajoute aucun
style.
{%- assign autres = 0 -%}
<ul class="a-lire-aussi">
{%- for a in blog.articles -%}
{%- if a.adresse != article.adresse and autres < section.settings.nombre -%}
<li><a href="{{ a.adresse }}">{{ a.titre }}</a></li>
{%- assign autres = autres | plus: 1 -%}
{%- endif -%}
{%- endfor -%}
</ul>La boucle nomme sa variable a, pas article : {% for article in … %}
masquerait l’article ouvert le temps de la boucle, et la comparaison
d’adresses ne comparerait plus rien.
#annee
L’année en cours, en entier. Elle est calculée au moment du rendu, pas à la compilation du thème.
<span>© {{ annee }} {{ site.nom }}</span>Pourquoi l’exposer alors qu’un thème pourrait s’en passer ? Parce que sans elle, chaque thème écrirait l’année en dur dans son pied de page, et tous les sites afficheraient 2025 en février 2026. Un pied de page périmé est le genre de détail qui fait douter un visiteur de la fraîcheur de tout le reste.
Le calcul a lieu à chaque rendu de page, jamais à la génération d’un cache. Une page qui serait mise en cache pour la nuit du 31 décembre garderait sinon l’année de sa génération. Le rendu public étant dynamique, la valeur est toujours juste.
#theme_base
La base des adresses des fichiers d’assets/. Sa valeur est
/theme-assets/<id-du-thème> — un identifiant de thème, pas son nom.
Ne l’utilise pas directement. Un thème peut techniquement écrire
{{ theme_base }}/theme.css et court-circuiter asset_url, mais ce n’est pas
une promesse qu’on tient : le jour où la route des assets changera, seul
asset_url suivra. Ce champ est documenté parce qu’il est visible, pas parce
qu’il est fait pour être écrit.
Écris {{ 'theme.css' | asset_url }}. Voir les filtres.
#content_for_layout
Le HTML des sections de la page, déjà rendu. Il n’existe que dans
layout/theme.liquid, et c’est le point d’insertion du contenu : la coquille
décide où il tombe et pose autour ce qu’elle veut.
{% section 'entete' %}
<main id="contenu">{{ content_for_layout }}</main>
{% section 'pied' %}C’est du HTML déjà assemblé : ne lui applique ni escape, ni lignes, ni aucun
filtre de texte — tu afficherais le balisage au visiteur. La coquille est
détaillée sur sa propre page.
#Ce qui n’est pas là
Ni product, ni cart, ni collection, ni customer, ni checkout. Ce n’est
pas un oubli et ce n’est pas provisoire : Webcosa fait des sites vitrines, et
exposer des objets systématiquement vides ferait écrire des thèmes contre une API
qui ne rend jamais rien. La page des absents explique le
raisonnement en détail et dit quoi faire à la place.
blog et article suivent la même règle à leur échelle : ils n’existent que
sur les vues du blog qui en ont l’usage. Pas de blogs à la racine, pas de
blog.articles sur une page ordinaire.

