Création automatique d'images Open Graph pour le blog

- Read in english
Ressources: garabit

Partager du contenu sur les réseaux sociaux est l'une des stratégies les plus efficaces pour engager un public plus large. Lors de la promotion d'un article de blog, l'inclusion d'une image Open Graph (image OG) est cruciale pour améliorer l'attrait visuel de l'article. Ces images sont affichées lors du partage de liens sur des plateformes telles que Twitter, Facebook et LinkedIn.

Note

Une image OG est un PNG ou JPEG qui doit mesurer au moins 1200 x 630 pixels, peser moins de 5 Mo et être référencée dans la balise <meta property="og:image" content="..."> dans la section <head> de l'HTML en utilisant une URL absolue.

Créer une image OG pour chaque article de blog manuellement peut être chronophage. De plus, toute modification de design nécessite la mise à jour de toutes les images OG, ce qui est impossible sur le long terme. Il est essentiel d'être agile dans la mise à jour et l'évolution de notre blog sans y consacrer un temps excessif.

La théorie

Pour générer dynamiquement une image OG, nous devons d'abord créer un modèle SVG. Dans ce modèle, des espaces réservés représenteront le contenu tel que le titre de la page, la description et l'auteur. En utilisant une bibliothèque appropriée, nous convertirons ce modèle SVG en image PNG pour chaque page, en utilisant les espaces réservés désignés.

Ces images générées seront ajoutées au fichier .gitignore, garantissant qu'à chaque fois que nous générons le site, les images OG sont également créées. Pendant le développement, nous vérifions si une image OG existe déjà pour éviter des calculs inutiles.

Note

Cette approche a été inspirée par le blog d'Anthony Fu. Je l'ai adaptée à nos besoins.

Les étapes pour générer les images OG sont les suivantes :

  1. Créons un modèle SVG avec Figma (ou un autre outil de votre choix).
  2. Exportons deux versions : une avec du texte contour et une sans.
  3. Intégrons manuellement les deux modèles SVG, en préservant le texte contour tout en gardant les espaces réservés et les balises <text> de l'original.
  4. Implémentons une fonction dans VitePress pour créer dynamiquement l'image OG.

L'implémentation

Assez de théorie ; plongeons dans l'implémentation.

Créer le modèle SVG

Dans Figma, concevons un modèle d'image OG avec des dimensions de 1200 x 630 pixels. Définissons des espaces réservés pour le titre de l'article en utilisant , , et , chacun pouvant accueillir jusqu'à 30 caractères, permettant un total de 90 caractères pour la plupart des titres. N'oublions pas que les pratiques SEO découragent les titres dépassant 100 caractères.

Modèle d'image OG dans Figma
Modèle d'image OG dans Figma

Tip

Si vous n'êtes pas sûr de comment commencer, envisagez d'utiliser le modèle que j'ai créé pour ce projet et téléversez-le sur Figma.

