Aller au contenu

#Assets

Le dossier assets/ contient les fichiers statiques d’un thème : sa feuille de style, ses éventuelles icônes vectorielles. Deux extensions sont admises, .css et .svg, et la liste est fermée.

Cette page explique comment un asset reçoit son adresse, comment il est réellement servi — par une route, et non depuis un disque — et pourquoi il n’y a pas de JavaScript.

#Obtenir l’adresse d’un fichier

Un seul chemin : le filtre asset_url, qui prend le nom du fichier.

layout/theme.liquidliquid
{{ 'theme.css' | asset_url | stylesheet_tag }}
HTML renduhtml
<style data-feuille="theme.css">/* le contenu de assets/theme.css */</style>

asset_url colle le nom derrière theme_base, qui vaut /theme-assets/<id-du-thème>. stylesheet_tag pose la feuille dans la page quand elle est un fichier de ton thème de moins de 120 000 caractères ; sinon — feuille plus lourde, ou adresse qui n’est pas un fichier du thème — il rend une balise link vers l’adresse, échappée.

Pourquoi dans la page : la feuille est sur le chemin critique — rien ne s’affiche avant elle — et la route qui la sert lit la base à chaque visite (voir « Le cache » plus bas). Écrite dans le HTML, elle arrive avec lui, il est mis en cache au bord du réseau, et elle se rafraîchit avec lui quand tu enregistres un fichier du thème publié. Ses url() relatives sont ancrées sur theme_base au passage, et un </style> écrit dans un commentaire ne peut pas fermer la balise.

Danger

Le nom seul, jamais le chemin. {{ 'assets/theme.css' | asset_url }} produit /theme-assets/<id>/assets/theme.css — deux segments de nom, qui ne correspondent à aucune route. La feuille de style répond 404 et la page rend quand même, en HTML nu.

C’est exactement le défaut qui a tenu sur tous les sites à la fois, sans jamais lever d’erreur : un site sans CSS ressemble à un thème mal écrit, pas à une adresse fausse.

Pour une image :

liquid
<img src="{{ 'motif.svg' | asset_url }}" alt="">
Attention

asset_url lit theme_base, qui est posé en variable globale du moteur et non dans la portée ordinaire. Il fonctionne donc partout, y compris dans un snippet appelé par {% render %} — alors que ce dernier n’y voit ni site, ni section, ni rien d’autre du contexte de la page. Voir Snippets.

#Comment un asset est servi

Pas depuis le disque. La route /theme-assets/<theme>/<fichier> lit le contenu en base, dans la table des fichiers du thème.

GET/theme-assets/8f2c…/theme.css
200 — text/css
Content-Type: text/css; charset=utf-8
Cache-Control: private, max-age=60
Vary: Cookie, Host
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox
Content-Disposition: inline
404 — extension non servie, nom refusé, thème non publié, ou thème d’un autre site
Not found

#Pourquoi une route, et pas un fichier statique

Parce que les assets sont modifiables. L’éditeur de code du CMS et le CLI les réécrivent ; un fichier posé sur le disque au moment de l’installation ne suivrait pas. Un thème vit en base une fois installé — voir Anatomie.

#Les quatre gardes de la route

  1. L’extension décide du type servi

    .css devient text/css, .svg devient image/svg+xml. Toute autre extension répond 404 avant la moindre requête en base. Renvoyer le type déclaré par le fichier lui-même permettrait de servir du HTML sous le domaine d’un client — donc du script, avec ses cookies.

  2. Le nom est revalidé

    Même expression régulière qu’à l’écriture. Le nom est recomposé, jamais concaténé depuis l’URL : ..%2F et autres échappements ne peuvent pas sortir du dossier assets.

  3. Le thème doit être publié

    Un brouillon en cours de refonte reste invisible même si l’on devine son identifiant. La lecture publique est ouverte — les visiteurs d’un site n’ont évidemment aucun compte — mais uniquement pour ce qui est en ligne.

  4. Le thème doit appartenir au site de l’hôte

    Sans ce test, l’identifiant d’un thème publié suffisait à le faire servir sous le domaine de n’importe quel autre client. C’est l’un des trois défauts qui, composés, ouvraient une exécution de script sous l’origine de n’importe quel site.

#Le cache

Cache-Control: private, max-age=60, avec Vary: Cookie, Host.

