Chapitre 10Construire
Images et vidéos
Une photo de 8 Mo sortie d'un téléphone est le cas nominal, pas le cas limite. Où passe chaque étape du traitement, et pourquoi aucune vidéo n'est jamais téléversée.
Où vivent les images
Dans src/media/, et pas dans public/media/. C'est la seule façon
pour <Image /> d'astro:assets de les traiter au build : AVIF,
WebP, jeu de largeurs, dimensions écrites dans le HTML.
src/media/photo.webp
Traité au build. Plusieurs formats, plusieurs largeurs, width et height dans la balise, empreinte dans le nom servi.
public/media/photo.webp
Servi tel quel. Aucun format moderne, aucune largeur alternative, aucune dimension — donc un décalage de mise en page au chargement.
Le composant Media vit dans le paquet, les images vivent dans le site : la
recherche des fichiers doit donc s'exécuter côté site. L'intégration crée pour cela un fichier
de trois lignes, src/media/library.ts, à laisser tel quel :
export const library = import.meta.glob<{ default: ImageMetadata }>(
'./**/*.{png,jpg,jpeg,webp,avif}',
{ eager: true },
);Un motif absolu serait résolu depuis la racine du projet, dont l'écriture varie selon la façon dont le build est lancé — sous Windows, la casse de la lettre de lecteur suffit à le faire échouer.
Les trois étapes du traitement
Chacune est là où elle a les moyens de se faire :
| Étape | Où | Ce qui s'y passe |
|---|---|---|
| 1. Décodage, redressement, recadrage, réduction, conversion WebP | Navigateur | Une photo de 15 Mo part en quelques centaines de kilo-octets. |
| 2. Contrôle et rangement | Fonction | Format reconnu aux octets, dimensions lues dans l'en-tête, fichier renommé, écrit dans le dépôt. |
| 3. AVIF, WebP, jeu de largeurs | Build | <Image /> d'astro:assets. |
Le runtime des fonctions de périphérie n'a pas de codec, et la compilation de WebAssembly à l'exécution y est interdite : la seule voie serait un module WASM, qui ne démarrerait pas. Le résultat est meilleur de toute façon — ce qui traverse le réseau se compte en centaines de kilo-octets, et le dépôt ne grossit pas de photos brutes.
Ce que le client fait, lui
Il clique sur l'image, choisit un fichier sur son appareil, et remplit un seul champ : « Description de l'image », prérempli quand le nom du fichier veut dire quelque chose. Il n'a jamais à redimensionner ni à convertir quoi que ce soit — c'est une règle du projet, pas une facilité.
Les photos HEIC d'iPhone
Un iPhone photographie en HEIC par défaut. Safari sait l'afficher, Chrome et Firefox non. Sans traitement, un client sur PC recevant une photo par AirDrop ou par courriel se verrait refuser son fichier — c'est-à-dire qu'on lui demanderait de le convertir, exactement ce que le projet s'interdit.
- Le décodeur du navigateur est tenté en premier.
- En cas d'échec, et seulement si le fichier est reconnu HEIC à ses octets, un décodeur dédié est chargé.
- Ce module pèse 1,4 Mo et ne part que dans ce cas : un client qui ne dépose jamais de HEIC ne le télécharge jamais.
- Une photo de 12 Mpx est décodée en 1,4 s environ, puis repasse par le chemin commun — recadrage, réduction, WebP.
Le décodage complet ne peut pas être testé sans une vraie photo : aucun encodeur HEVC n'est
disponible pour en fabriquer une, et une photo personnelle n'a rien à faire dans un dépôt.
npm run test:heic vérifie donc toujours la reconnaissance du format et
saute explicitement le décodage faute d'échantillon. Pour l'exécuter en
entier :
INLINE_HEIC_SAMPLE=/chemin/vers/photo.heic npm run test:heicCe que la fonction vérifie
Rien de ce que déclare l'appelant n'est cru : ni le type MIME annoncé, ni le nom du fichier, ni les dimensions.
| Contrôle | Règle | Refus |
|---|---|---|
| Débit | 30 envois par quart d'heure | 429 |
| Taille annoncée | 20 Mo | 413, avant de lire le corps |
| Identité | cookie de session valide | 401 |
| Taille reçue | 20 Mo | 413 |
| Format | JPEG, PNG ou WebP, reconnu aux octets | 415, en disant ce qui a été reconnu |
| Dimensions | entre 1 et 10 000 px | 415 |
| Nom | réécrit systématiquement, puis revérifié | 400 |
| Collision | un nom déjà pris est suffixé | — |
Un .jpg qui contient en réalité un SVG est refusé ; un fichier vidéo aussi — avec
un message qui dit lequel des deux c'était, pour que l'interface puisse être claire.
Le nom de fichier
Le nom envoyé par le navigateur n'est jamais repris tel quel. Il est réécrit : minuscules, accents supprimés, espaces et caractères spéciaux remplacés par des traits d'union, extension déduite du format réel.
Photo de l'Équipe (2).HEIC → photo-de-l-equipe-2.webp
IMG_4832.JPG → img-4832.jpg
La même liste blanche décide de ce que /api/upload écrit et de ce
que /api/save accepte de voir référencé dans un contenu :
^[a-z0-9]+(?:-[a-z0-9]+)*\.(jpg|png|webp)$. Un contenu qui pointerait vers un
fichier hors de cette forme est refusé, même si le fichier existe.
Ajouter une image en tant que développeur
-
Copier le fichier
Dans
src/media/, nommé en minuscules, sans accent ni espace. -
Le référencer dans le contenu
json json { "type": "media", "kind": "image", "src": "fournil-au-petit-matin.webp", "alt": "Le fournil au petit matin", "width": 1600, "height": 900 }srcest un nom de fichier, pas un chemin. Les dimensions doivent être les vraies : elles servent à réserver la place avant le chargement. -
Le poser dans la page
astro astro <Media data={data} path="blocks.showcase.visual" widths={[480, 800, 1200, 1600]} sizes="(max-width: 48rem) 100vw, 48rem" />
[Media] Le fichier « … » est absent de src/media. Le message liste les fichiers
disponibles : c'est presque toujours une différence de casse ou une extension qui ne
correspond pas.
Les vidéos
Le schéma n'accepte qu'un fournisseur et un identifiant. Aucun fichier vidéo n'entre dans le dépôt : un fichier lourd dans Git casse le dépôt et les builds, définitivement, et sans moyen simple de revenir en arrière.
Le client colle ce qu'il a sous la main. Toutes ces formes fonctionnent, parce qu'aucune n'est plus « correcte » que les autres de son point de vue :
https://www.youtube.com/watch?v=aqz-KE-bpKQ
https://youtu.be/aqz-KE-bpKQ
https://www.youtube.com/embed/aqz-KE-bpKQ
https://www.youtube.com/shorts/aqz-KE-bpKQ
https://www.youtube.com/live/aqz-KE-bpKQ
youtube.com/watch?v=aqz-KE-bpKQ (sans protocole)
<iframe src="https://www.youtube.com/embed/aqz-KE-bpKQ" …></iframe> (code collé en entier)
https://vimeo.com/123456789
https://player.vimeo.com/video/123456789
https://vimeo.com/channels/staffpicks/123456789Un identifiant YouTube fait exactement onze caractères ; un identifiant Vimeo est numérique. Ces règles sont vérifiées des deux côtés : à la saisie, et à l'écriture par la fonction, qui refuse un couple incohérent.
Ce qui est rendu
<figure data-cms="blocks.showcase.film" data-cms-type="media" data-cms-kind="video">
<iframe src="https://www.youtube.com/embed/aqz-KE-bpKQ" title="Une nuit au fournil" loading="lazy" …></iframe>
<figcaption>Une nuit au fournil</figcaption>
</figure>
Le contenu de l'iframe est chez le fournisseur ; le titre, lui, est dans le HTML
servi. loading="lazy" ne retire rien à l'index : ce n'est pas une
hydratation de composant, l'élément est là dès la source.
Le référencement des images
altprésent partout — imposé par le schéma et par<Image />.widthetheightsystématiques : pas de décalage de mise en page.- AVIF et WebP produits au build, avec repli.
- Un jeu de largeurs adapté au gabarit, via
widthsetsizes. - Des noms de fichiers qui décrivent l'image — ils comptent, et ils sont normalisés à l'envoi.
Si quelque chose cloche
| Symptôme | Cause |
|---|---|
| « Ce format d'image n'est pas accepté » | Le fichier n'est ni JPEG, ni PNG, ni WebP à ses octets — quelle que soit son extension. |
| Une photo HEIC met plusieurs secondes | Normal : le décodeur dédié est chargé, puis la photo est décodée. Une seule fois par session. |
| L'image publiée n'apparaît pas | Le fichier est dans le dépôt, mais le HTML n'est pas encore reconstruit. Attendre la fin du build. |
| Le build ne trouve pas l'image | Casse du nom, extension différente, ou fichier placé dans public/. |
| L'envoi est refusé pour taille | Plus de 20 Mo après traitement navigateur : c'est rare, et cela signale une image d'origine hors norme. |