FR EN

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.

6 min de lecture11 sectionsChapitre 10 / 22

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 :

src/media/library.ts — créé par l'intégration ts
export const library = import.meta.glob<{ default: ImageMetadata }>(
  './**/*.{png,jpg,jpeg,webp,avif}',
  { eager: true },
);
Pourquoi un motif relatif

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 :

ÉtapeCe 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.
Pourquoi le navigateur fait le travail sur les pixels

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.
Ce que le test couvre, et ce qu'il ne couvre pas

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 :

bash bash
INLINE_HEIC_SAMPLE=/chemin/vers/photo.heic npm run test:heic

Ce 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ôleRègleRefus
Débit30 envois par quart d'heure429
Taille annoncée20 Mo413, avant de lire le corps
Identitécookie de session valide401
Taille reçue20 Mo413
FormatJPEG, PNG ou WebP, reconnu aux octets415, en disant ce qui a été reconnu
Dimensionsentre 1 et 10 000 px415
Nomréécrit systématiquement, puis revérifié400
Collisionun 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.

text text
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

  1. Copier le fichier

    Dans src/media/, nommé en minuscules, sans accent ni espace.

  2. 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
    }

    src est un nom de fichier, pas un chemin. Les dimensions doivent être les vraies : elles servent à réserver la place avant le chargement.

  3. 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"
    />
Le build échoue si le fichier manque

[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 :

formes reconnues text
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/123456789

Un 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

astro astro
<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

  • alt présent partout — imposé par le schéma et par <Image />.
  • width et height systé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 widths et sizes.
  • Des noms de fichiers qui décrivent l'image — ils comptent, et ils sont normalisés à l'envoi.

Si quelque chose cloche

SymptômeCause
« 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 secondesNormal : 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 pasLe 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'imageCasse du nom, extension différente, ou fichier placé dans public/.
L'envoi est refusé pour taillePlus de 20 Mo après traitement navigateur : c'est rare, et cela signale une image d'origine hors norme.