Exportons deux fichiers SVG : un avec du texte contour et un autre sans (assurons-nous que l'option est décochée). Les deux exports doivent être des fichiers SVG.

Fusionner les deux modèles SVG

Après l'exportation, nous avons deux fichiers SVG qui doivent être fusionnés pour développer notre modèle.

Warning

C'est la partie la plus difficile. Continuez à lire avant de commencer à fusionner les fichiers SVG.

Le SVG plus petit contient un texte lisible, qui existe dans des balises <text>. Conservons ces balises <text> représentant nos espaces réservés :

xml
<text fill="black" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="60" font-weight="600" letter-spacing="0em"><tspan x="140" y="198.318">{{line1}}</tspan></text>
<text fill="black" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="60" font-weight="600" letter-spacing="0em"><tspan x="140" y="283.318">{{line2}}</tspan></text>
<text fill="black" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="60" font-weight="600" letter-spacing="0em"><tspan x="140" y="368.318">{{line3}}</tspan></text>

Ouvrons les deux fichiers SVG côte à côte pour simplifier le processus de fusion. Trouvons les balises <pathx> équivalentes dans le fichier plus grand, qui représentent le texte contour ou vectorisé.

Par exemple, le texte se transforme en le chemin :

xml
<pathx d="M141.982 180.358V176.629C144.396 176.629 146.08 176.139 172.602V179.761H357.958V172.602H364.712Z" fill="black"/>

Note

L'exemple de chemin est abrégé pour plus de clarté. Les chemins réels sont considérablement plus longs.

Gardons les fichiers ouverts simultanément, car l'ordre du texte correspond dans les deux. Remplaçons les balises <path> dans le deuxième fichier par les balises <text> du premier fichier. Conservons toutes les balises <path> non associées aux espaces réservés de texte.

Tip

Attribuez une couleur aux espaces réservés dans Figma pour les identifier facilement lors de la fusion. Vous pouvez mettre à jour la couleur dans le fichier SVG via la propriété fill.

Après la combinaison, optimisons le fichier SVG résultant en utilisant SVGO.

Ce modèle SVG affiné est désormais prêt à être utilisé dans notre projet VitePress. Enregistrons-le dans le répertoire .vitepress sous le nom og-template.svg.

xml
<!-- Partial SVG, DO NOT USE AS IS -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1200 630">
  <!-- Template elements -->
  <path fill="#FA8EFD" d="M0 0h1200v630H0z"/>
  <path fill="#000" d="M96 96h1040v363H96zm310 415h407v69H406z"/>
  <path fill="#FEEA35" d="M401 506h401v63H401z"/>
  <path stroke="#000" stroke-width="6" d="M401 506h401v63H401z"/>
  <path fill="#000" d="M451.415 534.119a4.504 4.504 0 0 0-.588-1.235 3.79 3.79 0 0 0-.887-.946 3.571 3.571 0 0 0-1.176-.589 4.917 4.917 0 0 0-1.449-.204c-.937 0-1.772.236-2.505.707-.733.472-1.31 1.165-1.73 2.08-.415.909-.623 2.017-.623 1.679c.375.733.56 1.622.554 2.668V546h-3.085v-7.858c0-.875-.227-1.56-.682-2.054-.449-.494-1.071-.741-1.866-.741-.54 0-1.02.119-1.441.358a2.477 2.477 0 0 0-.98 1.014c-.233.443-.349.98-.349 1.611Z"/>
  <path fill="#fff" d="M83 83h1034v357H83z"/>
  <path stroke="#000" stroke-width="6" d="M83 83h1034v357H83z"/>
  <!-- Text placeholders -->
  <text xml:space="preserve" fill="#000" font-family="Inter" font-size="60" font-weight="600" letter-spacing="0em" style="white-space:pre"><tspan x="140" y="198.318">{{line1}}</tspan></text>
  <text xml:space="preserve" fill="#000" font-family="Inter" font-size="60" font-weight="600" letter-spacing="0em" style="white-space:pre"><tspan x="140" y="283.318">{{line2}}</tspan></text>
  <text xml:space="preserve" fill="#000" font-family="Inter" font-size="60" font-weight="600" letter-spacing="0em" style="white-space:pre"><tspan x="140" y="368.318">{{line3}}</tspan></text>
</svg>

Générer l'image OG de manière dynamique

Pour maintenir l'organisation, ajoutons un fichier nommé genOg.ts dans le répertoire .vitepress. Ce script contient des fonctions nécessaires pour diviser le titre en trois lignes et générer l'image OG.

Tout d'abord, installons les dépendances nécessaires :

bash
pnpm add -D @types/fs-extra fs-extra sharp

Ensuite, créons la fonction pour la génération de l'image OG.

Les étapes à accomplir comprennent :

  1. Charger le modèle SVG.
ts
import { join } from 'node:path'
import fs from 'fs-extra'

const ogSvg = fs.readFileSync(join('.vitepress', 'og-template.svg'), 'utf-8')
  1. Vérifions si une image OG existe déjà ; générons-en une si elle est absente :
ts
if (fs.existsSync(output)) {
  // eslint-disable-next-line no-useless-return
  return
}
  1. Créons le répertoire pour les images OG si celui-ci n'existe pas :
ts
import { dirname } from 'node:path'
import fs from 'fs-extra'

await fs.mkdir(dirname(output), { recursive: true })
  1. Divisons le titre en trois lignes :
ts
const lines = title
  .trim()
  .split(/(.{0,30})(?:\s|$)/g)
  .filter(Boolean)

const data: Record<string, string> = {
  line1: lines[0],
  line2: lines[1],
  line3: lines[2],
}
  1. Remplacons les espaces réservés dans le modèle SVG par les lignes de titre :
ts
const svg = ogSvg.replace(/\{\{([^}]+)\}\}/g, (_, name) => data[name] || '')
  1. Transformons le modèle SVG en image PNG et stockons-le :
ts
import { Buffer } from 'node:buffer'
import sharp from 'sharp'

await sharp(Buffer.from(svg)).resize(1440, 810).png().toFile(output)

Voici la fonction complète :

ts
import { Buffer } from 'node:buffer'
import { dirname, join } from 'node:path'
import fs from 'fs-extra'
import sharp from 'sharp'

