#Créer son premier thème
À la fin de ce guide, un site Webcosa rendra avec un thème Liquid que tu auras sur ton disque, dans ton éditeur, sous ton Git — et un changement de couleur mettra une dizaine de secondes à se retrouver en ligne. On part d’un site vide et on ne saute aucune étape.
Le parcours en une phrase : installer un thème d’origine, le publier, le récupérer sur sa machine, le modifier, le repousser. Cinq gestes, dont trois ne se font qu’une fois.
Fais ce guide sur un site qui n’est pas encore devant des clients — un site d’essai, ou une préproduction. On y publie un thème dès la deuxième étape, et publier change ce que voient les visiteurs. Un site en ligne se traite autrement : voir La boucle locale et Publier.
#Avant de commencer
Node.js≥ 20.13requisVérifie avec node --version — Node 22 LTS ou 24 est le choix recommandé, et
Installation dit d’où vient le plancher. Le CLI n’a aucune
dépendance : le fichier scripts/theme.mjs se suffit à lui-même et peut être
copié hors du dépôt.
Un site actif dans ton compterequisUn site suspendu ou dont l’essai est terminé refuse toute écriture de thème
avec Ce site n'est plus actif (402).
Un éditeur qui connaît LiquidFacultatif mais confortable. Le format est celui de Shopify : n’importe quelle
extension « Shopify Liquid » colore correctement sections/*.liquid.
#Le parcours
Installer un thème d’origine
Dans le CMS, ouvre Site web. En bas de l’écran, la section Installer un thème liste les thèmes d’origine que ce site n’a pas encore : Origo, Forge, Vela, Piazza. Chacun affiche à qui il s’adresse et trois traits qui le distinguent.
Prends Forge pour ce guide — c’est le plus court des quatre, donc le plus facile à lire en entier.
Ce que tu dois voir : un message « « Forge » est installé, en brouillon. », et une nouvelle ligne dans Brouillons, au-dessus de la section d’installation. La carte « Forge » disparaît de la liste des thèmes disponibles — on ne propose pas deux fois le même.
NoteLe catalogue de
store.webcosa.comn’est pas celui-ci. Le Store installe des réglages et une page d’accueil pour l’ancien moteur de rendu ; les thèmes Liquid, eux, s’installent depuis l’écran Site web du CMS et de nulle part ailleurs. Les deux moteurs coexistent — voir Anatomie d’un thème.Le publier
Sur la ligne « Forge » des brouillons, clique Publier. Une confirmation nomme ce qui va changer, puis le site bascule.
Pourquoi si tôt ? Parce qu'un thème brouillon ne se prévisualise pas. Il n’existe aucune adresse qui rende le site avec un thème non publié : le rendu public cherche le thème de rôle
publie, et lui seul. Tant que Forge reste brouillon, tu écriras du Liquid sans jamais voir le résultat.Ce que tu dois voir : « « Forge » est en ligne. », la ligne quitte les brouillons et remonte en haut de l’écran avec la pastille En ligne, et l’aperçu en haut de page montre le nouveau thème. Ouvre l’adresse publique du site dans un onglet : la page d’accueil de Forge s’affiche, avec ses textes d’exemple.
AstuceLa publication crée au passage une page d’accueil au site s’il n’en avait pas. C’est délibéré : publier un thème, c’est mettre le site en ligne, et un site en ligne sans page d’accueil n’a nulle part où poser un titre.
Créer une clé d’API
Le CLI ne connaît pas les cookies du navigateur. Il s’authentifie avec une clé, qui se crée dans le CMS : Réglages → Clés d’API.
Choisis la portée
themes:lecturene permet quelisteretrecuperer, et tu vas pousser.DangerLe secret —
wc_suivi de 43 caractères — ne s’affiche qu’une fois. La base n’en garde que l’empreinte : personne, pas même nous, ne peut te le rendre. Copie-le maintenant. Perdue, une clé se révoque et se remplace, elle ne se retrouve pas.Connecter le CLI
node scripts/theme.mjs connexionLa commande demande l’adresse du CMS, puis la clé — sans l’afficher à l’écran. Elle la vérifie avant de l’écrire dans
~/.webcosa/config.json, en0600. Une clé qui ne marche pas n’est pas enregistrée : la découvrir maintenant vaut mieux que trois commandes plus tard.Ce que tu dois voir :
✓ Clé enregistrée dans /Users/toi/.webcosa/config.json (0600) https://cms.webcosa.com — site Mon Client (mon-client), 1 thème visible. Thème publié : ForgeLa ligne « site … » est celle qu’il faut lire deux fois. Sans
--site=, le serveur retient le premier site accessible à la clé, et cet ordre change dès qu’un site s’ajoute au compte. Si le nom affiché n’est pas celui que tu visais, relance avec--site=<slug>.Voir ce qu’il y a sur le site
node scripts/theme.mjs listerid nom version rôle modifié 11111111 Forge 1.0.0 publié il y a 3 minL’identifiant court — les huit premiers caractères — suffit partout où l’on attend un
--theme=. Le nom aussi.Récupérer le thème sur le disque
node scripts/theme.mjs recuperer --theme=Forge --dossier=~/webcosa/forgeCe que tu dois voir :
✓ 23 fichiers de « Forge » écrits dans /Users/toi/webcosa/forge .webcosa.json y a été posé : les prochaines commandes n'ont plus besoin d'options.Le dossier contient exactement les six dossiers du format :
~/webcosa/forgeassets/theme.css config/settings_data.json config/settings_schema.json config/theme.json layout/theme.liquid sections/annonce.liquid sections/appel.liquid sections/avis.liquid … snippets/bouton.liquid snippets/section-tete.liquid templates/a-propos.json templates/contact.json templates/index.json templates/page.json .webcosa.json.webcosa.jsondit quel site et quel thème. Il est versionnable et ne contient jamais la clé — celle-ci vit dans~/.webcosa/config.json. Un fichier de projet qui contiendrait le secret finirait poussé sur GitHub le jour même.AttentionN’utilise pas
--dossier=themes/…depuis la racine du dépôt du CMS : cethemes/est celui des thèmes d’origine, compilés au catalogue et proposés à tous les sites. Le CLI t’avertit si tu le fais. Voir Ajouter un thème au catalogue.Se placer dans le dossier
cd ~/webcosa/forgeÀ partir d’ici, plus aucune option n’est nécessaire : le CLI cherche
.webcosa.jsondans le dossier courant puis dans ses parents, comme git avec.git. Lancé de n’importe quel sous-dossier du thème, il retrouve le site et le thème visés.Lire la coquille
Ouvre
layout/theme.liquid. C’est le HTML qui entoure toutes les pages du site — le seul fichier que le rendu exige d’abord, et sans lequel le thème est purement et simplement ignoré.layout/theme.liquidliquid<!doctype html> <html lang="fr"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{ page.titre }} — {{ site.nom }}</title> {{ 'theme.css' | asset_url | stylesheet_tag }} <style> :root { --encre: {{ settings.encre }}; --papier: {{ settings.papier }}; --accent: {{ settings.accent }}; --accent-sombre: {{ settings.accent | teinte: -25 }}; --trait: {{ settings.encre | teinte: 80 }}; --largeur: {{ settings.largeur }}px; } </style> </head> <body> <a class="saut" href="#contenu">Aller au contenu</a> {% section 'annonce' %} {% section 'entete' %} <main id="contenu">{{ content_for_layout }}</main> {% section 'pied' %} </body> </html>Trois choses à retenir de ce fichier, elles reviendront partout :
content_for_layoutest le trou par lequel la page entre. Les sections du template s’y déversent, dans l’ordre.{% section 'entete' %}rend une section directement depuis la coquille. Ses valeurs ne vivent pas dans un template mais dansconfig/settings_data.json, souscurrent.sections. C’est ce qui rend l’en-tête et le pied modifiables comme le reste.- Les réglages deviennent des variables CSS, posées une seule fois sur la racine. Toute la feuille de style suit ensuite un changement de couleur sans qu’on la touche.
Dangerasset_urlprend le nom seul du fichier, jamais son chemin :{{ 'theme.css' | asset_url }}, et surtout pas{{ 'assets/theme.css' | asset_url }}. L’adresse produite est/theme-assets/<id>/theme.css— un seul segment — et c’est la route qui remet le préfixeassets/. Écrire le chemin complet fabrique une adresse à deux segments qui ne correspond à aucune route : la feuille de style répond 404, et la page rend quand même, en HTML nu. Aucune erreur, un site qui ressemble juste à un thème mal écrit. C’est le bug le plus coûteux que ce format ait produit.Changer une couleur
Ouvre
config/settings_data.json. Ce sont les valeurs des réglages globaux pour ce site — leurs déclarations, elles, sont danssettings_schema.json.config/settings_data.jsonjson{ "current": { "encre": "#16211c", "papier": "#f7f5f0", "accent": "#2f6f4e", "largeur": 1200, "trait": 2, "sections": { "annonce": { "settings": { "texte": "Devis gratuit sous 48 h — déplacement compris" } } } } }Remplace
"accent": "#2f6f4e"par"accent": "#b4442c". Enregistre.Tu n’as touché à aucune ligne de CSS : la coquille pose
--accentà partir de ce réglage, et--accent-sombres’en déduit parteinte: -25. Ce filtre existe précisément pour ça — sans lui, chaque nuance d’une couleur réglable devrait être un réglage de plus, et personne ne remplit un panneau de vingt sélecteurs de couleur.Pousser
node scripts/theme.mjs pousserForge · /Users/toi/webcosa/forge ↑ modifié config/settings_data.json ✓ 1 modifié, 22 inchangésLe CLI compare les empreintes SHA-256 locales à celles du serveur et n’envoie que ce qui a changé. Vingt-deux fichiers identiques ne traversent pas le réseau.
Regarder
Recharge l’adresse publique du site. L’accent a changé : les boutons, les filets, les surtitres.
Il n’y a rien à purger et rien à attendre — la page publique est rendue à chaque requête, donc la modification est visible au rechargement suivant.
#Ajouter une section
Changer une couleur, c’est du réglage. Ajouter une section, c’est écrire du thème. Deux fichiers : la section, puis sa déclaration dans le template.
Écrire la section
Crée
sections/horaires.liquid. Le fichier a deux parties : le balisage, puis un bloc{% schema %}qui décrit ses réglages.sections/horaires.liquidliquid<section class="section horaires"> <div class="enveloppe"> {%- render 'section-tete', surtitre: section.settings.surtitre, titre: section.settings.titre -%} <dl class="horaires-liste"> {%- for bloc in section.blocks -%} <div> <dt>{{ bloc.settings.jour }}</dt> <dd>{{ bloc.settings.heures }}</dd> </div> {%- endfor -%} </dl> {%- if section.settings.note != blank -%} <p class="horaires-note">{{ section.settings.note | lignes }}</p> {%- endif -%} </div> </section> {% schema %} { "name": "Horaires", "settings": [ { "type": "text", "id": "surtitre", "label": "Surtitre", "default": "Nous trouver" }, { "type": "text", "id": "titre", "label": "Titre", "default": "Horaires d'ouverture" }, { "type": "textarea", "id": "note", "label": "Note", "default": "" } ], "blocks": [ { "type": "jour", "name": "Jour", "settings": [ { "type": "text", "id": "jour", "label": "Jour", "default": "Lundi" }, { "type": "text", "id": "heures", "label": "Heures", "default": "9 h – 18 h" } ] } ], "max_blocks": 7 } {% endschema %}Quatre choses s’y jouent, et elles sont générales :
section.settings.<id>lit un réglage déclaré plus bas. L’iddu schéma et la clé lue doivent coïncider — une faute de frappe rend du vide, sans erreur. Voir Les types de réglage.section.blocksest la liste des éléments répétables. Le client en ajoute et en retire depuis l’éditeur, dans la limite demax_blocks. Voir Les blocs.render 'section-tete'appellesnippets/section-tete.liquid. Un snippet ne voit que ce qu’on lui passe — il n’hérite pas du contexte de l’appelant. Voir Snippets.lignestransforme les retours à la ligne en<br>et échappe le HTML. C’est le filtre à utiliser sur tout texte saisi dans une zone multiligne : sans lui, la note s’afficherait d’un seul tenant.
La déclarer dans le template
Une section écrite mais non déclarée ne rend rien. C’est
templates/index.jsonqui compose la page d’accueil, et il tient deux clés :sections, qui donne les valeurs de chacune, etorder, qui donne l’ordre d’affichage.Ajoute l’entrée dans
sections, puis le nom dansorder:templates/index.jsonjson{ "sections": { "horaires": { "type": "horaires", "settings": { "titre": "Horaires d'ouverture", "note": "Fermé les jours fériés.\nSur rendez-vous le samedi." }, "blocks": { "j1": { "type": "jour", "settings": { "jour": "Lundi au jeudi", "heures": "8 h – 18 h" } }, "j2": { "type": "jour", "settings": { "jour": "Vendredi", "heures": "8 h – 16 h" } }, "j3": { "type": "jour", "settings": { "jour": "Samedi", "heures": "sur rendez-vous" } } }, "block_order": ["j1", "j2", "j3"] } }, "order": ["bandeau", "chiffres", "prestations", "horaires", "contact", "appel"] }AttentionCe bloc est un extrait : garde les autres sections du fichier. La clé de premier niveau (
"horaires") est un identifiant libre dans la page ;"type"est le nom du fichier, sans.liquid. Les deux se ressemblent ici parce que c’est l’usage, mais rien ne l’impose — deux sections du même type peuvent coexister sous deux clés différentes.L’ordre des blocs vient de
block_order, pas de l’ordre des clés deblocks: un objet JSON n’a pas d’ordre garanti, et s’en remettre à lui produit une page qui se réordonne toute seule après un aller-retour en base.Pousser et regarder
node scripts/theme.mjs pousserForge · /Users/toi/webcosa/forge + nouveau sections/horaires.liquid ↑ modifié templates/index.json ✓ 1 nouveau, 1 modifié, 22 inchangésRecharge l’accueil : la section apparaît entre les prestations et le formulaire de contact.
#Quand ça ne marche pas
Trois échecs couvrent la quasi-totalité des premiers essais.
#Le CLI refuse le fichier
✗ sections/horaires.liquid — tag "for" not closed, line:14, col:1 [syntaxe]La syntaxe est validée sur le serveur, à l’écriture, et un fichier refusé n’est pas enregistré. L’envoi s’interrompt : un thème à moitié poussé est pire qu’un thème non poussé. Corrige, repousse.
#La section n’apparaît pas, sans message
Regarde le code source de la page rendue — pas l’inspecteur, qui ne montre
pas toujours les commentaires — et cherche <!-- section.
<!-- section absente : horaire -->Une section qui échoue est remplacée par un commentaire HTML plutôt que par une
page blanche. Ici, le "type" du template et le nom du fichier ne coïncident
pas — horaire contre horaires.
#Un réglage « ne marche pas »
C’est presque toujours un id qui ne correspond pas. Le moteur tourne avec
strictVariables désactivé : une variable absente rend une chaîne vide, pas
une erreur. Pratique en production, traître en développement.
Vérifie que l’id du schéma, la clé du template et ce que lit le Liquid
s’écrivent exactement pareil, accents compris.
#La suite
Tu as maintenant la boucle complète, mais avec un pousser à la main à chaque
sauvegarde. La commande de suivi le fait pour toi — et il faut savoir quand
elle est dangereuse.

