Le texte, c'est la partie facile : le pipeline qui illustre les articles rédigés par IA

Générer le texte est un problème résolu. L'illustrer ne l'est pas. Le pipeline de bout en bout — prompt routeur, recherche de photos, diagrammes Mermaid, graphiques vérifiés, et le texte alternatif, le crédit et le balisage ImageObject qui en font du SEO.

Partager
Un programmeur tapant à un bureau avec deux écrans remplis de code, éclairé par des lampes colorées.
Photo via Unsplash

Vous êtes développeur avec un objectif de contenu : des dizaines d'articles par mois, parfois des centaines. Le modèle de rédaction gère le brouillon, le plan, la meta description, les liens internes. Puis le pipeline arrive à la seule étape qui n'a pas de modèle propre — les images — et s'arrête. Pas parce que les images sont difficiles à obtenir, mais parce que rien dans la chaîne ne sait laquelle choisir, d'où, avec quelle licence, décrite comment.

Cet article est cette étape manquante, câblée de bout en bout : quel type de visuel produire pour quel type de section, le prompt qui transforme un brouillon terminé en briefs de recherche, l'appel API qui renvoie les photos, le second modèle qui dessine ce qu'une photographie ne peut pas dire, et l'assembleur qui émet un balisage que Google peut réellement lire. Deux workflows complets à la fin, prêts à être repris.

Le texte est résolu. L'illustration est là où ça bloque.

Faites l'audit honnête d'un pipeline d'articles automatisé. Plan : résolu. Brouillon : résolu. Titre, meta description, schéma, liens internes, traduction : résolu, tout par le même modèle, tout en texte. Ensuite :

Étape du pipeline Statut Ce qui bloque réellement
Plan & brouillon Résolu Un appel modèle, un prompt
Titres, meta, schéma, liens Résolu Du texte en entrée, du texte en sortie
Image hero Bloqué Nécessite un vrai fichier, une licence, des dimensions et un texte alt — rien de tout cela qu'un modèle de texte ne peut produire
Images de section Bloqué Trois à cinq par article, chacune différente, aucune répétée sur tout le site
Graphiques & schémas Bloqué Doit être exact — le seul visuel qu'une recherche ne peut pas renvoyer et qu'un générateur ne doit pas inventer

L'échec n'est pas esthétique, il est structurel : l'article sort avec un placeholder générique, ou avec la même photo que les douze précédents, ou avec une image générée dont la main à six doigts est la première chose que voit un lecteur. Et l'image n'est pas de la décoration — la documentation de Google sur les images le dit clairement : le texte alt « est l'attribut le plus important pour fournir des métadonnées sur une image », et la recommandation est d'utiliser de vrais éléments <img> avec un alt descriptif plutôt que des arrière-plans CSS, pour que l'image puisse être trouvée et comprise.1

Quatre types de visuel, quatre types de modèle

La plus grande erreur de conception consiste à traiter « l'image » comme un seul problème avec un seul fournisseur. Ce sont en réalité quatre problèmes, et le routeur entre eux est une ligne de prompt, pas un service :

La section a besoin de… Le produire avec Pourquoi pas les autres
Une scène du monde réelhero, situations humaines, lieux, objets, gestes Recherche photo sémantique (Pexafy) Un générateur invente les détails ; un graphique n'a rien à tracer
Des chiffres que vous possédez réellementbenchmarks, tarifs, résultats d'enquête, latence Un modèle qui écrit du code de tracé, exécuté dans un sandbox On ne peut pas faire confiance à un modèle d'image pour une valeur ; une photo ne peut pas porter de données
Une structure ou un fluxarchitecture, séquence, machine à états Un modèle qui écrit du Mermaid / Graphviz, rendu de façon déterministe Les banques de photos gratuites n'ont aucun schéma de votre système
Votre produit à l'écrandocs, changelog, tutoriels Une capture d'écran de navigateur scriptée (Playwright) Rien d'autre ne peut montrer une interface qui n'existe que dans votre build

Et l'illustration générée ? Elle conserve un rôle légitime : la scène qui ne peut être photographiée et qui n'est pas une donnée — un mécanisme abstrait, un produit qui n'existe pas encore, un style d'illustration maison qui vous appartient. (L'argumentaire complet en faveur de la photographie réelle plutôt que de la génération — la rapidité en volume, la précision, le problème de l'uniformité — est développé ici.) Un indicateur de la direction prise : depuis le 2 août 2026, l'article 50 de l'AI Act européen impose aux fournisseurs de systèmes génératifs de marquer les sorties synthétiques dans un format lisible par machine.2 Il s'agit d'une obligation qui pèse sur les fournisseurs et déployeurs d'IA, pas d'une règle sur ce qu'un blog peut publier — mais c'est pourquoi la provenance de l'image en tête de votre article devient de plus en plus une chose que le lecteur peut vérifier plutôt qu'accepter sur parole.

Le pipeline, de bout en bout