private et non public, parce que la réponse dépend de deux choses invisibles dans l’URL : l’hôte appelant, et la session — le propriétaire d’un site en brouillon voit ce qu’un anonyme ne voit pas, et c’est ce qui fait marcher l’aperçu. Un cache partagé pouvait mémoriser la réponse servie au propriétaire et la redistribuer.

La feuille principale n’emprunte plus cette route sur les pages du site : elle est posée dans la page (voir plus haut). La route sert encore les SVG, les feuilles appelées par link, et l’éditeur de code.

Soixante secondes, et c’est court exprès : qui retouche son CSS dans l’éditeur doit voir l’effet en rechargeant, pas dans une heure. Le cache du navigateur reste en place, et c’est lui qui portait l’essentiel du gain — une page vitrine recharge la même feuille de style à chaque écran.

Astuce

Après un pousser, si le CSS ne bouge pas : attends une minute, ou recharge en contournant le cache. Ce n’est presque jamais le thème.

#Ce que la plateforme fait de ta page, après ton thème

Le HTML que ton thème produit passe par quatre corrections avant de partir. Tu n’as rien à écrire pour en profiter ; il faut seulement ne pas les contrarier.

CorrectionCe qu’elle faitCe qu’il te reste à faire
Les photos à la taille de l’écranUne <img> dont l’adresse vient de la médiathèque reçoit un srcset en cinq largeurs (480 à 1 920) servies en WebP, et un sizes : auto, 100vw si elle est différée, 100vw si c’est la première image chargée tout de suite — celle du bandeau. Une image de fond de la médiathèque (url() dans un style) passe en 1 920 de large.Pose loading="lazy" sur toute image sous la ligne de flottaison, et pas sur celle du bandeau. Donne class="…logo…" à un logo : il n’est jamais servi en pleine largeur. Un srcset ou un sizes que tu écris toi-même est respecté.
Un <h1> par pageUne page sans <h1> reçoit le premier <h2> qui porte une classe de son contenu (après <main>, à défaut après </header>), promu en <h1>.Écris toi-même le <h1> du titre de chaque page. Style tes titres par classe, pas par balise : la promotion ne change alors rien à l’écran.
Aucun niveau de titre sautéUn <h3> posé directement sous un <h1> reçoit aria-level="2" — le niveau change pour les lecteurs d’écran et les contrôles, pas la balise ni l’apparence.Respecte la hiérarchie dans tes sections : h2 pour le titre de section, h3 pour les cartes.
Les liens-images en doubleUn lien qui ne contient qu’une image sans texte alternatif, et dont l’adresse est aussi celle d’un lien textuel de la page (la carte d’un article : image et titre), sort du parcours clavier et des lecteurs d’écran (aria-hidden, tabindex="-1").Donne un texte alternatif à une image qui est le SEUL lien vers sa page.

Et sur les pages publiques du site, ton <title>, ta meta charset et ta viewport sont retirées : la page qui accueille ton thème les pose déjà, avec le titre et la description que le client a écrits pour Google. Le titre de ton layout/theme.liquid ne sert donc plus qu’à l’aperçu du Store.

#La feuille de style n’est pas un template

Attention

Le contenu d’assets/theme.css est servi tel quel. Il n’est pas rendu par Liquid : {{ settings.accent }} dans un .css sera écrit littéralement dans la feuille de style, et le navigateur ignorera la déclaration.

C’est pour cela que les huit thèmes posent leurs réglages en variables CSS dans la coquille, et nulle part ailleurs :

layout/theme.liquidliquid
<style>
  :root {
    --accent: {{ settings.accent }};
    --accent-sombre: {{ settings.accent | teinte: -18 }};
    --largeur: {{ settings.largeur }}px;
  }
</style>
assets/theme.csscss
.bouton {
  background: var(--accent);
  border-radius: var(--bouton-rayon);
}

.enveloppe {
  max-width: var(--largeur);
  margin-inline: auto;
}

L’avantage n’est pas seulement de faire marcher les réglages : la feuille de style reste un fichier CSS ordinaire, que l’éditeur colore, que le navigateur met en cache, et qu’on peut relire sans démêler du Liquid.

Les huit thèmes n’ont chacun qu’un seul asset — theme.css, de 21 à 79 Ko. Rien n’interdit d’en découper plusieurs : une @import relative depuis theme.css résout bien vers le même dossier servi.

#Ce que la page peut charger d’ailleurs

Un site public est servi avec une politique de sécurité de contenu, et elle borne ce qu’une feuille de style peut aller chercher :

