#Depuis un thème Shopify
Le format est le même : mêmes dossiers, même langage, mêmes schémas, mêmes templates JSON. Ce qui n’est pas le même, c’est ce qu’un thème peut lire — Webcosa fait des sites vitrines, pas des boutiques. Ce guide dit exactement ce qui se transpose, ce qui se réécrit, ce qui disparaît, et dans quel ordre s’y prendre.
Ce n’est pas un portage de dix minutes, et ce n’est pas non plus une réécriture. Sur un thème Shopify de site vitrine — accueil, à propos, prestations, contact — comptez une demi-journée : la structure passe telle quelle, les sections marchandes s’effacent, et le travail se concentre sur les filtres d’image et de traduction, qui sont partout. Sur un thème de boutique complet, ne portez pas : reprenez la feuille de style et recomposez. La moitié du fichier parle de produits qui n’existent pas ici.
#Le tableau, en un coup d’œil
| Se transpose tel quel | Change de forme | N’existe pas |
|---|---|---|
Les six dossiers layout/ sections/ snippets/ assets/ config/ templates/ | asset_url : nom seul, jamais de chemin | product, collection, cart, customer, order |
La syntaxe Liquid entière : if, for, case, assign, capture, render | handleize → handle | Les métachamps (metafields) |
{% schema %} : name, settings, blocks, max_blocks, presets | color_lighten / color_darken → teinte: ±n | Les sections d’application et les blocs d’app |
templates/*.json : sections, order, blocks, block_order | newline_to_br → lignes (échappe en plus) | {% form %}, {% paginate %}, {% style %}, {% javascript %}, {% stylesheet %} |
settings_schema.json groupé, settings_data.json avec current | La date localisée → date_fr | content_for_index, {% sections %}, {% content_for 'blocks' %} |
layout/theme.liquid avec content_for_layout | L’archive : un .json, pas un .zip | locales/ et le filtre t de traduction |
{% section 'entete' %} dans la coquille | Les images : par URL, pas par fichier de thème | .js dans assets/, et {% javascript %} avec |
La plupart des filtres standard (default, replace, split, truncate, date, map, where…) | 13 types de réglage, pas davantage | routes, shop, localization, all_products, search |
Le reste de cette page reprend les trois colonnes, une par une.
#Ce qui se transpose tel quel
#L’arborescence
Copie les dossiers. Il n’y a rien à renommer.
layout/theme.liquid .liquid
sections/<type>.liquid .liquid
snippets/<nom>.liquid .liquid
assets/theme.css .css .svg
config/theme.json .json
config/settings_schema.json .json
config/settings_data.json .json
templates/index.json .jsonUne seule règle, mais elle est stricte : un chemin de thème a exactement deux
segments. Pas de sections/blocks/…, pas de templates/customers/…, pas de
sous-dossier dans assets/. Tout ce qui est plus profond est refusé à l’entrée.
Voir Arborescence.
#Le langage
C’est du Liquid, exécuté par LiquidJS — la même grammaire, portée en
JavaScript. Les tags de contrôle, d’affectation et d’inclusion se comportent
comme chez Shopify : if / elsif / unless, case / when, for avec
break et continue, tablerow, cycle, assign, capture, increment,
liquid, echo, raw, comment, et surtout render en portée isolée contre
include en portée partagée. Le contrôle des espaces ({%- et -%}) est
identique.
Voir Tags pour la liste exacte, et Filtres pour les filtres.
#Les schémas de section
Aucun changement d’écriture. Le bloc {% schema %} est du JSON pur posé à la
fin du fichier, extrait par une expression régulière — exactement comme chez
Shopify.
{% schema %}
{
"name": "Avis clients",
"settings": [
{ "type": "text", "id": "titre", "label": "Titre", "default": "Ce qu'ils en disent" }
],
"blocks": [
{
"type": "avis",
"name": "Avis",
"settings": [
{ "type": "textarea", "id": "citation", "label": "Citation" },
{ "type": "text", "id": "auteur", "label": "Auteur" }
]
}
],
"max_blocks": 6
}
{% endschema %}Un type de réglage inconnu est ignoré, pas rejeté. Un "type": "video_url"
hérité de Shopify n’empêche pas la section de rendre, et il ne bloque pas non
plus la valeur : le réglage est lu normalement — sa valeur et son default
remontent dans section.settings comme pour un type admis, puisque le rendu ne
consulte que id et default. Seul le futur panneau de réglages l’ignorera.
C’est délibéré : un thème écrit pour une version plus récente continue
d’afficher sa page. La contrepartie, c’est qu’un type mal orthographié ne se
signale jamais. Les treize types admis sont sur
Les types de réglage.
#Les templates JSON
Même forme, mêmes clés : sections (chaque entrée porte un type, des
settings, des blocks et un block_order) et order.
L’ordre de résolution est aussi celui de Shopify, avec un niveau de moins :
templates/<nom>.json, puis templates/page.json, puis templates/index.json.
Le nom du template suit le slug de la page — une page « tarifs » cherche
templates/tarifs.json. Voir Les templates.
#La coquille
layout/theme.liquid, content_for_layout, et {% section 'entete' %} pour
les sections posées hors template. Leurs valeurs vivent dans
config/settings_data.json, sous current.sections — comme chez Shopify.
Ce qui n’a pas d’équivalent : content_for_header. Webcosa n’injecte rien
dans le head ; ce qui doit y être, le thème l’écrit.
#Ce qui change de forme
Sept réécritures, et elles reviennent souvent. Passe-les en recherche-remplacer avant tout le reste.
asset_urlfiltrePrend le nom seul, jamais le chemin. {{ 'theme.css' | asset_url }}, jamais
{{ 'assets/theme.css' | asset_url }}.
L’adresse produite est /theme-assets/<id-du-thème>/theme.css : un seul segment
de nom, et c’est la route qui remet le préfixe assets/. Un chemin complet
fabrique une adresse à deux segments, qui répond 404 — et la page rend quand
même, en HTML nu. C’est l’erreur la plus coûteuse du format, parce qu’elle ne
lève rien.
handleizefiltreS’appelle handle. Même effet : Mon Titre devient mon-titre, accents
retirés, utilisable en ancre.
color_lighten / color_darkenfiltresUn seul filtre, signé : {{ settings.accent | teinte: -25 }} assombrit de 25 %,
teinte: 40 éclaircit de 40 %. Il n’accepte que du #rrggbb sur six chiffres —
#fff, rgb() et les noms de couleur sortent inchangés.
newline_to_brfiltrenewline_to_br existe (c’est du LiquidJS standard), mais préfère lignes : il
convertit les sauts de ligne et échappe le HTML. Sur un texte saisi par le
client dans une zone multiligne, c’est celui qu’il faut.
datefiltre{{ article.date | date: '%d %B %Y' }} rend un mois en anglais. date_fr rend
8 mars 2026, fuseau Europe/Paris, sans format à passer. Le prix de la
simplicité : un seul style, non paramétrable.
Les imagesassets/assets/ n’accepte que .css et .svg. Pas de .png, pas de .jpg, pas de
.woff2. img_url et image_url n’existent pas.
Les photographies passent donc par une adresse : un réglage "type": "image"
tient une URL, et le thème l’écrit dans un src ou un background-image. Les
polices se chargent depuis une feuille de style externe ou restent celles du
système.
L’archive.jsonShopify livre un .zip. Webcosa livre un .json qui contient les mêmes
fichiers, dans un objet fichiers où la clé est le chemin. Le CLI et le CMS
lisent ce format, et pousser --nouveau le fabrique à la volée depuis un
dossier. Voir Import et export.
#Le formulaire de contact, sans {% form %}
C’est la réécriture la plus visible, et elle est plus simple qu’attendu : un
<form> ordinaire qui poste vers la route du CMS.
<form method="post" action="/api/contacts">
<label class="champ">
<span>Votre nom</span>
<input type="text" name="nom" required>
</label>
<label class="champ">
<span>Votre e-mail</span>
<input type="email" name="email" required>
</label>
<label class="champ">
<span>Votre projet</span>
<textarea name="message" required></textarea>
</label>
<button type="submit" class="bouton">{{ section.settings.bouton }}</button>
</form>Le retour se lit dans page.demande, qui vaut merci ou erreur après l’envoi
et reste vide sinon — de quoi remercier sans une ligne de JavaScript :
{%- 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, puis réessayez.
</p>
{%- endif -%}#Ce qui n’existe pas
#Les objets du commerce
Ni product, ni collection, ni cart, ni customer, ni order. Exposer
des objets vides serait pire que de ne rien exposer : on écrirait des thèmes
contre une API qui ne rend jamais rien, et on découvrirait le vide en
production.
La liste de ce qu’un thème peut lire est fermée, et c’est un contrat :
site, page, settings, section, menu, articles, annee,
theme_base. Rien d’autre. Voir Objets et
Les absents.
Une variable absente rend une chaîne vide, pas une erreur — le moteur tourne
avec strictVariables désactivé. Un {{ product.title }} oublié dans un coin
de section ne casse rien et n’affiche rien : il ne se verra qu’à l’œil, sur la
page. C’est la première chose à soupçonner quand un bloc paraît « ne pas
marcher ».
#Les tags absents
Un tag inconnu, lui, ne passe pas silencieusement : c’est une erreur d’analyse. La section entière est remplacée par un commentaire HTML dans la page rendue, et le CLI refuse le fichier à l’écriture.
| Ce qu’on écrivait | Ce qu’on obtient |
|---|---|
{% form 'contact' %} | <!-- section … : tag "form" not found --> |
{% paginate a by 10 %} | <!-- section … : tag "paginate" not found --> |
{% style %} | <!-- section … : tag "style" not found --> |
{% javascript %} | idem |
{% stylesheet %} | idem |
{% sections 'header-group' %} | idem |
Remplacements : un <form> nu pour form, un <style> dans la coquille pour
style, assets/theme.css pour stylesheet, et {% section %} un par un pour
sections.
#Les traductions
Il n’y a pas de locales/, donc pas de {{ 'general.title' | t }}. Les textes
s’écrivent en clair dans le thème, en français, et ce qui doit varier d’un site
à l’autre devient un réglage. Un thème Webcosa sert un site, dans une langue.
#Le JavaScript
Pas de .js dans assets/, et l’extension est refusée à l’entrée, pas
ignorée au rendu :
! ignoré : assets/menu.js — extension non admise dans ce dossierCe n’est pas du rangement, c’est de la sécurité. Un thème s’exécute sous le
domaine du client, avec ses cookies ; un .js déposé dans assets/ serait du
script exécuté chez tous ses visiteurs. Le JavaScript de thème viendra quand on
saura le relire.
Beaucoup de comportements de thème Shopify se refont sans JavaScript. Vela
implémente ses questions fréquentes en accordéon avec <details> / <summary>,
et son menu mobile avec une case à cocher masquée. C’est ce que fait Piazza pour
ses onglets. Regarde ces thèmes avant de conclure qu’il manque quelque chose.
#Les bornes du bac à sable
Elles n’ont pas d’équivalent visible chez Shopify, et un gros thème peut les toucher.
| Borne | Valeur |
|---|---|
| Temps de rendu | 3 secondes, par page et par section |
| Source analysée | 512 Ko par rendu, inclusions comprises |
Imbrication de {% section %} | 6 niveaux |
| Taille d’un fichier | 512 Ko |
| Nombre de fichiers | 120 par thème |
C’est la dernière ligne qui arrête les portages : un thème Shopify complet dépasse allègrement 120 fichiers. Voir Le bac à sable.
#La méthode de reprise
Dans cet ordre. Chaque étape se vérifie avant la suivante, et l’ordre n’est pas indifférent : on veut voir une page rendre le plus tôt possible, quitte à ce qu’elle soit laide.
Trier le thème d’origine
Ouvre
sections/et range chaque fichier dans une des trois colonnes du tableau du haut. Tout ce qui parle de produit, de panier, de collection, de recherche, de compte client ou de paiement part à la poubelle maintenant — pas « on verra plus tard ». Compte ce qui reste : si tu passes sous 120 fichiers au total, le portage est viable.Ce que tu dois voir : une liste de sections dont chacune tient debout avec
site,page,settings,section,menuetarticles, et rien d’autre.Partir d’un thème d’origine, pas d’un dossier vide
Installe Origo sur un site de préproduction et récupère-le. Tu obtiens une coquille qui fonctionne, un
settings_schema.jsonvalide, unindex.jsonvalide, et un.webcosa.jsondéjà réglé.node scripts/theme.mjs recuperer --theme=Origo --dossier=~/webcosa/portagecd ~/webcosa/portagePartir de zéro coûte une demi-journée à retrouver ce que les trois fichiers indispensables doivent contenir. Voir Créer son premier thème.
Porter la feuille de style d’abord
Copie le CSS de l’ancien thème dans
assets/theme.css. C’est la partie qui se transpose le mieux et qui donne le plus de résultat visible : la mise en page, la typographie, les couleurs.Deux choses à corriger au passage : les
url()qui pointaient vers des images du thème (elles doivent devenir des adresses absolues) et les@font-facequi chargeaient un.woff2local.node scripts/theme.mjs pousser↑ modifié assets/theme.css ✓ 1 modifié, 22 inchangésPorter la coquille
layout/theme.liquid. Retirecontent_for_header, corrige lesasset_url, remplace{% sections 'header-group' %}par des{% section %}un par un.Ce que tu dois voir : la page rend, avec l’en-tête et le pied de l’ancien thème. Vérifie dans le code source que le
<link rel="stylesheet">pointe sur/theme-assets/<id>/theme.csset que la feuille répond bien 200 — c’est le piègeasset_url, et il ne se signale pas autrement.Porter les sections, une par une
Une section, un
pousser, un rechargement. Pas dix d’un coup : quand quelque chose ne rend pas, on veut savoir quoi.Pour chacune : remplacer les objets absents par des réglages, réécrire les sept filtres du tableau, et laisser le schéma tel quel — il passe presque toujours.
Porter les templates
templates/index.jsonen dernier, parce qu’il référence les sections. Les clés et l’ordre viennent de l’ancien thème ; lessettingsde chaque entrée sont à relire, puisque des réglages ont disparu en route.Ajoute
templates/page.jsonsi l’ancien thème avait un gabarit de page générique : sans lui, toute page sans template dédié retombe surindex.json, c’est-à-dire affiche la page d’accueil entière sous un autre titre.Relire le rendu, pas seulement le code
Ouvre le code source de chaque page — pas l’inspecteur, qui ne montre pas toujours les commentaires — et cherche
<!-- section. Chaque commentaire est une section qui a échoué en silence.Rendu de la pagehtml<!-- section produits : tag "paginate" not found --> <!-- section absente : header-group -->Chercher les vides
Les objets absents rendent du vide sans rien dire. Passe le thème au peigne :
grep -rnE "product|collection|cart|customer|metafield|routes\.|shop\." sections snippets layoutChaque occurrence est un trou dans la page, qui ne se signalera jamais tout seul.
#Ce qu’on gagne au change
La liste des absents est longue, alors il faut dire l’autre moitié : ce format donne des choses que Shopify ne donne pas à un site vitrine.
menuvient du CMS, pas d’une liste écrite dans le thème. Le client gère sa navigation dans Site web → Navigation, et le thème l’affiche. Un thème qui déclare ses propres liens oblige à les tenir à jour à deux endroits.page.demanderend le remerciement de formulaire trivial, sans JavaScript et sans page dédiée.teinteévite les vingt sélecteurs de couleur que personne ne remplit.- Le formulaire survit au changement de thème : il poste vers le CMS, pas vers le thème.