Cinq étapes. Seule l'étape 3 touche une API d'images, et seule l'étape 4 est optionnelle :

La forme générale — un article en entrée, un article publiable en sortie
┌─ 1. WRITE ────────────────────────────────────────────────────┐
   topic ──▶ LLM ──▶ draft.md  (h2 sections, front-matter)
└───────────────────────────────┬───────────────────────────────┘
┌─ 2. BRIEF ────────────────────┴───────────────────────────────┐
   draft.md ──▶ LLM ──▶ { hero: {...}, sections: [ {...} ] }
                     un seul objet JSON : par emplacement, un kind +
                     soit un brief caméra, soit une spec de données/schéma
└──────────┬──────────────────────────────────┬─────────────────┘
           │ kind = "photo"                │ kind = "chart" | "diagram"
           ▼                               ▼
┌─ 3. SEARCH ───────────────┐   ┌─ 4. DRAW (optional) ─────────┐
   GET /search/photos          LLM ──▶ mermaid | plotting code
   ← credited photo +           ──▶ sandbox ──▶ .svg / .png
     w/h, blur_hash, alt,       (rendu déterministe, aucun
     licence, source URL         chiffre inventé)
└──────────┬────────────────┘   └──────────────┬───────────────┘
           └───────────────┬──────────────────┘
┌─ 5. ASSEMBLE ─────────────┴───────────────────────────────────┐
   <img> avec width/height + fetchpriority | loading
   alt écrit pour un humain · crédit visible · ImageObject JSON-LD
   photo_id stocké pour qu'aucune page ne partage un hero
└───────────────────────────────────────────────────────────────┘

Deux propriétés comptent plus que le schéma. L'étape 2 est un routeur : elle décide, par emplacement, quel producteur intervient, si bien que vous ne demandez jamais à une banque de photos un graphique en barres. Et l'étape 5 est là où se joue le SEO — tout ce que Google documente sur les images (alt descriptif, métadonnées de licence, un hero sûr pour le LCP) est émis ici, à partir de champs que la réponse de recherche portait déjà.

Étape 2 : transformer le brouillon en briefs visuels

Un seul appel de modèle sur le brouillon terminé, et ce qu'il retourne est un plan plutôt qu'une requête. Comment rédiger un brief photo unique — pourquoi le titre de l'article est la pire entrée possible, à quoi ressemble un brief caméra de 12 à 25 mots, et ses modes d'échec — est exposé en détail dans le guide sur l'illustration d'un article, et n'est pas repris ici. Ce qu'un pipeline ajoute, c'est le routage : le même appel doit décider, emplacement par emplacement, quel producteur s'exécute — et une entrée de type graphique porte des données là où une entrée photo porte une scène.

Le prompt routeur — à copier tel quel
# prompt système — exécuté une fois par brouillon terminé
Vous êtes le directeur artistique d'une publication technique. Lisez l'article et
renvoyez le plan visuel : une entrée pour le hero, une par section H2.

Pour chaque entrée, choisissez exactement un kind :
  "photo"    une scène réelle : quelqu'un fait quelque chose quelque part, un lieu,
              un objet, un geste. Le choix par défaut pour les heroes.
  "chart"    la section énonce des chiffres qui figurent DANS l'article. N'inventez
              jamais de valeurs : copiez-les dans data, mot pour mot.
  "diagram"  la section décrit une structure, un flux ou une séquence.
  "none"     la section est courte, ou porte déjà un bloc de code.

Règles pour les entrées "photo" — le champ est query :
   Rédigez un descriptif de prise de vue : une scène qu'une caméra aurait pu
   capturer, de 12 à 25 mots, en anglais, correspondant à l'ambiance de la
   section. Nommez ce qui figure dans le cadre, jamais le sujet. Pas de texte,
   de logos, de marques ni de personnes célèbres ; pas de métaphores invisibles.
   (Règles complètes, avec exemples et cas d'échec :
   pexafy.com/blog/illustrate-blog-articles-at-scale/)

N'écrivez PAS le texte alt d'une entrée photo : l'image que vous recevrez est la
correspondance la plus proche du brief, pas la scène que vous avez décrite, donc son alt
doit être rédigé à partir de la photo choisie. Les entrées chart et diagram portent
bien un alt — là, vous contrôlez exactement ce qui est rendu.

Renvoyez uniquement du JSON :
{
  "hero": { "kind": "photo", "query": "…", "orientation": "landscape" },
  "sections": [
    { "h2": "…", "kind": "photo",   "query": "…" },
    { "h2": "…", "kind": "chart",   "title": "…", "unit": "ms",
      "data": [ {"label": "…", "value": 0} ], "alt": "…" },
    { "h2": "…", "kind": "diagram", "spec": "flowchart LR; …", "alt": "…" }
  ]
}

La ligne kind fait l'essentiel du travail, et l'instruction data fait le reste : une entrée de type graphique ne peut porter que des nombres déjà présents dans le brouillon, si bien que le modèle transcrit plutôt qu'il n'invente. Voici le routeur appliqué à trois sections réelles d'un article destiné aux développeurs :

