Quand quelqu'un partage une de vos pages sur LinkedIn ou WhatsApp, ce qui s'affiche est une image de 1200 par 630 avec le titre par-dessus. La générer ressemble à un problème de design et relève en réalité de la typographie : tout se ramène à décider à quelle taille tient un titre dont vous ignorez la longueur. Les exemples sont en Laravel, l'une de nos spécialités chez AndorraDev, mais le problème et les décisions sont les mêmes quel que soit le stack.
Un navigateur entier pour dessiner une image
La solution la plus répandue consiste à maquetter la carte en HTML et à la capturer avec Puppeteer ou Playwright. Cela fonctionne, et la logique est difficile à contester : vous savez déjà écrire du HTML et du CSS, donc le design sort en dix minutes.
Le prix arrive ensuite. Vous introduisez Node et quelque trois cents mégaoctets de Chromium dans un projet PHP, dans l'image Docker, dans le déploiement et sur toute machine où quelqu'un devra régénérer les cartes. Pour un site qui en produit une par article, cela fait beaucoup d'infrastructure pour un rectangle avec du texte.
L'alternative que nous utilisons est plus ennuyeuse : un gabarit SVG à trous et un binaire qui le convertit en PNG.
apt install librsvg2-bin
Cela installe rsvg-convert, qui rasterise un SVG en un appel. L'autre option est resvg, un binaire Rust autonome au meilleur support de SVG2. L'un comme l'autre est un exécutable, pas un environnement.
Le gabarit est un SVG à trous
Un SVG est du texte, donc le gabarit se remplit par substitution de chaînes, comme une vue.
<text x="80" y="{{title_baseline}}" font-size="{{title_font_size}}" fill="#ffffff">
{{title_tspans}}
</text>
Rien de magique là-dedans, et c'est tout l'intérêt : le design s'ouvre dans n'importe quel éditeur vectoriel et se retouche sans rien recompiler.
Mesurer le texte est le vrai travail
Quatre-vingt-dix pour cent du problème est là. Un SVG ne sait pas ajuster du texte : pas d'overflow, pas de retour à la ligne automatique, pas de « réduis si ça ne rentre pas ». C'est à vous de décider combien de lignes occupe le titre et à quelle taille, puis d'écrire chaque ligne comme un <tspan>.
Ce que presque tout le monde fait, c'est estimer au nombre de caractères : à 82 pixels il en tient vingt, à 72 il en tient vingt-trois. C'est rapide et c'est faux, parce que les lettres n'ont pas la même largeur. Avec un titre ordinaire cela passe ; avec un titre plein de majuscules ou de mots larges, il déborde de la carte sans que personne ne s'en aperçoive avant publication.
La mesure honnête, c'est GD qui la donne, et il est déjà livré avec PHP :
$box = imagettfbbox($size, 0, $fontPath, $text);
$width = abs($box[2] - $box[0]);
Et il y a là un piège qui nous a coûté un moment. GD interprète la taille comme des points et rend à 96 ppp, alors que le font-size d'un SVG est en pixels utilisateur. Les largeurs ressortent d'un tiers trop grandes. Nous l'avons vérifié en rasterisant un échantillon et en mesurant la tache d'encre du PNG :
imagettfbbox annonce : 940 px
largeur rendue réelle : 705 px
Exactement 0,75, soit 72 divisé par 96. Sans conversion, tous les titres s'ajustent plus petits que nécessaire et tout ce que vous positionnez à partir d'une largeur mesurée tombe décalé. Avec la conversion, le même échantillon donne 704 contre 705 réels.
Vérifiez votre mesure contre le rendu réel avant de vous y fier. Générez une image avec une chaîne connue, ouvrez-la avec n'importe quelle bibliothèque et regardez le rectangle d'encre. S'il ne correspond pas à ce que dit votre fonction de mesure, vous avez un facteur d'échelle caché et tous vos ajustements sont biaisés dans le même sens.
Placer un bloc dont vous ignorez la hauteur
Un titre peut tenir sur une ligne ou sur trois, et le SVG ne sait pas additionner. Si vous fixez la position de la première ligne, un titre court laisse un trou énorme en dessous et un titre long vient buter sur le pied de page.
La solution consiste à calculer la ligne de base en PHP et à ancrer le bloc par le bas, de sorte que la dernière ligne tombe toujours sur la même règle. Et à exposer aussi l'endroit où le bloc se termine, pour que ce qui vient dessous s'y accroche au lieu d'une position fixe :
<g transform="translate(0 {{title_bottom}})">
<text x="80" y="52" font-size="{{subtitle_font_size}}">{{subtitle_tspans}}</text>
</g>
Ainsi, un sous-titre garde le même écart avec un titre d'une ligne et avec un titre de trois, et c'est ce qui fait qu'une collection de cartes paraît dessinée plutôt que générée.
Le fond est une couche comme une autre
C'est la partie qui ramène souvent les gens vers le navigateur : mettre une photo derrière le texte. En SVG, rien de spécial n'est nécessaire, car une image s'incorpore sous forme d'URI de données.
<image x="0" y="0" width="1200" height="630"
preserveAspectRatio="xMidYMid slice" href="data:image/jpeg;base64,..."/>
Et comme tout finit par être un fichier local, peu importe d'où il vient : une couleur unie, une photo d'Unsplash téléchargée et mise en cache, ou un PNG produit par un modèle d'image. Le gabarit ne change pas. Par-dessus vient un dégradé qui assombrit davantage le côté du texte que l'autre, et le titre se lit sur n'importe quelle photo.
Une couche optionnelle doit disparaître, pas rester vide
Celui-ci nous a valu une intégration continue au rouge et mérite d'être raconté, parce que c'est le genre de panne qui n'apparaît que sur la machine qui possède le binaire.
Si le gabarit porte toujours l'<image> du fond et que vous lui passez une chaîne vide en l'absence de photo, le SVG obtenu contient ceci :
<image x="0" y="0" width="1200" height="630" href="" xlink:href=""/>
Et librsvg avorte. Il ne renvoie pas d'erreur lisible : le processus meurt sur le signal 6, qui vous parvient au mieux sous la forme d'une exception étrange de votre bibliothèque de processus.
La leçon est de conception, pas de bibliothèque : « sans fond » ne peut pas vouloir dire « un fond sans source ». La couche optionnelle doit disparaître entièrement du gabarit, via une section conditionnelle, et non se rendre vide.
Les polices sont la panne qui ne prévient pas
rsvg-convert résout font-family via fontconfig : si votre typographie de marque n'est pas installée sur la machine, il bascule sur une autre sans rien dire. La carte se génère, rien n'échoue, et elle ne ressemble simplement pas au reste de votre site.
On peut installer la police dans l'image Docker, mais cela oblige à la reconstruire. La voie la moins gênante consiste à pointer fontconfig vers les polices que votre projet sert déjà :
<fontconfig>
<dir prefix="relative">../../public/fonts</dir>
<dir>/usr/share/fonts</dir>
</fontconfig>
Ce fichier remplace la configuration système au lieu de s'y ajouter, il faut donc lister aussi les répertoires du système, sinon vous vous retrouvez sans aucune autre police. Le prefix="relative" résout les chemins par rapport au fichier lui-même, ce qui le rend valable sur votre portable comme dans le conteneur.
Un détail que l'on oublie : mesurez avec la police avec laquelle vous rendez. Mesurer avec l'une et dessiner avec l'autre fausse l'ajustement sans que rien ne le signale.
Ne régénérer que ce qui a changé
Avec cinq cents cartes, tout régénérer à chaque déploiement est du temps perdu. Le réflexe est de sauter celles qui existent déjà, et cela marche jusqu'au jour où vous changez quelque chose d'imprimé dans une carte déjà présente sur le disque.
Cela nous est arrivé avec la date de publication : en déplaçant la date d'un article, sa carte continuait d'afficher l'ancienne parce que le fichier existait. La bonne réponse n'est pas de penser à la supprimer, c'est de conserver une empreinte de tout ce qui entre dans le dessin, le gabarit compris. Si vous modifiez le SVG et que l'empreinte ne le prend pas en compte, vous changez le design et rien ne se régénère.
Générez les cartes en local et versionnez-les. Le faire à la demande place un rendu sur le chemin critique d'une requête et une dépendance binaire dans le conteneur web, en échange de rien : une carte ne change que quand le contenu change, et cela vous le savez avant le serveur.
Ce qui existe sur le marché et en quoi cela diffère
Les options se distinguent par l'infrastructure qu'elles réclament en échange de la liberté de design qu'elles offrent.
Puppeteer ou Playwright sont la référence et vous maquettez en HTML, ce qui est le plus grand avantage possible pour ce type de solution. En échange, Node et Chromium partout.
Satori, de Vercel, convertit du JSX en SVG et c'est très bien fait, mais cela vit dans l'écosystème Node.
Intervention Image et GD en direct sont ce qu'on finit par utiliser en PHP. Ils conviennent si la carte est plate, et deviennent pénibles dès qu'il faut des couches, des dégradés ou des recadrages, car chaque élément devient un appel de code au lieu d'une balise.
Laracards, celui que nous maintenons, fait le pari du SVG plus un binaire : le design s'édite comme un vecteur et l'ajustement du texte est mesuré. Si vous avez déjà Node dans le projet et que maquetter en HTML vous arrange, Satori ou Puppeteer sont des choix parfaitement raisonnables.
Et si vous ne travaillez pas avec Laravel
Les critères ne dépendent pas du langage :
- Mesurez le texte pour de vrai et vérifiez la mesure contre le rendu. Compter les caractères déborde de la carte dès le premier titre large.
- Calculez hors du SVG tout ce qui demande de l'arithmétique. Un format vectoriel ne sait pas additionner, les lignes de base se résolvent donc avant.
- Qu'une couche optionnelle disparaisse plutôt que de rester vide. Un élément sans source, c'est la différence entre une image et un processus mort.
- Fixez les polices explicitement. Un repli silencieux ne casse rien et ruine votre cohérence de marque.
- Décidez de ce qui régénère une carte. Si c'est l'existence du fichier, le jour où vous changerez le gabarit, aucune ne changera.
Où est le code
Le paquet est sur Packagist, le code sur GitHub et la documentation du paquet couvre les gabarits, les fonds et la configuration. Il est sous licence MIT et ne dépend de rien d'autre que de GD et d'un binaire.
Les cartes de ce blog sont faites ainsi, et nous travaillons cela en développement Laravel et en conception web. Si vous partagez du contenu souvent et que vos liens sortent sans image ou avec une image générique, dites-nous comment vous publiez et nous vous dirons ce qui convient.