#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.
{{ 'theme.css' | asset_url | stylesheet_tag }}<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.
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 :
<img src="{{ 'motif.svg' | asset_url }}" alt="">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.
/theme-assets/8f2c…/theme.cssContent-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: inlineNot 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
L’extension décide du type servi
.cssdevienttext/css,.svgdevientimage/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.Le nom est revalidé
Même expression régulière qu’à l’écriture. Le nom est recomposé, jamais concaténé depuis l’URL :
..%2Fet autres échappements ne peuvent pas sortir du dossierassets.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.
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.
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.
| Correction | Ce qu’elle fait | Ce qu’il te reste à faire |
|---|---|---|
| Les photos à la taille de l’écran | Une <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 page | Une 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 double | Un 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
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 :
<style>
:root {
--accent: {{ settings.accent }};
--accent-sombre: {{ settings.accent | teinte: -18 }};
--largeur: {{ settings.largeur }}px;
}
</style>.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 :
| Directive | Valeur 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 |
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
sandboxposé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 unimgou 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.
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
.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 :
| Besoin | Sans JavaScript |
|---|---|
| Menu repliable | details / summary, ou une case à cocher |
| Accordéon de FAQ | details / summary |
| Apparition au défilement | animation-timeline: view() |
| Défilement doux vers une ancre | scroll-behavior: smooth |
| Carrousel | scroll-snap-type, avec un débordement horizontal |
| Retour après envoi d’un formulaire | page.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 externeBloqué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 pousserSoixante secondes de cache navigateur. Recharge en contournant le cache avant de chercher plus loin.