Section kind Ce que le routeur a renvoyé
Hero — « Pourquoi notre build nocturne prend 40 minutes » photo « un développeur qui travaille tard à un bureau avec deux écrans et un clavier mécanique, dans une pièce sombre éclairée par les écrans »
« Où passe réellement le temps » chart data copié du paragraphe : installation 480 s, compilation 1080 s, tests 720 s, upload 120 s
« Comment nous découpons le graphe » diagram flowchart LR du graphe de dépendances des jobs
« Ce que nous avons changé, et ce que nous referions » photo « deux ingénieurs debout devant un tableau blanc couvert de schémas, réfléchissant ensemble à un problème »

Étape 3 : les photos reviennent créditées

Chaque entrée kind: "photo" correspond à une requête. Le brief hero ci-dessus, exécuté contre l'API publique, renvoie ceci en 147 ms :

GET /search/photos — brief hero · « un développeur qui travaille tard à un bureau avec deux écrans… » · 147 ms
Le moteur classe par sens, si bien que la phrase longue restreint l'ensemble au lieu de le vider. Lancez cette recherche exacte →

Le brief de la dernière section, une scène complètement différente, en 144 ms :

GET /search/photos — brief de section · « deux ingénieurs debout devant un tableau blanc couvert de schémas… » · 144 ms
Même article, même exécution, une scène que personne ne confondrait avec le hero — parce que le brief a été rédigé par section, pas par article. Lancez celle-ci aussi →

Ce qui revient par photo est ce qui rend l'étape 5 possible — pas seulement un fichier :

Un résultat, réduit aux champs que l'assembleur consomme
{
  "photo_id":  "019e1c7f-0063-759e-b498-33ce1714e6c9",   // à stocker : pas de répétition
  "urls": { "small": "…?w=400", "regular": "…?w=1080",
             "large": "…?w=1920" },
  "width": 3000, "height": 1688,          // → pas de décalage de mise en page
  "blur_hash": "LJ8gjv9rVq-6OFxanNNFI7xco$Na",   // → vrai placeholder
  "alt_description": "Person types on keyboard in front of dual monitors…",
  "photographer_full_name": "Jakub Żerdzicki",   // → ImageObject.creator
  "source": "Unsplash", "license_type": "free",
  "source_image_url": "https://unsplash.com/photos/…",
  "attribution": { "html": "<span…>Photo by …</span>",
                    "plain": "Photo by Jakub Żerdzicki on Unsplash (…)" }
}

Étape 4 : ce qu'une photographie ne peut pas dire

Deux emplacements du plan ne sont pas recherchables, et c'est précisément là qu'un second modèle trouve sa place — pas pour dessiner une image, mais pour écrire le code qui la dessine. La distinction compte : le code est révisable, déterministe et ne peut pas halluciner la hauteur d'une barre.

Schémas : du texte en entrée, du SVG en sortie

Mermaid rend des schémas à partir d'une définition en texte brut,3 ce qui en fait la cible la plus sûre pour un modèle : la sortie est inspectable, comparable dans git, et se rend de la même manière à chaque fois. Le routeur a déjà renvoyé la spec.

diagram.sh — le modèle a écrit la spec, la CLI la rend
# le champ "spec" d'une entrée diagram, écrit dans build/graph.mmd
cat build/graph.mmd
flowchart LR
  install["install deps · 480s"] --> compile["compile · 1080s"]
  compile --> test["test suite · 720s"]
  compile --> upload["upload artifacts · 120s"]

npx -y @mermaid-js/mermaid-cli -i build/graph.mmd -o static/img/graph.svg
# → un SVG que vous pouvez relire dans la PR, pas une image à croire sur parole

Graphiques : uniquement des chiffres que l'article contient déjà

Même principe, un garde-fou supplémentaire. Le routeur a copié les valeurs depuis le brouillon ; le modèle écrit le code de tracé ; le code s'exécute dans un sandbox ; l'assembleur revérifie les valeurs rendues contre les chiffres source avant que le graphique ne soit autorisé à approcher une page.

chart.py — tracer les données transcrites, puis les vérifier
import re, matplotlib
matplotlib.use("Agg")                # sans interface : pas d'affichage en CI
import matplotlib.pyplot as plt

def assert_in_draft(value, draft: str) -> None:
    """Un chiffre tracé doit apparaître dans l'article EN TANT QUE NOMBRE."""
    # Le piège ici, c'est la correspondance de sous-chaîne : "120" est dans "1200",
    # et dans "?w=1200" — un `str(v) in draft` naïf passe sur n'importe quoi.
    # On matche sur des frontières de mot, et on accepte 1 234 / 1,234 / 1234.
    body = re.sub(r"(?<=\d)[  ,](?=\d{3}\b)", "", draft)   # retire les séparateurs
    if not re.search(rf"(?<![\d.]){re.escape(str(value))}(?![\d.])", body):
        raise ValueError(f"{value} is not stated in the article — refusing to plot")