const ogSvg = fs.readFileSync(join('.vitepress', 'og-template.svg'), 'utf-8')

/**
 * @credit Anthony Fu, https://antfu.me
 * @link https://github.com/antfu/antfu.me/blob/main/vite.config.ts#L242
 */
export async function genOg(title: string, output: string) {
  // Skip if the file already exists
  if (fs.existsSync(output))
    return

  // Ensure the output directory exists
  await fs.mkdir(dirname(output), { recursive: true })

  // Break the title into lines of 30 characters
  const lines = title
    .trim()
    .split(/(.{0,30})(?:\s|$)/g)
    .filter(Boolean)

  const data: Record<string, string> = {
    line1: lines[0],
    line2: lines[1],
    line3: lines[2],
  }
  const svg = ogSvg.replace(/\{\{([^}]+)\}\}/g, (_, name) => data[name] || '')

  console.info(`Generating ${output}`)
  try {
    await sharp(Buffer.from(svg)).resize(1440, 810).png().toFile(output)
  }
  catch (e) {
    console.error('Failed to generate og image', e)
  }
}

Assigner des images OG à chaque article

Maintenant que nous avons une méthode fonctionnelle pour générer des images OG en utilisant un titre, il est crucial de déterminer quand et où cette fonction doit être utilisée.

La stratégie la plus simple consiste à faire fonctionner cette fonction chaque fois qu'un fichier est traité par VitePress dans transformPageData, après les ajouts de l'article précédent.

Tout d'abord, extrayons le nom de l'image OG en utilisant le chemin de fichier slugifié :

ts
import { defineConfig } from 'vitepress'

export default defineConfig({
  async transformPageData(pageData) {
    // ...

    const ogName = pageData.filePath
      .replaceAll(/\//g, '-')
      .replace(/\.md$/, '.png')
  }
})

Ici, un chemin de fichier tel que blog/learn-vitepress.md se transforme en blog-learn-vitepress.png. Cette transformation est sûre car les noms de fichiers Markdown sont déjà compatibles avec les URL et respectent les normes du système de fichiers.

Ensuite, invoquons la fonction :

ts
import { joinURL } from 'ufo'
import { defineConfig } from 'vitepress'
import { genOg } from './genOg'

export default defineConfig({
  async transformPageData(pageData, { siteConfig }) { // Attention, `{ siteConfig }` est un nouveau paramètre
    // ...

    await genOg(
      pageData.frontmatter.title || pageData.title || siteConfig.site.title,
      joinURL(siteConfig.srcDir, 'public', 'og', ogName),
    )
  }
})

Pour garantir une approche de secours, nous vérifions ces titres potentiels : le titre de la frontmatter, le titre de page déduit du premier h1, ou, si aucun des deux n'est présent, le titre du site. Cela empêche une image OG d'être vide. Stockons les images OG générées dans le répertoire public sous og pour éviter les conflits de noms, car ce dossier fusionne dans le dist.

Incluons og dans le fichier .gitignore car il ne contient que des sorties générées.

Incorporons des métadonnées

Enfin, ajoutons des métadonnées pour informer les réseaux sociaux de l'emplacement de l'image OG, en tirant parti des avancées de l'article SEO précédent.

ts
import { joinURL } from 'ufo'
import { defineConfig } from 'vitepress'

export default defineConfig({
  async transformPageData(pageData, { siteConfig }) {
    // ...

    // Integrate OG image URL into frontmatter
    pageData.frontmatter.head.push(
      [
        'meta',
        {
          property: 'og:image',
          content: joinURL(
            'https://garabit.barbapapazes.dev', // Please, change this before deploying
            'og',
            ogName,
          ),
        },
      ],
      [
        'meta',
        {
          name: 'twitter:image',
          content: joinURL(
            'https://garabit.barbapapazes.dev', // Please, change this before deploying
            'og',
            ogName,
          ),
        },
      ],
    )
  }
})

Une image OG pour chaque chemin

Ce guide a été attendu avec impatience car les images OG sont à la fois essentielles et laborieuses à produire. Les mécanismes ici, que j'ai employés pendant plusieurs mois, garantissent la régénération rapide des images OG lors des modifications de modèle. Au fil du temps, j'ai continuellement adapté le modèle pour répondre à des besoins évolutifs. En développant sur cette méthodologie, j'ai récemment incorporé un titre pour les titres de série suivant des techniques similaires.

Je ne peux que recommander cette approche simple, car elle a considérablement amélioré mon flux de travail. J'espère qu'elle fera de même pour vous.

Image OG de cet article
Image OG de cet article