DirectiveValeur sur un site client
style-src'self' 'unsafe-inline'
font-src'self' data:
img-src'self' data: blob: + la bibliothèque de médias
media-src'self' blob: + la bibliothèque de médias (depuis le 5 octobre 2026 : une video ou un audio déposé dans Fichiers se lit)
script-src'self' + un nonce, plus les traceurs si le client les active
Danger

Une police web hébergée ailleurs ne se charge pas. font-src 'self' data: bloque Google Fonts et tous les autres. Six des huit thèmes nomment leur police avec un réglage font_picker, et c’est la plateforme qui la sert depuis /polices/ — voir le réglage police_titres d’origo ; aplomb et epure s’en tiennent aux piles système. Une police personnelle devrait être encodée en data: dans le CSS, ce qui alourdit la première visite.

Même chose pour une image de fond servie par un CDN tiers : img-src ne l’autorise pas.

#Les SVG

Admis, et servis avec trois protections qui vont ensemble.

Un SVG est un document scriptable : ouvert dans un onglet, image/svg+xml exécute ses balises script, et nosniff n’y change rien. D’où :

  • une CSP sandbox posée par la route elle-même, et non par le proxy — le proxy exclut les chemins finissant par une extension d’image, il ne passe donc jamais sur cette réponse ;
  • un Content-Disposition: inline; filename="…", qui laisse le fichier se rendre comme une image dans un img ou en CSS — le seul usage prévu — mais empêche son ouverture directe d’être interprétée comme une page ;
  • le nosniff, qui interdit au navigateur de deviner un autre type.
Astuce

Pour une petite icône, un SVG écrit directement dans le balisage d’une section est souvent préférable : une requête de moins, et il hérite de currentColor. Réserve assets/ aux fichiers qui se répètent ou qui sont gros.

#Pourquoi il n’y a pas de JavaScript

Danger

.js n’est pas admis dans assets/, et l’absence est délibérée.

Un thème est du contenu modifiable par le client, et demain importable depuis l’extérieur. Un .js déposé dans assets/ serait du script exécuté dans le navigateur de tous les visiteurs du site, sous son domaine, avec accès à ses cookies. Ce n’est pas un risque théorique : c’est le vecteur classique d’une archive de thème piégée.

Le JavaScript de thème viendra quand on saura le relire — c’est-à-dire quand il y aura une réponse à « qu’est-ce qu’on accepte, et qui le vérifie ». En attendant, un thème est du HTML et du CSS.

Le blocage est double, et c’est voulu. La liste d’extensions arrête le fichier à l’écriture ; la CSP du site public arrête le script au navigateur, puisque script-src n’admet que l’origine et un nonce que le thème n’a aucun moyen de connaître.

#Ce qu’on fait à la place

Le CSS moderne couvre l’essentiel de ce pour quoi un thème vitrine ouvrait un fichier JavaScript :

BesoinSans JavaScript
Menu repliabledetails / summary, ou une case à cocher
Accordéon de FAQdetails / summary
Apparition au défilementanimation-timeline: view()
Défilement doux vers une ancrescroll-behavior: smooth
Carrouselscroll-snap-type, avec un débordement horizontal
Retour après envoi d’un formulairepage.demande, côté serveur

Ce dernier mérite un mot : le formulaire de contact de six des huit thèmes d’origine poste vers /api/contacts, la route redirige, et la page se rend à nouveau avec page.demande valant merci ou erreur. Un message de remerciement sans une ligne de script. Voir Objets.

#Les erreurs fréquentes

Le chemin complet passé à asset_url

{{ 'assets/theme.css' | asset_url }}. Le site rend sans style, et rien ne le signale. Vérifie dans l’onglet réseau : un 404 sur une adresse à deux segments.

Un filtre dans un argument de render

{% render 'carte', fond: 'fond.svg' | asset_url %} n’applique pas le filtre : le snippet reçoit la chaîne brute, ou rien. Construis la valeur dans un {% assign %} avant l’appel. Le filtre lui-même marche dans un snippet — c’est l’argument qui ne le supporte pas.

Du Liquid dans un .css

Écrit littéralement dans la feuille de style. Passe par des variables CSS posées dans la coquille.

Une police ou une image externe

Bloquée par la politique de sécurité du site public. Rien n’apparaît dans le rendu, tout apparaît dans la console du navigateur.

Le CSS ne change pas après un pousser

Soixante secondes de cache navigateur. Recharge en contournant le cache avant de chercher plus loin.

#Pages voisines