Quand on partage un lien sur Mastodon, Bluesky, LinkedIn ou dans une messagerie, c'est l'image og:image qui s'affiche. Donc si on veut avoir quelque chose de sympa, obligé de trouver une image, qui n'a pas forcément de sens, ou alors une image par défaut... J'ai fini par mettre en place la génération d'une image automatique avec mon logo et le titre de l'article, quand je n'ai pas mieux à proposer.
Le principe : une cascade à trois niveaux
Pour chaque contenu, l'image de partage est résolue dans cet ordre :
- Un champ image « Image Opengraph » dédié, présent sur tous mes types de contenus : si je veux une image de partage spécifique, je la charge, elle prime sur tout (recadrée en 1200×630 par un style d'image)
- Sinon, l'image du contenu (le champ image classique de l'article)
- Sinon, en fallback une image est générée automatiquement en PHP : logo du site à gauche, titre du contenu et tags à droite, dans la charte graphique du site.
Pourquoi 1200×630 ?
C'est le format recommandé par Facebook et LinkedIn (ratio 1,91:1) et exploité tel quel par Twitter/X et Bluesky en carte « large ». En dessous de 600×315, certaines plateformes dégradent l'affichage en vignette. 1200×630 est visiblement le standard de fait, autant générer directement à cette taille.
La génération : GD, deux fontes TTF et un logo PNG
Pas de service externe, pas d'API tierce, pas de navigateur headless : la génération repose sur GD + FreeType, disponibles sur n'importe quel hébergement PHP correct. Le module embarque trois assets :
- les fontes du site en TTF (IBM Plex Sans pour le titre, JetBrains Mono pour le reste
- le logo en PNG transparent, rendu une fois pour toutes depuis le SVG du thème avec ImageMagick.
La composition reprend les tokens CSS du thème (fond #121214, accent #ff9900, etc.) :
protected const COLORS = [
'bg' => [0x12, 0x12, 0x14],
'accent' => [0xFF, 0x99, 0x00],
'text' => [0xE8, 0xE8, 0xEA],
'text_soft' => [0x9A, 0x9A, 0xA2],
'tag_bg' => [0x1D, 0x17, 0x10],
'tag_border' => [0x3D, 0x2E, 0x14],
];La colonne de gauche reçoit le logo, séparé par un filet vertical. La colonne de droite empile trois éléments :
- une ligne de « prompt » pour rappeler la charte du site :
kgaut@kgaut.net:~$ article - le titre, avec une taille de fonte adaptative : on part de 50 px, on mesure chaque ligne avec
imagettfbbox(), et on descend par pas de 4 px tant que le titre ne tient pas sur 4 lignes - les tags
Le cœur du découpage de texte tient en quelques lignes :
protected function wrapText(string $text, string $font, int $size, int $max_width): array {
$lines = [];
$current = '';
foreach (preg_split('/\s+/u', $text) as $word) {
$candidate = $current === '' ? $word : $current . ' ' . $word;
if ($current !== '' && $this->textWidth($font, $size, $candidate) > $max_width) {
$lines[] = $current;
$current = $word;
}
else {
$current = $candidate;
}
}
if ($current !== '') {
$lines[] = $current;
}
return $lines;
}Un seul token metatag pour toute la cascade
Côté Drupal, la config metatag ne connaît qu'un seul token custom, déclaré dans le module du site via hook_token_info() / hook_tokens() :
og_image: '[node:kgaut-og-image]'
og_image_url: '[node:kgaut-og-image]'
og_image_secure_url: '[node:kgaut-og-image]'
twitter_cards_image: '[node:kgaut-og-image]'Le token fait référence à un service qui implémente la cascade :
public function resolveUrl(NodeInterface $node): ?string {
foreach (self::IMAGE_FIELDS as $field_name => $style_id) {
if (!$node->hasField($field_name) || $node->get($field_name)->isEmpty()) {
continue;
}
$file = $node->get($field_name)->referencedEntities()[0] ?? NULL;
if ($file instanceof FileInterface) {
return ImageStyle::load($style_id)->buildUrl($file->getFileUri());
}
}
// Aucune image : on la génère.
$uri = $this->generate($node);
return $uri ? $this->fileUrlGenerator->generateAbsoluteString($uri) : NULL;
}Cache par hash de contenu
Générer une image à chaque affichage serait un petit peu lourd... Chaque image générée est écrite une fois pour toutes dans public://og-generated/, avec un nom de fichier qui contient un hash du contenu de l'image :
$hash = substr(hash('sha256', serialize([self::VERSION, $title, $tags, $prompt])), 0, 12);
$uri = self::DIRECTORY . '/' . $node->id() . '-' . $hash . '.png';
if (file_exists($uri)) {
return $uri;
}Les conséquences sont élégantes :
- titre ou tags modifiés → hash différent → l'image se régénère toute seule au prochain rendu ;
- rien ne change → le fichier existant est resservi, coût quasi nul ;
- la constante
VERSIONpermet d'invalider tout le stock d'images quand je fais évoluer le rendu des images.
La génération est en « Lazy » : c'est le premier rendu de la page (souvent le crawler du réseau social lui-même) qui déclenche la création du fichier.
Commentaires (0)