Hier c'était LE grand jour : la publication de notre premier article[1] sur le blog !!
Explosion de confettis !
Sauf que… en le partageant sur les réseau, j'ai réalisé que j'avais complètement oublié d'intégrer des images pour openGraph !!
Rien de bien grave, rien d'essentiel, mais bon c'est quand même plus sympa le petit aperçu avec une vignette.
Et comme à chaque fois : c'est une invitation à suivre le lapin dans son terrier[2]
En effet, en me replongeant dans la doc des openGraph et en cherchant à définir un visuel par défaut (pour les articles auxquels je n'arrive pas à associer de visuels pertinents), je réalise que plusieurs questions/défis m'attendent :
- Image par défaut : utiliser mon visuel de favicon, sachant que les openGraph ne supportent pas les images SVG 😖
- Trouver la taille et le ratio d'image parfait
- Fournir une alternative textuelle ? 🤔
Utiliser un SVG comme image OpenGraph
Je souhaite utiliser comme vignette le même visuel bien compact que celui que j'ai utilisé comme favicon, sauf qu'il s'agit d'un SVG et que les openGraph ne supportent pas ce format. Zut et flûte !
Générer une image au format adapté à partir d'une source SVG
Mais je ne vais pas en rester là. Avec Eleventy on sait traduire des SVG en image, c'est d'ailleurs ce que fait le plugin eleventy-plugin-gen-favicons(S'ouvre dans un nouvelle fenêtre)… Je pourrais réutiliser les images générées pour celui-ci mais les tailles proposées pour des favicons risquent d'être insuffisantes pour une vignette OpenGraph…
Je vais donc m'inspirer de ce que j'ai fait pour les images responsives et créer un shortcode.
Ce sera l'occasion d'isoler toute la logique des solutions de repli et éviter de surcharger mon fichier base.html qui commence à devenir assez gros (j'ai même envisagé de faire un super-shortcode intégrant toute la logique des OpenGraph, mais je ne voulais pas non plus me retrouver avec une procession de paramètres à transmettre à mon shortcode)
Retouches sur les favicon et galères avec RenderTemplate
Comme je compte réutiliser ce fichier à plusieurs endroits, j'ai décidé d'en faire une constante du site en l'intégrant dans mon fichier _data/base.js. En théorie il suffit ensuite de remplacer dans l'appel en dur dans le shortcode :
{% favicons base.logo.compact, appleIconBgColor="transparent" %}
Sauf que… patatra, ça ne marche pas ! La cause ? J'ai mis longtemps à la trouver mais en fait c'est dû au fait que ce shortcode ne marche correctement qu'avec le langage de templating Nunjucks(S'ouvre dans un nouvelle fenêtre), langage que je n'utilise pas par défaut (je lui ai préféré le langage Liquid[3])
Du coup j'ai dû utiliser le shortcode spécial RenderTemplate afin d'appeler du Nunjucks en plein milieu d'un document écrit avec du Liquid.
Et ce shortcode a un défaut : il ne transmet pas les données de la cascade au contenu du shortcode. Il faut définir manuellement mes données lors de l'appel du shortcode RenderTemplate :
{% renderTemplate "njk", base %}
{% set faviconUrl = logo.compact %}
{% favicons faviconUrl, appleIconBgColor="transparent" %}
{% endrenderTemplate %}
Information
Explications
Je passe "base" en paramètre de la balise RenderTemplate, ce qui a pour effet de charger les données de _data/base.js directement à la racine des données disponible au sein de la balise. Résultat, au lieu d'écrire base.logo.compact comme je le ferais ailleurs dans mon template HTML, dans cette balise RenderTemplate j'écris : logo.compact
Nouveau shortcode "og-image"
Pour ce shortcode, je reprend la base de mon shortcode "responsive-img" et je la simplifie :
// In /eleventy.config.js
import pluginOgImage from "./shortcodes/og-image.js";
export default async function(eleventyConfig) {
eleventyConfig.addPlugin(pluginOgImage);
}
// In /shortcodes/og-image.js
import path from "node:path";
import eleventyImage from "@11ty/eleventy-img";
export default function(eleventyConfig) {
const imageDefaultWidths= ["280"]
eleventyConfig.addShortcode("og-image", async function (src){
let imgHTML = ""
let imgPath = src
// call the API to transform my SVG source
let imgModel = await eleventyImage(imgPath, {
widths: imageDefaultWidths,
formats: ["png"],
outputDir: "./__site/_src/img/renditions/",
urlPath: "/_src/img/renditions/",
filenameFormat: function (id, src, width, format, options) {
const extension = path.extname(src);
const name = path.basename(src, extension);
return `${name}-${width}w.${format}`;
}
})
// generate the HTML code to insert
imgHTML = `<meta property="og:img" content="https://www.div-agations.fr${imgModel.png[0].url}">`
return imgHTML
})
}
Cette première itération est très naïve :
- il faut que je déclare le path complet de l'image à utiliser à chaque appel du shortcode (pas de variable par défaut)
- le nom de domaine du site est intégré en dur
- je ne donne aucune indication supplémentaire sur l'image (j'en parle plus tard mais les openGraph recommandent/supportent un certain nombre de données additionnelles sur l'image associée)
- Dans mon dossier
_srcfinal, l'image openGraph générée est mélangée avec les autres images responsives
Astuce
Je n'attend qu'une seule image
J'appelle directement l'url dans imgModel car je n'attend qu'une seule entrée dans l'objet retourné par l'API, n'ayant demandé qu'un seul format d'image, avec une seule largeur possible.
Pour information, voici la réponse de l'API avec ces paramètres :
{
png: [
{
format: 'png',
width: 280,
height: 280,
url: '/_src/img/renditions/favicon-280w.png',
sourceType: 'image/png',
srcset: '/_src/img/renditions/favicon-280w.png 280w',
filename: 'favicon-280w.png',
outputPath: '__site/_src/img/renditions/favicon-280w.png',
size: 10125
}
]
}
Intégrer des valeurs par défaut
Pour pouvoir définir une image openGraph par défaut et arrêter d'écrire des noms de domaine en dur, il faut que je récupère dans mon shortcode les constantes de mon site (celles stockées dans /_data/base.js) :
import siteConst from "../_data/base.js"
Je peux maintenant remplacer le nom de domaine écrit en dur dans le code et définir une image par défaut :
let imgPath = siteConst.logo.compact
if (src) {
imgPath = src
}
…
imgHTML = `<meta property="og:img" content="${siteConst.url + imgModel.png[0].url}">`
Finaliser cette première itération
Il ne me reste plus qu'à un peu mieux ranger mes assets openGraph (en changeant les options outputDir et urlPath) et adresser la question du format et de la transparence.
Sur ce dernier point, il faut que je prenne en compte que si j'utilise une image jpg, il n'est pas très utile de convertir ça en png (moins bonne compression et un support inutile de la transparence)
Pour ça il faut que j'analyse le format du fichier original (par flemme, je vais me baser sur l'extension de fichier) et que je module le format de sortie en fonction :
let sourceFormat = "svg"
if (src) {
sourceFormat = src.match(/\.(\w+)$/)[1]
imgPath = src
}
let outputFormat = "png"
switch (sourceFormat) {
case "jpeg":
case "jpg":
outputFormat = "jpg";
break;
case "svg":
case "png":
default:
outputFormat = "png";
break;
}
// call the API to transform my SVG source
let imgModel = await eleventyImage(imgPath, {
…
formats: [outputFormat],
…
})
imgHTML = `<meta property="og:img" content="${siteConst.url + imgModel[outputFormat][0].url}">`
Avertissement
Les limites de mon implémentation naïve
Là, avec cette implémentation, plus rien ne marche, j'ai cassé mon blog 😱 !
Je pense bien avoir passé une heure à chercher en vain l'origine du bug avant de comprendre : le problème est dans la réponse de l'API
Pour les images sans transparence, je lui demande des "jpg", mais l'API ne range pas pour autant sa réponse dans une propriété imgModel.jpg. Les réponse sont organisées en fonction du MIME type et non de l'extension. Donc peut importe que je réclame un format "jpg", "jpeg" ou "JPEG"… la réponse sera toujours rangée dans imgModel.jpeg.
Il faut donc que je traite un peu la réponse de l'API avant de l'intégrer dans mon HTML. Plutôt que de faire un mapping extension de fichier / MIME type, je choisi de tabler sur le fait que je ne demande toujours qu'une seule image pour openGraph :
let outputEnum = Object.keys(imgModel)
if(outputEnum.length == 1) {
let outputType = outputEnum[0] // Expects only one type of file
let outputFile = imgModel[outputType][0] // Expects only one file per type (only one width requested)
// generate the HTML code to insert
imgHTML = `<meta property="og:img" content="${siteConst.url + outputFile.url}">`
} else {
throw new Error(
`og-image: Wrong number (${outputEnum.length}) of images generated for: ${imgPath}
Generated formats: ${outputEnum}
--> og:image meta tag won't be generated.`)
}
Trouver la taille parfaite pour tous les réseaux
Bon mes première me recherches semblent indiquer que je faisais fausse route avec ma favicon : aujourd'hui les réseaux semblent privilégier un format plutôt "bannière" en 1,91:1 (donc quasi 2x plus large que haut)…
- https://myogimage.com/blog/og-image-size-meta-tags-complete-guide
- https://www.ogimage.gallery/libary/the-ultimate-guide-to-og-image-dimensions-2024-update
Les visuels que j'ai créé pour le logo dans le header seraient probablement plus proche de l'attendu, mais elles sont prévues pour s'adapter au changement de mode (clair/sombre), option qui n'est pas proposée avec OpenGraph. Il va donc falloir que je travaille une version spécifique openGraph, qui marche quelque soit le contexte…
Ou alors j'abandonne la notion de bannière par défaut et j'accepte que mes partages d'articles sur les réseaux sociaux seront plus… discrets.
Je n'ai de toute façon pas le temps de faire plus aujourd'hui (je répond à l'appel du BBQ en famille)
Je m'abstient pour l'instant de publier le code du jour : mieux vaut pas d'image OpenGraph que de mauvaises images OpenGraph 😉
Alternative textuelle ou non ? Telle est la question 🤔
Sur l'accessibilité et le fait de fournir ou non une alternative textuelle, je suis aussi en pleine hésitation : je ne voudrais pas surcharger les utilisateurs avec des informations redondantes, sachant que normalement mes partages devraient être toujours accompagnés d'un titre et d'une description pertinente…
Cet article (Why we don’t set the og:image:alt tag – Yoast(Opens in a new window)) semble aller dans mon sens, mais il faut encore que j'y réfléchisse plus en détails, et peut-être que je ressorte Alt decision tree(Opens in a new window) de la W3C.
Raison de plus pour ne pas publier tout ça tout de suite. Je vais laisser passer le WE et me remettrai dessus Lundi.
Je ne compte pas mes petites entrées de journal, que j'écris dans mon coin et publie directement ici, sans relecture… ↩︎
Référence au terrier du lapin dans "Alice au Pays des merveilles"… Bon, ça sonne mieux en anglais : "Down the rabbit hole!" ↩︎
Je ne l'ai pas choisi pour sa supériorité mais simplement par facilité : c'est le langage par défaut d'Eleventy et il est légèrement moins verbeux (donc plus léger d'usage au quotidien) ↩︎