def render_chart(entry: dict, draft: str, out: str) -> str:
    labels = [d["label"] for d in entry["data"]]
    values = [d["value"] for d in entry["data"]]
    for v in values:
        assert_in_draft(v, draft)       # valeur hallucinée → pas de graphique

    fig, ax = plt.subplots(figsize=(8, 4.5), dpi=160)
    ax.barh(labels, values)
    ax.set_xlabel(entry["unit"])
    ax.set_title(entry["title"])
    fig.tight_layout()
    fig.savefig(out)                    # artefact déterministe, révisable
    return out

Le garde-fou est court, mais écrivez-le soigneusement : une vérification par sous-chaîne ne fonctionne pas. "120" in draft est vrai pour un article qui contient 1200 ou ?w=1200, si bien que la version naïve passe sur tout et ne protège rien. Ancrez sur des frontières de chiffres, normalisez les séparateurs de milliers, et la classe d'erreur qui fait démonter un article technique dans les commentaires — un graphique contredisant son propre paragraphe — ne peut plus atteindre la production. Les captures d'écran suivent le même principe : un page.screenshot() scripté contre votre vrai build est la seule source de vérité pour votre propre interface, et elle reste vraie au fil des évolutions de l'interface.

Étape 5 : c'est dans l'assemblage que se joue le SEO

Tout ce qui précède a produit des fichiers et des champs. Cette étape les transforme en balisage — et il vaut mieux être précis ici, car trois comportements documentés se décident dans ces quelques lignes.

Le hero, émis depuis la réponse de recherche — rien d'inventé
<!-- Les octets viennent d'une autre origine : payez la poignée de main tôt -->
<link rel="preconnect" href="https://images.unsplash.com" crossorigin>

<!-- Élément LCP : jamais lazy, toujours priorité haute -->
<figure>
  <img src="{urls.regular}"
       width="{width}" height="{height}"        <!-- élimine le décalage de mise en page -->
       alt="{alt}"                                <!-- écrit APRÈS le choix -->
       fetchpriority="high" decoding="async"
       style="background:{color_hex}">   <!-- couleur dominante, 1 champ -->
  <figcaption>{attribution.html}</figcaption>
</figure>

<!-- Images de section, sous la ligne de flottaison : les réglages inverses -->
<img src="{urls.regular}" width="{width}" height="{height}"
     alt="{alt}" loading="lazy" decoding="async">

Hotlink ou réhéberger ? L'extrait ci-dessus fait du hotlink, ce qui est le plus rapide à mettre en production et la raison du preconnect : un hero distant coûte une résolution DNS et une poignée de main TLS sur le chemin critique, ce qui peut annuler le gain que vous venez d'acheter avec fetchpriority. Réhéberger supprime entièrement l'origine tierce, permet de servir de l'AVIF/WebP à vos propres points de rupture, et survit à un changement d'URL en amont — au prix du stockage, d'une étape de récupération dans le pipeline et de votre propre facture CDN. Quel que soit votre choix, color_hex donne un placeholder pour un champ (un blur hash est plus joli, mais il doit d'abord être décodé en URI de données — ce n'est pas une couleur CSS). Copier un hero brut directement dans background: est là où la plupart des pipelines livrent discrètement une boîte grise vide.

  1. Ne jamais mettre le hero en lazy-load. web.dev est sans ambiguïté : « Ne mettez jamais votre image LCP en lazy-load, cela entraînera toujours un délai de chargement de ressource inutile et aura un impact négatif sur le LCP », et il recommande fetchpriority="high" sur l'élément susceptible d'être le LCP — utilisé avec parcimonie, sur une seule image. 4 Un pipeline qui applique loading="lazy" sur toutes les images, hero compris, est la blessure Core Web Vitals auto-infligée la plus courante en publication automatisée.
  2. Émettez toujours width et height — ils reviennent dans la réponse, donc aucune excuse ; cette seule paire d'attributs est ce qui permet au navigateur de réserver l'espace et empêche la mise en page de sauter. Utilisez blur_hash comme placeholder pendant le chargement du fichier.
  3. Écrivez l'alt après le choix, jamais avant. C'est le point subtil. Le champ alt du plan décrit la scène demandée ; la photo obtenue est la correspondance la plus proche, pas cette scène. Livrer le texte du brief comme alt est exactement l'échec d'accessibilité que ce pipeline est censé éviter — une description d'une image qui n'est pas sur la page. Construisez l'alt à partir de l'alt_description de la photo choisie, affiné en fonction du paragraphe dans lequel elle s'insère. La recommandation de Google est de « se concentrer sur la création de contenu utile et riche en informations, qui utilise les mots-clés de manière appropriée et dans le contexte du contenu de la page », et elle prévient que bourrer les attributs alt de mots-clés « entraîne une mauvaise expérience utilisateur et peut faire percevoir votre site comme du spam ».1

La partie que presque personne n'automatise : les métadonnées de licence

