#Fichiers
C’est la route qui fait le travail : theme recuperer, theme pousser et
theme suivre ne sont que des boucles autour d’elle. Elle lit une arborescence,
lit un fichier, en écrit un, en supprime un — un seul à la fois, jamais le
thème entier.
Deux onglets ouverts sur deux fichiers différents ne se marchent donc pas dessus, et une sauvegarde interrompue ne laisse pas un thème à moitié réécrit. Pour déplacer un thème complet, c’est Import et export.
L’<id> du chemin est celui d’un thème du site résolu. Un thème d’un autre site
répond 404 introuvable, sur les trois méthodes.
#Lire l’arborescence
/api/themes/<id>/fichiers{
"theme": {
"id": "11111111-1111-4111-8111-111111111111",
"nom": "Origo",
"role": "publie"
},
"fichiers": [
{
"chemin": "assets/theme.css",
"octets": 18422,
"empreinte": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"maj_le": "2026-08-05T09:12:44.108Z"
},
{
"chemin": "config/settings_schema.json",
"octets": 1204,
"empreinte": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
"maj_le": "2026-07-28T14:03:02.554Z"
},
{
"chemin": "layout/theme.liquid",
"octets": 3987,
"empreinte": "486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7",
"maj_le": "2026-07-28T14:03:02.554Z"
}
]
}{ "error": "introuvable" }Portée lecture. Les chemins sont triés par ordre alphabétique, et le
contenu n’est pas rendu : un thème complet pèse plusieurs centaines de
kilo-octets, et l’éditeur n’affiche qu’un fichier à la fois.
themeobjetid, nom et role du thème. role vaut publie ou brouillon — c’est ce que
le CLI regarde avant de refuser de suivre un thème en ligne.
fichiers[].cheminchaîneToujours deux segments : dossier/fichier. Voir Arborescence
pour les six dossiers et leurs extensions.
fichiers[].octetsentierLa taille réelle en octets UTF-8 du contenu. Un « é » y pèse 2.
fichiers[].empreintechaîneLe SHA-256 hexadécimal du contenu, encodé en UTF-8. Calculé à la volée, jamais stocké.
fichiers[].maj_lehorodatageDernière écriture de ce fichier précis.
#À quoi sert l’empreinte
C’est elle qui rend theme pousser supportable. Le CLI calcule le SHA-256 de
chacun de ses fichiers locaux, le compare à celui d’ici, et n’envoie que les
différents. Sans elle, il faudrait télécharger le thème entier avant chaque
envoi juste pour savoir quoi envoyer — quelques centaines de kilo-octets à chaque
sauvegarde, pour au plus un fichier modifié.
octets ne peut pas jouer ce rôle. Corriger une faute de frappe, changer une
couleur hexadécimale ou permuter deux lignes laisse le fichier exactement aussi
long : un client qui se fierait à la taille sauterait précisément les
modifications qu’on fait le plus souvent.
Elle n’est stockée nulle part, et c’est délibéré. Le contenu est de toute façon
lu par cette requête — il faut bien le lire pour donner octets — tandis qu’une
colonne d’empreinte en base serait une seconde source de vérité à tenir à jour à
chaque écriture, donc une occasion de mentir le jour où on l’oublierait quelque
part.
# L'arborescence distante, ramenée à « chemin empreinte »
curl -s "https://cms.webcosa.com/api/themes/$THEME/fichiers" \
-H "Authorization: Bearer $WEBCOSA_CLE" \
| jq -r '.fichiers[] | "\(.chemin) \(.empreinte)"' > distant.txt
# La même chose en local
find layout sections snippets assets config templates -type f \
-exec shasum -a 256 {} \; | awk '{print $2, $1}' | sort > local.txt#Lire un fichier
/api/themes/<id>/fichiers?chemin=sections/hero.liquid{
"fichier": {
"chemin": "sections/hero.liquid",
"contenu": "<section class=\"hero\">\n <h1>{{ section.settings.titre }}</h1>\n</section>\n",
"maj_le": "2026-08-05T09:12:44.108Z"
}
}{ "error": "fichier_introuvable" }Le paramètre chemin est comparé à l’identique, sans normalisation : pas de
barre oblique de tête, pas de ./, la casse compte. Pensez à l’encoder — la
barre oblique passe telle quelle dans une chaîne de requête, mais un espace ou un
accent, non.
Notez les deux codes distincts : introuvable désigne le thème,
fichier_introuvable le chemin. Un client qui les confondrait chercherait
une erreur d’identifiant là où il n’y a qu’une faute de frappe dans un nom de
section.
#Écrire un fichier
/api/themes/<id>/fichiersCorps de la requête
{
"chemin": "sections/temoignages.liquid",
"contenu": "<section class=\"temoignages\">…</section>\n{% schema %}\n{ \"name\": \"Témoignages\" }\n{% endschema %}\n"
}{ "ok": true }{
"error": "syntaxe",
"message": "Liquid invalide — ligne 14, colonne 1 : la balise « for » n'est jamais refermée — il manque {% endfor %}.",
"ligne": 14,
"colonne": 1
}{
"error": "syntaxe",
"message": "Le schéma de la section n'est pas un JSON valide — ligne 73, colonne 3 : il manque une virgule ou une accolade fermante après une valeur.",
"ligne": 73,
"colonne": 3
}{ "error": "hors_dossier", "message": "Ce fichier n'est dans aucun dossier connu. Attendus : layout, sections, snippets, assets, config, templates." }{ "error": "trop_de_fichiers", "message": "Un thème est limité à 120 fichiers." }{ "error": "fichier_existe", "message": "Ce fichier existe déjà dans le thème : il n'a pas été remplacé." }{ "error": "site_inactif", "etat": "expire", "message": "Ce site n'est plus actif : ses modifications sont bloquées. Reprends un abonnement depuis le CMS (Réglages → Abonnement)." }Écriture idempotente : le chemin existe, il est remplacé ; il n’existe pas, il
est créé — sauf avec siAbsent, plus bas, qui refuse de remplacer. Il n’y a pas
de POST distinct, et il n’y a pas de version précédente à laquelle revenir.
cheminchaînerequisDe 3 à 120 caractères, exactement deux segments. Voir les refus ci-dessous.
contenuchaînerequisLe fichier entier, en clair. Plafonné à 524 288 — attention, des unités de code
UTF-16, pas des octets : un fichier plein d’accents passe la barre un peu plus
tard qu’un ls -l ne le laisse croire. C’est la même mesure partout dans le
produit, et elle est volontairement large.
Une chaîne vide est acceptée : un fichier peut être vidé sans être supprimé.
siAbsentbooléenN’écrire que si le chemin est libre. Un fichier qui existe déjà avec un autre
contenu répond 409 fichier_existe et n’est pas touché ; un fichier qui porte déjà
exactement ce contenu répond 200 avec { "ok": true, "inchange": true }, sans
rien écrire — rejouer la requête est donc sans effet. Absent ou false, le PUT
remplace, comme ci-dessus.
C’est ce qu’emploie Cosa quand il ajoute une section à un thème : sa proposition annonce un fichier neuf, et ne doit jamais écraser celui qu’on aurait écrit sous le même nom entre-temps.
#Les quatre refus de chemin
error | Ce qui l’a déclenché |
|---|---|
traversee | Le chemin commence par /, contient .. ou une barre oblique inversée |
hors_dossier | Pas exactement deux segments, ou premier segment inconnu |
nom | Le nom du fichier ne commence pas par une lettre ou un chiffre, ou contient autre chose que lettres, chiffres, points, tirets et tirets bas |
extension | Extension non admise dans ce dossier — un .js dans assets/, un .liquid dans config/ |
traversee est la faille classique des archives, celle qui permet d’écrire
ailleurs que là où on croit. Elle est vérifiée à l’entrée, pas au moment
d’écrire : un seul endroit à relire.
#La syntaxe est vérifiée avant d’écrire
Un fichier Liquid mal formé casse la page où il est rendu. Il est donc analysé avant d’être enregistré, et le refus dit où et pourquoi : en français, position comprise.
C’est ce qui distingue un éditeur de code utilisable d’un champ de texte : on apprend l’erreur en enregistrant, pas en visitant son site.
| Extension | Ce qui est vérifié |
|---|---|
.json | JSON.parse |
.liquid | L’analyse Liquid ; pour une section, le JSON de son {% schema %} aussi |
.css, .svg | Rien. Un CSS invalide dégrade l’apparence, il ne casse pas le rendu |
Le contrôle porte sur la syntaxe, pas sur le sens. {{ nawak }} passe — comme
chez Shopify — et rend du vide. Aucun {% render %} n’est résolu à ce stade :
appeler un snippet qui n’existe pas ne se voit qu’au rendu de la page.
Le refus porte deux champs en plus du message, quand l’analyseur sait où :
ligne et colonne, comptées à partir de 1 dans le fichier entier — y
compris pour une erreur du {% schema %}, dont la position est celle du
fichier et non celle du bloc. Un JSON qui s’arrête trop tôt (une accolade jamais
refermée) n’a pas de position : les deux champs sont alors absents. Le
message reste la seule chose à afficher ; ligne et colonne servent à y
emmener — c’est ce que fait l’éditeur de code du CMS.
Le {% schema %} d’une section est validé depuis le 30 septembre 2026. Avant,
un schéma au JSON malformé s’écrivait sans erreur, et la section perdait en
silence tous ses réglages dans l’éditeur visuel — une panne qu’on ne reliait à
rien. Un thème importé avant cette date peut encore en contenir un : le
premier enregistrement du fichier le signalera.
#Le plafond de fichiers
120 par thème, et il n’est contrôlé qu’à la création. Un thème déjà au
plafond reste modifiable — sinon on ne pourrait plus corriger celui qui dépasse.
Autrement dit, PUT sur un chemin existant ne compte jamais.
curl -s -X PUT "https://cms.webcosa.com/api/themes/$THEME/fichiers" \-H "Authorization: Bearer $WEBCOSA_CLE" \-H "Content-Type: application/json" \-d "$(jq -n --arg c "$(cat sections/hero.liquid)" \ '{chemin:"sections/hero.liquid", contenu:$c}')"{"ok":true}Une écriture réussie met aussi à jour le maj_le du thème, pas seulement
celui du fichier : c’est lui qui ordonne la liste des thèmes.
#Supprimer un fichier
/api/themes/<id>/fichiers?chemin=snippets/vieux.liquid{ "ok": true }{ "error": "invalid_input" }{
"error": "fichier_requis",
"message": "Ce fichier est indispensable au thème : il ne peut pas être supprimé."
}Trois chemins sont protégés, et la liste est close :
layout/theme.liquid
templates/index.json
config/settings_schema.jsonSans coquille ni template, le thème ne rend plus rien et le site tombe. Ces trois
fichiers se remplacent par un PUT, ils ne se retirent pas.
Supprimer un chemin qui n’existe pas répond 200 ok. La suppression est
idempotente : rejouer une requête après un délai réseau ne doit pas produire une
erreur qui enverrait chercher un problème inexistant.
Le CLI, lui, ne supprime jamais par défaut : theme pousser signale les
fichiers présents à distance et absents en local, sans les toucher. Il faut
--supprimer, et une confirmation. Ces fichiers n’existent qu’en base — il n’y a
ni corbeille ni version précédente.
#L’original d’un fichier
/api/themes/<id>/fichiers/origine?chemin=sections/hero.liquid{
"origine": { "id": "origo", "nom": "Origo", "version": "1.1.0" },
"installee": "1.0.2",
"contenu": "<section class=\"hero\">…"
}{ "error": "sans_origine", "message": "Ce thème n'a pas d'original connu : il a été importé ou écrit à la main." }{ "error": "absent_de_l_original", "message": "« sections/offre.liquid » n'existe pas dans Origo : il a été ajouté à ce thème." }Portée lecture, mêmes gardes que les autres méthodes : un thème d’un autre site
répond 404 introuvable. La réponse est le fichier tel que le catalogue le
livre aujourd’hui — c’est ce que l’éditeur de code compare au fichier retouché,
et ce qu’il remet d’un clic (« Restaurer l’original » n’est qu’un PUT de ce
contenu : aucune écriture n’a sa route à elle).
origineobjetLe thème du catalogue dont celui-ci est issu : son identifiant, son nom et sa version actuelle.
installeechaîneLa version installée sur ce site. Quand elle diffère de origine.version, le
contenu rendu est celui de la version plus récente : restaurer, c’est aussi en
prendre les nouveautés pour ce fichier.
Mise en cache une minute, en privé : le catalogue ne change qu’à un déploiement.
#L’ordre des contrôles
| Rang | Contrôle | Refus |
|---|---|---|
| 1 | Clé valable, portée suffisante, site accessible | 401, 403, 409 |
| 2 | Le thème est bien du site | 404 introuvable |
| 3 | Site actif — écritures seulement | 402 site_inactif |
| 4 | Corps ou paramètre conforme | 400 invalid_input |
| 5 | Chemin admis par le format | 400 traversee, hors_dossier, nom, extension |
| 6 | Syntaxe du fichier | 400 syntaxe |
| 7 | Chemin déjà pris, avec siAbsent | 409 fichier_existe |
| 8 | Plafond de fichiers, à la création | 400 trop_de_fichiers |
Le 404 du thème passe avant la garde de facturation : un identifiant erroné
sur un site suspendu répond 404, pas 402.

