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
<link rel="stylesheet" href="/theme-assets/8f2c…/theme.css">

asset_url colle le nom derrière theme_base, qui vaut /theme-assets/<id-du-thème>. stylesheet_tag enveloppe le résultat dans une balise link, en échappant l’adresse.

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.

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.

#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 quatre 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 quatre thèmes n’ont chacun qu’un seul asset — theme.css, entre 11 et 23 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
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. Les quatre thèmes utilisent des polices système, ou proposent un choix entre sans empattements et empattements — voir le réglage police_titres d’origo. 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 des quatre thèmes 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