Google prend en charge les données structurées ImageObject pour la licence des images. Cela requiert contentUrl plus au moins un des champs creator, creditText, copyrightNotice ou license, recommande acquireLicensePage, et les images avec des informations de licence deviennent éligibles au badge Licensable dans Google Images.5 Chacun de ces champs est déjà présent dans la réponse de recherche — l'émettre relève donc d'un template, pas d'un projet :

ImageObject JSON-LD, rempli depuis la réponse de l'API
<script type="application/ld+json">
{
  "@context": "https://schema.org/",
  "@type": "ImageObject",
  "contentUrl": "{urls.large}",                 // requis
  "creator": { "@type": "Person",
                "name": "{photographer_full_name}" },
  "creditText": "{photographer_full_name} on {source}",
  "license": "{LICENSE_URL[source]}",           // la propre page de la banque
  "acquireLicensePage": "{source_image_url}"     // la page de la photo
}
</script>

# LICENSE_URL fait correspondre le champ `source` à la licence qui régit
# réellement la photo — unsplash.com/license, pexels.com/license, pixabay.com/…

Un détail qui vaut la peine d'être bien fait : license doit pointer vers la licence qui régit cette photo — la propre page de licence de la banque source — pas une page récapitulative sur votre domaine. Google la lit pour décider de l'éligibilité au badge, et une URL auto-référencée est à la fois un signal plus faible et difficile à défendre autrement que comme un lien vers vous-même. Conservez votre propre résumé de licence comme page interne pour les lecteurs ; placez l'URL canonique dans le balisage.

Et tant que vous êtes dans l'assembleur : donnez au fichier un nom court et descriptif plutôt que IMG_0042.jpg, et ajoutez l'image à un sitemap — le format de sitemap d'images de Google accepte jusqu'à 1 000 images par URL de page.6 Les deux tiennent chacun en une ligne dans un pipeline et aucun des deux n'est jamais fait à la main.

Deux pipelines à reprendre

Mêmes cinq étapes, deux formes très différentes — l'une pour les articles que vous rédigez, l'autre pour la documentation qui doit correspondre à un produit en fonctionnement. Choisissez celui dont vous reconnaissez le mode d'échec.

1 · Le blog de développeurs en CI — Markdown dans le dépôt

Les articles vivent en Markdown, les images sont commitées à côté, et tout tourne au push. Déterministe, révisable dans la PR, aucune dépendance à l'exécution envers une quelconque API :

.github/workflows/illustrate.yml
on: { pull_request: { paths: ["content/**.md"] } }

jobs:
  illustrate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: python plan_to_pr.py $(git diff --name-only origin/main -- 'content/*.md')
        env:
          PEXAFY_API_KEY: ${{ secrets.PEXAFY_API_KEY }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
      - uses: peter-evans/create-pull-request@v6   # les images arrivent dans la PR
        with: { commit-message: "chore(content): illustrate" }

Un humain approuve toujours la PR, et c'est là tout l'intérêt : le pipeline propose, le relecteur dispose, et le front-matter qu'il a écrit est comparable ligne à ligne.

La partie recherche tient en un seul GET — la requête, son score_threshold et les champs qu'elle retourne sont détaillés ligne par ligne dans le guide sur l'article unique, ils sont donc importés ici plutôt que réimprimés. Ce que ce fichier ajoute, c'est tout ce dont un plan a besoin et qu'une seule photo n'a pas : la nouvelle tentative sur un brief qui n'a rien retourné, la réservation d'un photo_id pour qu'aucune page ne partage une image avec une autre, et le texte alternatif rédigé à partir de la photo obtenue.

plan_to_pr.py — la colle : le plan visuel en entrée, le front-matter en sortie
import frontmatter
from photo_search import search   # un seul GET /search/photos ; ignore les ids présents dans `used`

def find_photo(entry: dict, used: set) -> dict | None:
    """Cherche ; si le brief était trop spécifique, l'élargit une fois, puis abandonne."""
    orientation = entry.get("orientation", "landscape")
    for query in (entry["query"], widen(entry["query"])):
        photo = search(query, orientation, used)
        if photo:
            used.add(photo["photo_id"])   # réservée : pas de répétition sur le site
            return photo
    return None                        # l'appelant décide : ignorer l'emplacement, ou échouer

def widen(query: str) -> str:
    """Retire la dernière clause — généralement la trop spécifique."""
    return query.rsplit(" in ", 1)[0] if " in " in query else query

def illustrate(path: str, plan: dict, used: set) -> None:
    post = frontmatter.load(path)
    hero = find_photo(plan["hero"], used)
    if hero is None:                    # pas de hero vaut mieux qu'un mauvais
        raise SystemExit(f"{path}: no photo above threshold — rewrite the brief")

    post["hero"] = {                    # tout ce dont le template a besoin
        "src": hero["urls"]["regular"], "w": hero["width"], "h": hero["height"],
        # L'alt décrit la photo REÇUE, jamais la scène demandée.
        "alt": alt_for(hero, section=post.get("title", "")),
        "bg": hero["color_hex"], "credit": hero["attribution"]["html"],
        "creator": hero["photographer_full_name"], "id": hero["photo_id"],
        "source": hero["source"],
        "source_url": hero["source_image_url"],   # → acquireLicensePage
    }
    open(path, "w").write(frontmatter.dumps(post))

def alt_for(photo: dict, section: str) -> str:
    """alt_description comme base, réduit à ~125 caractères pour les lecteurs d'écran.
    Repassez-le par le modèle avec le paragraphe si vous voulez mieux."""
    base = photo.get("alt_description") or photo.get("description", "")
    return base[:125].rstrip(" ,;")

2 · Docs & changelog — captures d'écran d'abord, photos en dernier

Inversez les valeurs par défaut du routeur. Dans la documentation produit, le visuel honnête est presque toujours votre propre interface : un script Playwright qui ouvre le vrai build, fixe un viewport précis et capture exactement l'état que décrit le paragraphe. Les schémas couvrent les pages d'architecture, et les photographies n'apparaissent que sur les pages conceptuelles et de landing — là où une capture d'écran ne dirait rien.

shots.py — la capture d'écran est générée, jamais décrite
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 1440, "height": 900},
                            device_scale_factor=2)   # net comme du retina
    page.goto("http://localhost:3000/dashboard")
    page.get_by_role("button", name="New API key").click()
    page.screenshot(path="static/img/docs/new-api-key.png")
    browser.close()
