Aller au contenu

#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

ObjetFormeDisponible
siteobjetpartout
pageobjetpartout
settingsobjet librepartout
menutableaupartout
articlestableaupartout (vide sur les vues du blog)
blogobjetles vues du blog rendues par le thème
articleobjettemplates/article.json seulement
anneeentierpartout
theme_basechaînepartout
sectionobjetdans une section seulement
content_for_layoutchaîne de HTMLdans layout/theme.liquid seulement
Attention

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îne

Le 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îne

Le 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.

sections/entete.liquid — Origoliquid
<a class="marque" href="{{ site.adresse }}/">{{ site.nom }}</a>
Danger

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îne

Le 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éen

Vrai 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.

sections/contact.liquid — Origoliquid
{%- 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>
Astuce

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.

config/settings_data.json — Origo, extraitjson
{
  "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.

Danger

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îne

L’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îne

Le nom du fichier, sans son extension : sections/prestations.liquid donne prestations.

section.settingsobjet

Les 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.blockstableau

Les 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.

sections/etapes.liquid — Forgeliquid
<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 :

Plusieurs types de blocsliquid
{%- 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.

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îne

Le texte du lien.

menu[].adressechaîne

L’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.

sections/entete.liquid — Origoliquid
<nav class="menu" aria-label="Menu principal">
  {%- for lien in menu -%}
    <a href="{{ lien.adresse }}">{{ lien.label }}</a>
  {%- endfor -%}
</nav>
Note

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îne

Le titre de l’article.

articles[].extraitchaîne

Le 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îne

L’adresse de l’article, de la forme /blog/<blog>/<slug>, déjà préfixée.

articles[].imagechaîne

L’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îne

La 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.

sections/journal.liquid — Origoliquid
{%- 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.

Note

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îne

Le 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îne

La 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.articlestableau

Les 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.blogstableau

Tous 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.

sections/liste-articles.liquidliquid
<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îne

Le titre de l’article. Il n’est pas dans article.contenu : c’est à la section de le poser, en <h1>.

article.extraitchaîne

Le chapeau saisi dans le CMS, vide s’il n’y en a pas.

article.adressechaîne

L’adresse de l’article, de la forme /blog/<blog>/<slug>, déjà préfixée.

article.imagechaîne

L’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îne

La 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 HTML

Le 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îne

Le titre du blog de l’article — pour un lien de retour.

article.blog.adressechaîne

L’adresse de ce blog, déjà préfixée.

Danger

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.

sections/a-lire-aussi.liquidliquid
{%- 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.

sections/pied.liquid — Origoliquid
<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.

Note

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.

Attention

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.

layout/theme.liquid — Piazzaliquid
{% 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.