# Tourne dans le même job CI que le build de la doc → la capture d'écran ne peut
# jamais décrire une version de l'interface qui n'existe plus.

Deux variantes qui méritent d'être nommées mais pas leur propre recette. Le SEO programmatique inverse la boucle : avec des centaines de pages générées depuis une base de données, vous ne cherchez pas par page — une requête renvoie jusqu'à 100 photos, donc vous cherchez par groupe thématique et assignez depuis un pool, avec une contrainte d'unicité qui gère la déduplication (cette architecture, en détail). Et les newsletters et cartes sociales ont besoin de la même photo en quatre recadrages : conservez le photo_id, demandez la taille dont vous avez besoin dans urls, et ne recherchez à nouveau que quand un recadrage échoue réellement.

Le même pipeline, comme un seul agent

Si un modèle rédige déjà le brouillon, le chemin le plus court consiste à lui donner directement l'outil de recherche plutôt que de faire circuler du JSON entre processus. Pexafy exploite un serveur Model Context Protocol hébergé à l'adresse mcp.pexafy.com/mcp. La configuration du connecteur et les outils qu'il expose sont traités ailleurs — la configuration bureau et éditeur dans le guide sur un article unique, la variante headless pour un runner CI dans l'article sur le contenu à grande échelle, et à quoi ressemble le marché plus large des connecteurs — qui propose un serveur d'images MCP, à quelles conditions — dans l'étude de l'infrastructure de recherche d'images pour les agents IA. Ce qui mérite d'être montré ici, c'est ce qui arrive à ce pipeline une fois que l'agent dispose des outils : il ne s'agit plus de cinq étapes mais d'une seule instruction.

Une instruction, tout le plan exécuté
Vous  Voici le brouillon. Construis le plan visuel, illustre-le, et ouvre une PR.
     Les graphiques uniquement à partir de chiffres déjà présents dans le texte.

Agent  → plan : hero=photo · §2=chart · §3=diagram · §4=photo
      → search_photos(q="a developer working late at a desk with two
         monitors and a mechanical keyboard in a dark room…")
      ← 16 photos · 147 ms · sélectionnée #1, 3000×1688, créditée
      → mermaid-cli build/graph.mmd → static/img/graph.svg
      → chart.py §2 → 4 valeurs vérifiées contre le brouillon ✓
      → search_photos(q="two engineers standing at a whiteboard…")
      ← 16 photos · 144 ms · sélectionnée #1

      ✓ 4 emplacements remplis · 2 appels API · alt + crédit + ImageObject écrits
      ⚠ §5 n'a rien renvoyé au-dessus de 0.5 — brief trop abstrait, réécrit en
        "a person at a kitchen table checking figures on a laptop"

Cette dernière ligne explique pourquoi un agent vaut la peine dans cette boucle plutôt qu'un simple script : le mode d'échec de l'illustration automatisée est un mauvais brief, et réécrire un mauvais brief est exactement ce à quoi sert un modèle de langage. Gardez les parties déterministes — l'assignation, la déduplication, le garde-fou numérique — dans le code.

Ce que coûte un article illustré

Trois emplacements photo par article signifient trois requêtes de recherche, si bien que le plan gratuit (5,000 requêtes/mois) couvre 1,666 articles par mois avant même que la question de payer se pose — et si vous mutualisez les recherches par groupe thématique plutôt que par article, ce plafond gagne encore un ordre de grandeur. La ventilation plan par plan, et l'architecture de mutualisation qui la rend accessoire, se trouvent dans l'article compagnon sur l'illustration de contenu à grande échelle.

Les deux appels modèle par article — un pour le brouillon, un pour le plan visuel — représentent quelques milliers de tokens et seront la ligne la moins chère du pipeline ; les rendus Mermaid et matplotlib ne coûtent rien à part des secondes de CI. Le chiffre qui surprend les gens, c'est la ligne qui n'est pas bon marché : générer quatre images par article, quatre cents images par mois, plus les tentatives qui n'ont pas été retenues — et le résultat ne porte toujours aucun photographe, aucune date et aucune URL source.

Ce que ce pipeline ne règle pas

Une section honnête, car l'échec qu'elle prévient est coûteux. Un article bien illustré reste un article : une vraie photographie, des graphiques exacts et un balisage correct améliorent une page qui mérite d'exister. Ils ne font pas se positionner un contenu léger produit en masse. Les politiques anti-spam de Google nomment l'abus de contenu à grande échelle — générer de nombreuses pages principalement pour manipuler le classement et offrant peu de valeur aux utilisateurs, que l'automatisation soit impliquée ou non — et la qualité des illustrations n'entre pas dans ce jugement.7

Le cadre qui tient donc la route : ce pipeline est un plancher de qualité que vous contrôlez, appliqué à des pages qui ont déjà une raison d'être publiées. Là où il paie démonstrablement :

  • Une provenance qu'un lecteur peut vérifier. Une mention de crédit avec un vrai photographe et une URL source est une affirmation vérifiable — et ces mêmes champs alimentent le balisage ImageObject que Google lit.
  • Accessibilité et Core Web Vitals. Un vrai texte alt, des dimensions sur chaque image, un hero jamais en lazy-load. Multipliez par chaque article publié et c'est ça qui fait la qualité d'image du site.
  • Une exactitude vérifiable. Un graphique dont les chiffres sont vérifiés contre l'article, une capture d'écran générée depuis le build en cours d'exécution, un schéma révisable comme du texte dans la PR — trois visuels qui ne peuvent pas dériver de la vérité sans qu'un test échoue.

Sur l'attribution, le point propre à ce pipeline est étroit : l'argument en faveur de l'affichage systématique du crédit est développé ailleurs, et ce qu'un assembleur automatisé ajoute, c'est que la même chaîne attribution qu'il affiche est le creditText dont les données structurées ont besoin. Un seul champ, deux emplacements, émis dans la même passe de template — ce qui fait qu'un pipeline a encore moins d'excuse qu'un humain de l'omettre.

Par où commencer

  1. Ajoutez le prompt routeur à ce qui rédige déjà vos brouillons, et affichez le JSON sans agir dessus. Lisez dix plans. Si les briefs nomment des sujets plutôt que des scènes, corrigez le prompt avant d'écrire la moindre intégration.
  2. Câblez uniquement les emplacements photo. Un GET /search/photos par brief, et stockez photo_id dès le premier jour — une déduplication rétrofittée sur 3 000 pages en production est une migration, pas une colonne.
  3. Émettez width, height et alt dans le même commit. C'est le travail Core Web Vitals le moins cher que vous ferez jamais.
  4. Ajoutez ensuite l'étape 4, les schémas avant les graphiques — Mermaid est du texte, donc c'est celui qui n'a aucun mode d'échec au-delà d'une erreur de syntaxe.
  5. Ajoutez le garde-fou numérique avant que le premier graphique n'atteigne un lecteur, pas après.

Références & notes de bas de page

1 Google Search Central, Bonnes pratiques SEO pour les images : le texte alt est « l'attribut le plus important pour fournir des métadonnées sur une image » ; la recommandation est de créer « du contenu utile et riche en informations, qui utilise les mots-clés de manière appropriée et dans le contexte du contenu de la page », d'éviter les attributs alt bourrés de mots-clés, d'utiliser des éléments HTML <img> plutôt que des images CSS, et de donner aux fichiers des noms courts mais descriptifs.

2 AI Act de l'UE, article 50 — obligations de transparence applicables à partir du 2 août 2026 : les fournisseurs de systèmes générant des images, audio, vidéo ou texte synthétiques doivent marquer les sorties dans un format lisible par machine et les rendre détectables comme générées artificiellement. Cela lie les fournisseurs et déployeurs d'IA ; ce n'est pas une règle sur les images qu'un site web peut publier.

3 Mermaid rend des schémas et graphiques à partir de définitions textuelles inspirées du Markdown, ce qui rend la sortie révisable et déterministe.

4 web.dev, Optimiser le Largest Contentful Paint : « C'est une bonne idée de définir fetchpriority="high" sur un élément <img> si vous pensez qu'il sera probablement l'élément LCP de votre page », utilisé avec parcimonie ; et « Ne mettez jamais votre image LCP en lazy-load, cela entraînera toujours un délai de chargement de ressource inutile et aura un impact négatif sur le LCP. »

5 Google Search Central, Métadonnées d'image (données structurées) : ImageObject requiert contentUrl plus au moins un des champs creator, creditText, copyrightNotice ou license ; acquireLicensePage est recommandé, et les images portant des informations de licence peuvent devenir éligibles au badge Licensable dans Google Images.

6 Google Search Central, Sitemaps d'images : les sitemaps d'images informent Google des images d'un site, y compris celles trouvées via JavaScript, et acceptent jusqu'à 1 000 images par URL de page.

7 Politiques anti-spam de Google Search — abus de contenu à grande échelle : générer de nombreuses pages principalement pour manipuler le classement et offrant peu de valeur aux utilisateurs, qu'elles soient créées par automatisation, effort humain ou une combinaison des deux.

Foire aux questions

Comment illustrer automatiquement des articles rédigés par un LLM ?
Ajoutez un appel de modèle entre la rédaction et la publication : demandez-lui de renvoyer un plan visuel — une entrée par emplacement d'image, chacune étiquetée comme photo, graphique, diagramme ou rien. Les entrées photo portent une description de 12 à 25 mots d'une scène qu'un appareil photo aurait pu capturer, que vous envoyez à une API de recherche sémantique d'images (GET /api/v1/search/photos, environ 150 ms). Les entrées graphique et diagramme sont envoyées à un modèle qui écrit du code de tracé ou du Mermaid, rendu de façon déterministe. Ne transmettez jamais le titre de l'article à une recherche d'images : les titres sont abstraits et aucune photographie ne les représente.
Un site de documentation doit-il utiliser des captures d'écran ou des photos de stock ?
Inversez les valeurs par défaut du routeur : dans la documentation produit, le visuel honnête est presque toujours votre propre interface. Un script Playwright qui ouvre le build réel, fixe le viewport et capture l'état exact décrit par le paragraphe s'exécute dans le même job CI que le build de la documentation, si bien qu'une capture d'écran ne peut jamais montrer une version de l'interface qui n'existe plus. Les diagrammes portent les pages d'architecture, et les photographies n'apparaissent que sur les pages conceptuelles et les pages d'atterrissage, où une capture d'écran ne dirait rien.
Comment empêcher un pipeline IA d'afficher de mauvais chiffres dans un graphique ?
Faites en sorte que le plan retranscrive plutôt qu'invente : le routeur ne peut copier que des valeurs déjà présentes dans le brouillon dans le champ data de l'entrée graphique. Vérifiez-le ensuite par assertion dans le code avant le rendu — pour chaque valeur, contrôlez que sa chaîne apparaît dans l'article, sinon levez une erreur. Neuf lignes, et un graphique qui contredit son propre paragraphe ne peut jamais atteindre un lecteur.
Quel balisage d'image un pipeline automatisé doit-il produire pour le SEO ?
Trois éléments, tous issus de champs déjà présents dans la réponse de recherche. Un alt descriptif rédigé dans le contexte du paragraphe — Google considère le texte alternatif comme la métadonnée d'image la plus importante et déconseille le bourrage de mots-clés. Des attributs width et height sur chaque image, avec fetchpriority="high" sur le visuel principal et jamais loading="lazy" sur celui-ci, car l'image LCP ne doit pas être chargée paresseusement. Et des données structurées ImageObject avec contentUrl, creator, creditText et license, ce qui rend une image éligible au badge Licensable dans Google Images.
Illustrer des articles rédigés par IA aide-t-il leur classement ?
Pas à soi seul, et il convient d'être précis. Les règles anti-spam de Google définissent l'abus de contenu à grande échelle comme la génération de nombreuses pages principalement pour manipuler le classement, avec peu de valeur pour les utilisateurs, que l'automatisation soit ou non impliquée ; les illustrations ne changent rien à cette évaluation. Ce qu'un bon pipeline apporte, c'est un plancher de qualité sur des pages qui méritent déjà d'exister : une provenance vérifiable, un texte alternatif accessible, des Core Web Vitals qui survivent à l'automatisation, et des graphiques et captures d'écran qui ne peuvent pas s'écarter de la réalité.
Comment exécuter l'étape d'illustration en CI sans perdre le contrôle éditorial ?
Déclenchez le job sur une pull request touchant vos fichiers de contenu, laissez-le écrire les images et le front-matter, et faites-lui ouvrir une pull request plutôt que de commiter directement sur la branche — le pipeline propose, un humain approuve, et chaque champ qu'il a écrit est comparable via un diff. Gardez trois choses déterministes dans le code plutôt que dans le modèle : la déduplication par photo_id, l'assertion que chaque chiffre tracé apparaît dans l'article, et un échec strict lorsqu'aucune photo ne franchit le seuil de score. Aucune image principale vaut mieux qu'une mauvaise.

Ne cherchez plus de mots-clés. Décrivez ce que vous avez en tête.

Recherchez parmi 9M+ images libres de droits par le sens — dans n'importe quelle langue, en moins de 100 ms.