Tekst Is het Makkelijke Deel: de Pipeline die AI-Geschreven Artikelen Illustreert

Tekst genereren is opgelost. Het illustreren ervan niet. De end-to-end pipeline — router prompt, foto zoeken, Mermaid diagrammen, geverifieerde grafieken, en de alt-tekst, credit en ImageObject markup die er SEO van maken.

Een programmeur die typt aan een bureau met twee monitoren vol code, verlicht door gekleurde lampen.
Foto via Unsplash

Je bent een developer met een contentdoel: tientallen artikelen per maand, misschien honderden. Het schrijfmodel regelt het concept, de outline, de meta description, de interne links. Dan botst de pijplijn op de ene stap die geen eigen model heeft — de afbeeldingen — en valt stil. Niet omdat afbeeldingen moeilijk te verkrijgen zijn, maar omdat niets in de stack weet welke afbeelding, waarvandaan, met welke licentie, hoe omschreven.

Dit artikel is die ontbrekende stap, end-to-end bedraad: welk type visual je voor welk type sectie produceert, de prompt die een afgerond concept omzet in zoekbriefings, de API-aanroep die de foto's teruglevert, het tweede model dat tekent wat een foto niet kan zeggen, en de assembler die markup genereert die Google daadwerkelijk kan lezen. Twee complete werkwijzen aan het einde, klaar om over te nemen.

Tekst is opgelost. Illustratie is waar het vastloopt.

Voer de eerlijke audit uit van een geautomatiseerde artikelpijplijn. Outline: opgelost. Concept: opgelost. Titel, meta description, schema, interne links, vertaling: opgelost, allemaal door hetzelfde model, allemaal in tekst. Dan:

Pijplijnstap Status Wat er daadwerkelijk blokkeert
Outline & concept Opgelost Eén modelaanroep, één prompt
Titels, meta, schema, links Opgelost Tekst erin, tekst eruit
Hero-afbeelding Geblokkeerd Vereist een echt bestand, een licentie, afmetingen en alt-tekst — geen daarvan kan een tekstmodel produceren
Sectie-afbeeldingen Geblokkeerd Drie tot vijf per artikel, elk anders, geen herhaling op de site
Grafieken & diagrammen Geblokkeerd Moeten kloppen — de ene visual die een zoekopdracht niet kan opleveren en een generator niet mag verzinnen

Het falen is niet esthetisch, het is structureel: het artikel verschijnt met een stockplaceholder, of met dezelfde foto als de laatste twaalf, of met een gegenereerde afbeelding waarvan de hand met zes vingers het eerste is wat een lezer ziet. En de afbeelding is geen decoratie — Google's eigen documentatie over afbeeldingen zegt het onomwonden: alt-tekst “is het belangrijkste attribuut als het gaat om het verstrekken van metadata voor een afbeelding”, en het advies is om echte <img>-elementen te gebruiken met beschrijvende alt in plaats van CSS-achtergronden, zodat de afbeelding überhaupt gevonden en begrepen kan worden.1

Vier soorten visuals, vier soorten modellen

De grootste ontwerpfout is “de afbeelding” behandelen als één probleem met één aanbieder. Het zijn er vier, en de router daartussen is een regel prompt, geen dienst:

De sectie heeft nodig… Produceer het met Waarom niet de andere
Een scène uit de echte wereldhero, menselijke situaties, plekken, objecten, gebaren Semantische fotozoekmachine (Pexafy) Een generator verzint de details; een grafiek heeft niets om te plotten
Cijfers die je al hebtbenchmarks, prijzen, enquêteresultaten, latency Een model dat plotcode schrijft, uitgevoerd in een sandbox Een beeldmodel kan niet vertrouwd worden met een waarde; een foto kan geen data dragen
Een structuur of een flowarchitectuur, sequentie, statusmachine Een model dat Mermaid / Graphviz schrijft, deterministisch gerenderd Gratis fotobibliotheken hebben geen diagram van jouw systeem
Jouw product op het schermdocumentatie, changelog, tutorials Een gescripte browserscreenshot (Playwright) Niets anders kan een UI tonen die alleen in jouw build bestaat

En gegenereerde illustratie? Die houdt één eerlijke plek open: de scène die niet gefotografeerd kan worden en geen data is — een abstract mechanisme, een product dat nog niet bestaat, een huisstijl-illustratie die je zelf bezit. (Het volledige pleidooi voor echte fotografie boven generatie — snelheid op schaal, nauwkeurigheid, het gelijkvormigheidsprobleem — wordt hier gevoerd.) Zet dit tegenover de richting van de regelgeving: sinds 2 augustus 2026 verplicht Artikel 50 van de EU AI Act aanbieders van generatieve systemen om synthetische output te markeren in een machineleesbaar formaat.2 Dat is een verplichting voor AI-aanbieders en -implementeerders, geen regel over wat een blog mag publiceren — maar het verklaart waarom de herkomst van de afbeelding boven je artikel steeds vaker iets is dat een lezer kan controleren in plaats van op vertrouwen aannemen.

De pijplijn, end-to-end

Vijf fasen. Alleen fase 3 raakt een beeld-API, en alleen fase 4 is optioneel:

De vorm ervan — één artikel erin, één publiceerbaar artikel eruit
┌─ 1. WRITE ────────────────────────────────────────────────────┐
   topic ──▶ LLM ──▶ draft.md  (h2 sections, front-matter)
└───────────────────────────────┬───────────────────────────────┘
┌─ 2. BRIEF ────────────────────┴───────────────────────────────┐
   draft.md ──▶ LLM ──▶ { hero: {...}, sections: [ {...} ] }
                     one JSON object: per slot, a kind +
                     either a camera brief or a data/diagram spec
└──────────┬──────────────────────────────────┬─────────────────┘
           │ 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,       (deterministic render, no
     licence, source URL         invented numbers)
└──────────┬────────────────┘   └──────────────┬───────────────┘
           └───────────────┬──────────────────┘
┌─ 5. ASSEMBLE ─────────────┴───────────────────────────────────┐
   <img> with width/height + fetchpriority | loading
   alt written for a human · visible credit · ImageObject JSON-LD
   photo_id stored so no two pages share a hero
└───────────────────────────────────────────────────────────────┘

Twee eigenschappen zijn belangrijker dan het diagram. Fase 2 is een router: die bepaalt per slot welke producent draait, zodat je nooit een fotobibliotheek om een staafdiagram vraagt. En fase 5 is waar de SEO zit — alles wat Google documenteert over afbeeldingen (beschrijvende alt, licentiemetadata, een LCP-veilige hero) wordt hier gegenereerd, uit velden die het zoekantwoord al meebracht.

Fase 2: het concept omzetten in visuele briefings

Eén modelaanroep op het afgeronde concept, en wat die teruggeeft is een plan in plaats van een query. Hoe je één fotobrief schrijft — waarom de artikeltitel de slechtst mogelijke input is, hoe een 12-tot-25-woorden cambrief eruitziet, en op welke manieren die faalt — staat volledig uitgewerkt in de gids voor het illustreren van één artikel, en wordt hier niet herhaald. Wat een pipeline toevoegt is routering: dezelfde aanroep moet, plek voor plek, beslissen welke producent draait — en een grafiekentry draagt data waar een fotoentry een scène draagt.

De routerprompt — kopieer hem zoals hij is
# system prompt — run once per finished draft
You are the art director of a technical publication. Read the article and
return the visual plan: one entry for the hero, one per H2 section.

For each entry choose exactly one kind:
  "photo"    a real scene: someone doing something somewhere, a place,
              an object, a gesture. The default for heroes.
  "chart"    the section states numbers that are IN the article. Never
              invent values: copy them into data, verbatim.
  "diagram"  the section describes a structure, a flow or a sequence.
  "none"     the section is short, or already carries a code block.

Rules for "photo" entries — the field is query:
   Schrijf een camerabrief: een scène die een camera had kunnen vastleggen, 12 tot 25 woorden,
   in het Engels, passend bij de sfeer van de sectie. Benoem wat er in beeld is,
   nooit het onderwerp. Geen tekst, logo's, merken of bekende personen; geen onzichtbare
   metaforen. (Volledige regels, met voorbeelden en mislukte gevallen:
   pexafy.com/blog/illustrate-blog-articles-at-scale/)

Do NOT write the alt text of a photo entry: the picture you get back is the
closest match to the brief, not the scene you described, so its alt has to be
written from the chosen photo. Chart and diagram entries DO carry an alt —
there you control exactly what is rendered.

Return JSON only:
{
  "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": "…" }
  ]
}

De regel kind doet het meeste werk, en de instructie data doet de rest: een grafiekentry mag alleen cijfers dragen die al in het concept voorkomen, zodat het model overschrijft in plaats van verzint. Hier is de router toegepast op drie echte secties van een developerartikel:

Sectie kind Wat de router teruggaf
Hero — “Waarom onze nachtelijke build 40 minuten duurt” photo “a developer working late at a desk with two monitors and a mechanical keyboard in a dark room lit by the screens”
“Waar de tijd daadwerkelijk naartoe gaat” chart data overgenomen uit de paragraaf: installatie 480 s, compileren 1080 s, testen 720 s, uploaden 120 s
“Hoe we de graaf opsplitsen” diagram flowchart LR van de jobafhankelijkheidsgraaf
“Wat we veranderd hebben, en wat we opnieuw zouden doen” photo “two engineers standing at a whiteboard covered in diagrams working through a problem together”

Fase 3: de foto's komen gecrediteerd terug

Elk item met kind: "photo" is één verzoek. De heroBriefing hierboven, uitgevoerd tegen de publieke API, levert dit op in 147 ms:

GET /search/photos — hero brief · “a developer working late at a desk with two monitors…” · 147 ms
De engine rangschikt op betekenis, dus de lange zin versmalt de set in plaats van hem leeg te maken. Voer deze exacte zoekopdracht uit →

De laatste sectiebriefing, een volledig andere scène, in 144 ms:

GET /search/photos — section brief · “two engineers standing at a whiteboard covered in diagrams…” · 144 ms
Hetzelfde artikel, dezelfde run, een scène die niemand met de hero zou verwarren — omdat de briefing per sectie is geschreven, niet per artikel. Voer ook deze uit →

Wat per foto terugkomt, is wat fase 5 mogelijk maakt — niet alleen een bestand:

Eén resultaat, teruggebracht tot de velden die de assembler gebruikt
{
  "photo_id":  "019e1c7f-0063-759e-b498-33ce1714e6c9",   // bewaar het: geen herhalingen
  "urls": { "small": "…?w=400", "regular": "…?w=1080",
             "large": "…?w=1920" },
  "width": 3000, "height": 1688,          // → geen layout shift
  "blur_hash": "LJ8gjv9rVq-6OFxanNNFI7xco$Na",   // → echte 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 (…)" }
}

Fase 4: wat een foto niet kan zeggen

Twee slots in het plan zijn niet doorzoekbaar, en dit is precies waar een tweede model zich bewijst — niet om een afbeelding te tekenen, maar om code te schrijven die het tekent. Het onderscheid is belangrijk: code is controleerbaar, deterministisch en kan geen staafhoogte hallucineren.

Diagrammen: tekst erin, SVG eruit

Mermaid rendert diagrammen vanuit een platte-tekstdefinitie,3 wat het het veiligste doelwit maakt voor een model: de output is inspecteerbaar, diffbaar in git, en rendert elke keer op dezelfde manier. De router leverde de spec al aan.

diagram.sh — het model schreef de spec, de CLI rendert hem
# het "spec"-veld van een diagramitem, weggeschreven naar 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
# → een SVG die je in de PR kunt beoordelen, geen afbeelding die je moet vertrouwen

Grafieken: alleen cijfers die het artikel al bevat

Zelfde principe, één extra beveiliging. De router heeft de waarden uit het concept overgenomen; het model schrijft de plotcode; de code draait in een sandbox; de assembler controleert de gerenderde waarden opnieuw tegen de brontcijfers voordat de grafiek bij een pagina in de buurt mag komen.

chart.py — plot de overgenomen data, controleer het dan
import re, matplotlib
matplotlib.use("Agg")                # headless: geen display in CI
import matplotlib.pyplot as plt

def assert_in_draft(value, draft: str) -> None:
    """Een geplot getal moet in het artikel voorkomen ALS GETAL."""
    # Substring-matching is hier de valkuil: "120" zit in "1200", en
    # in "?w=1200" — een naïeve `str(v) in draft` slaagt op van alles.
    # Match op woordgrenzen, en accepteer 1 234 / 1,234 / 1234.
    body = re.sub(r"(?<=\d)[  ,](?=\d{3}\b)", "", draft)   # scheidingstekens verwijderen
    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)       # gehallucineerde waarde → geen grafiek

    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)                    # deterministisch, controleerbaar artefact
    return out

De beveiliging is kort, maar schrijf hem zorgvuldig: een substring-check werkt niet. "120" in draft is waar voor een artikel dat 1200 of ?w=1200 bevat, dus de naïeve versie slaagt op alles en beschermt niets. Anker op cijfergrenzen, normaliseer duizendtalscheidingstekens, en de foutcategorie die een technisch artikel in de reacties uit elkaar laat halen — een grafiek die zijn eigen paragraaf tegenspreekt — kan de productie niet bereiken. Screenshots volgen hetzelfde principe: een gescripte page.screenshot() tegen jouw echte build is de enige bron van waarheid voor je eigen UI, en die blijft waar terwijl de UI verandert.

Fase 5: assemblage is waar de SEO zit

Alles tot nu toe produceerde bestanden en velden. Deze fase zet ze om in markup — en het loont om hier precies te zijn, want drie gedocumenteerde gedragingen worden in deze paar regels besloten.

De hero, gegenereerd uit het zoekantwoord — niets verzonnen
<!-- The bytes come from another origin: pay the handshake early -->
<link rel="preconnect" href="https://images.unsplash.com" crossorigin>

<!-- LCP element: never lazy, always high priority -->
<figure>
  <img src="{urls.regular}"
       width="{width}" height="{height}"        <!-- kills layout shift -->
       alt="{alt}"                                <!-- written AFTER the pick -->
       fetchpriority="high" decoding="async"
       style="background:{color_hex}">   <!-- dominant colour, 1 field -->
  <figcaption>{attribution.html}</figcaption>
</figure>

<!-- Section images, below the fold: the opposite settings -->
<img src="{urls.regular}" width="{width}" height="{height}"
     alt="{alt}" loading="lazy" decoding="async">

Hotlinken of zelf hosten? Het fragment hierboven hotlinkt, wat het snelst te implementeren is en de reden voor de preconnect: een externe hero kost een DNS-lookup en een TLS-handshake op het kritieke pad, en dat kan de winst opeten die je net kocht met fetchpriority. Zelf hosten verwijdert de derdepartij-origin volledig, laat je AVIF/WebP serveren op je eigen breakpoints, en overleeft een wijzigende upstream-URL — ten koste van opslag, een fetch-stap in de pijplijn en je eigen CDN-rekening. Wat je ook kiest, color_hex geeft je een placeholder voor één veld (een blur hash is mooier, maar moet eerst worden gedecodeerd naar een data-URI — het is geen CSS-kleur). Een ruwe hero direct kopiëren naar background: is waar de meeste pijplijnen stilletjes een lege grijze vlek afleveren.

  1. Laad de hero nooit lazy. web.dev is ondubbelzinnig: “Laad je LCP-afbeelding nooit lazy, want dat leidt altijd tot onnodige vertraging in het laden van resources en heeft een negatieve impact op LCP”, en beveelt fetchpriority="high" aan op het element dat waarschijnlijk de LCP is — spaarzaam gebruikt, op één afbeelding.4 Een pijplijn die loading="lazy" op elke afbeelding stempelt, hero inbegrepen, is de meest voorkomende zelf-toegebrachte Core Web Vitals-wond in geautomatiseerd publiceren.
  2. Genereer altijd width en height — ze komen terug in het antwoord, dus er is geen excuus; dat ene attributenpaar is wat de browser in staat stelt de ruimte te reserveren en de layout stopt met springen. Gebruik blur_hash als placeholder terwijl het bestand laadt.
  3. Schrijf de alt na de keuze, nooit ervoor. Dit is de subtiele. Het alt-veld van het plan beschrijft de scène die je gevraagd hebt; de foto die je kreeg is de dichtstbijzijnde match, niet die scène. De tekst van de briefing als alt verzenden, is precies het toegankelijkheidsprobleem dat deze pijplijn zou moeten voorkomen — een beschrijving van een afbeelding die niet op de pagina staat. Bouw de alt op basis van de alt_description van de gekozen foto, verfijnd tegen de paragraaf waarin hij staat. Google's advies is om je te “richten op het maken van nuttige, informatierijke content die zoekwoorden op een passende manier gebruikt en in context is met de content van de pagina”, en het waarschuwt dat het opvullen van alt-attributen met zoekwoorden “resulteert in een negatieve gebruikerservaring en ertoe kan leiden dat je site als spam wordt gezien”.1

Het onderdeel dat bijna niemand automatiseert: licentiemetadata

Google ondersteunt ImageObject-structuurdata voor beeldlicenties. Het vereist contentUrl plus minstens één van creator, creditText, copyrightNotice of license, beveelt acquireLicensePage aan, en afbeeldingen met licentie-informatie komen in aanmerking voor de Licensable-badge in Google Afbeeldingen.5 Elk van die velden zit al in het zoekantwoord — het genereren ervan is dus een template, geen project:

ImageObject JSON-LD, ingevuld vanuit het API-antwoord
<script type="application/ld+json">
{
  "@context": "https://schema.org/",
  "@type": "ImageObject",
  "contentUrl": "{urls.large}",                 // verplicht
  "creator": { "@type": "Person",
                "name": "{photographer_full_name}" },
  "creditText": "{photographer_full_name} on {source}",
  "license": "{LICENSE_URL[source]}",           // de eigen pagina van de bibliotheek
  "acquireLicensePage": "{source_image_url}"     // de pagina van de foto
}
</script>

# LICENSE_URL koppelt het `source`-veld aan de licentie die daadwerkelijk
# op de foto van toepassing is — unsplash.com/license, pexels.com/license, pixabay.com/…

Eén detail dat het waard is om goed te doen: license moet verwijzen naar de licentie die op die foto van toepassing is — de eigen licentiepagina van de bronbibliotheek — niet naar een samenvattingspagina op je eigen domein. Google leest het om te bepalen of de badge van toepassing is, en een zelfverwijzende URL is zowel zwakker als signaal als moeilijk te verdedigen als iets anders dan een link naar jezelf. Houd je eigen licentiesamenvatting als interne pagina voor lezers; zet de canonieke versie in de markup.

En terwijl je toch in de assembler bezig bent: geef het bestand een korte, beschrijvende naam in plaats van IMG_0042.jpg, en voeg de afbeelding toe aan een sitemap — het beeld-sitemapformaat van Google accepteert tot 1.000 afbeeldingen per pagina-URL.6 Beide zijn elk één regel in een pijplijn en geen van beide gebeurt ooit met de hand.

Twee pijplijnen om over te nemen

Dezelfde vijf fasen, twee heel verschillende vormen — één voor artikelen die je schrijft, één voor documentatie die moet overeenkomen met een draaiend product. Kies degene waarvan je het faalscenario herkent.

1 · De devblog in CI — Markdown in de repo

Artikelen bestaan als Markdown, afbeeldingen worden ernaast gecommit, en het geheel draait bij een push. Deterministisch, controleerbaar in de PR, geen runtime-afhankelijkheid van welke API dan ook:

.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   # afbeeldingen komen terecht in de PR
        with: { commit-message: "chore(content): illustrate" }

Een mens keurt de PR nog altijd goed, en dat is het punt: de pijplijn stelt voor, de beoordelaar beslist, en de front-matter die hij schreef is diffbaar.

De zoekhelft is één GET — het verzoek, de score_threshold ervan en de velden die het teruggeeft staan regel voor regel uitgeschreven in de gids voor één artikel, en worden hier dus geïmporteerd in plaats van opnieuw afgedrukt. Wat dit bestand toevoegt is alles wat een plan nodig heeft en één foto niet: de herhaalpoging op een brief die niets opleverde, de claim op een photo_id zodat geen twee pagina's hetzelfde beeld delen, en de alt-tekst geschreven op basis van de foto die terugkwam.

plan_to_pr.py — de lijm: visueel plan erin, front matter eruit
import frontmatter
from photo_search import search   # één GET /search/photos; slaat id's in `used` over

def find_photo(entry: dict, used: set) -> dict | None:
    """Zoek; als de briefing te specifiek was, verruim eenmalig, geef daarna op."""
    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"])   # claim: geen herhalingen sitebreed
            return photo
    return None                        # de aanroeper beslist: slot overslaan, of falen

def widen(query: str) -> str:
    """Laat de laatste bijzin vallen — meestal de te specifieke."""
    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:                    # geen hero is beter dan een slechte
        raise SystemExit(f"{path}: no photo above threshold — rewrite the brief")

    post["hero"] = {                    # alles wat de template nodig heeft
        "src": hero["urls"]["regular"], "w": hero["width"], "h": hero["height"],
        # De alt beschrijft de foto die we KREGEN, nooit de scène die we vroegen.
        "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 als basis, ingekort tot ~125 tekens voor schermlezers.
    Stuur het via het model terug met de paragraaf als je een beter resultaat wilt."""
    base = photo.get("alt_description") or photo.get("description", "")
    return base[:125].rstrip(" ,;")

2 · Documentatie & changelog — screenshots eerst, foto's laatst

Draai de standaardinstellingen van de router om. In productdocumentatie is de eerlijke visual vrijwel altijd je eigen UI: een Playwright-script dat de echte build opent, een vaste viewport instelt en precies de status vastlegt die de paragraaf beschrijft. Diagrammen dekken de architectuurpagina's, en foto's verschijnen alleen op de conceptuele en landingspagina's — waar een screenshot niets zou zeggen.

shots.py — de screenshot wordt gegenereerd, nooit beschreven
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)   # retina-scherp
    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()
# Draait in dezelfde CI-job als de documentatiebuild → de screenshot kan nooit
# een versie van de UI beschrijven die niet meer bestaat.

Twee varianten die het waard zijn om te noemen, maar geen eigen recept verdienen. Programmatische SEO keert de lus om: met honderden pagina's die uit een database worden gegenereerd, zoek je niet per pagina — één verzoek levert tot 100 foto's op, dus zoek je per onderwerpcluster en wijs je toe uit een pool, met een uniciteitsbeperking die de deduplicatie verzorgt (die architectuur, volledig uitgewerkt). En nieuwsbrieven en social cards hebben dezelfde foto nodig in vier bijsnijdingen: bewaar de photo_id, vraag de maat die je nodig hebt op uit urls, en zoek alleen opnieuw als een bijsnijding echt mislukt.

Dezelfde pijplijn, als één agent

<<>>

Als een model de conceptversie al aan het schrijven is, is de kortste weg om het model rechtstreeks toegang te geven tot de zoektool, in plaats van JSON tussen processen heen en weer te sturen. Pexafy draait een gehoste Model Context Protocol-server op mcp.pexafy.com/mcp. De connectorconfiguratie en de tools die deze aanbiedt, worden elders behandeld — de desktop- en editorconfiguratie in de gids voor één artikel, de headless-variant voor een CI-runner in het artikel over content op schaal, en hoe de bredere connectormarkt eruitziet — wie er een MCP-image-server aanbiedt, onder welke voorwaarden — in de studie naar zoekinfrastructuur voor afbeeldingen voor AI-agents. Wat het hier waard is om te laten zien, is wat er met deze pipeline gebeurt zodra de agent over de tools beschikt: het houdt op vijf fasen te zijn en wordt één instructie.

Eén instructie, het hele plan uitgevoerd
You  Here is the draft. Build the visual plan, illustrate it, and open a PR.
     Charts only from numbers already in the text.

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 · picked #1, 3000×1688, credited
      → mermaid-cli build/graph.mmd → static/img/graph.svg
      → chart.py §2 → 4 values checked against the draft ✓
      → search_photos(q="two engineers standing at a whiteboard…")
      ← 16 photos · 144 ms · picked #1

      ✓ 4 slots filled · 2 API calls · alt + credit + ImageObject written
      ⚠ §5 returned nothing above 0.5 — brief too abstract, rewritten as
        "a person at a kitchen table checking figures on a laptop"

Die laatste regel is waarom een agent in deze lus meerwaarde heeft ten opzichte van een puur script: het faalscenario van geautomatiseerde illustratie is een slechte briefing, en een slechte briefing herschrijven is precies waar een taalmodel voor dient. Houd de deterministische onderdelen — toewijzing, deduplicatie, de numerieke controle — in code.

Wat één geïllustreerd artikel kost

Drie fotoslots per artikel betekent drie zoekverzoeken, dus het gratis abonnement (5,000 verzoeken/maand) dekt 1,666 artikelen per maand voordat er ook maar een vraag over betalen rijst — en als je zoekopdrachten per onderwerpcluster bundelt in plaats van per artikel, verschuift dat plafond nog eens een orde van grootte. De uitsplitsing per abonnement, en de bundelingsarchitectuur die dit vraagstuk irrelevant maakt, staan in het aanvullende artikel over content op schaal illustreren.

De twee modelaanroepen per artikel — één voor het concept, één voor het visuele plan — zijn samen een paar duizend tokens en zullen de goedkoopste regel in de pijplijn zijn; Mermaid- en matplotlib-renders kosten niets behalve CI-seconden. Het getal dat mensen verrast, is welke regel niet goedkoop is: vier afbeeldingen per artikel genereren, vierhonderd afbeeldingen per maand, plus de pogingen die het niet haalden — en de output draagt nog altijd geen fotograaf, geen datum en geen bron-URL bij zich.

Wat deze pijplijn niet oplost

Een eerlijke sectie, want het falen dat hij voorkomt is kostbaar. Een goed geïllustreerd artikel is nog altijd een artikel: echte fotografie, accurate grafieken en correcte markup verbeteren een pagina die het verdient om te bestaan. Ze zorgen niet dat dunne, massaal geproduceerde content gaat ranken. Google's spambeleid noemt schaalmisbruik van content — het genereren van veel pagina's die primair bedoeld zijn om rankings te manipuleren en weinig waarde bieden aan gebruikers, ongeacht of automatisering erbij betrokken is — en de kwaliteit van de illustraties speelt geen rol in dat oordeel.7

Dus het kader dat wél standhoudt: deze pijplijn is een kwaliteitsvloer die je zelf beheert, toegepast op pagina's die al een reden hebben om gepubliceerd te worden. Waar het aantoonbaar loont:

  • Herkomst die een lezer kan verifiëren. Een credit met een echte fotograaf en een bron-URL is een claim die te controleren is — en diezelfde velden voeden de ImageObject-markup die Google leest.
  • Toegankelijkheid en Core Web Vitals. Echte alt-tekst, afmetingen op elke afbeelding, een hero die nooit lazy geladen wordt. Vermenigvuldig dat met elk artikel dat je publiceert en dit is het beeldkwaliteitsverhaal van de site.
  • Nauwkeurigheid waar die controleerbaar is. Een grafiek waarvan de cijfers getoetst worden aan het artikel, een screenshot gegenereerd uit de draaiende build, een diagram dat als tekst controleerbaar is in de PR — drie visuals die niet van de waarheid kunnen afwijken zonder dat een test faalt.

Wat betreft bronvermelding is het pipeline-specifieke punt smal: het pleidooi om het credit altijd te tonen wordt elders gevoerd, en wat een geautomatiseerde samensteller toevoegt is dat dezelfde attribution-string die hij afdrukt, de creditText is die de gestructureerde data nodig heeft. Eén veld, twee plekken, uitgevoerd in dezelfde templatepasage — waarom een pipeline nog minder excuus heeft dan een mens om het weg te laten.

Waar te beginnen

  1. Voeg de routerprompt toe aan wat je concepten nu al schrijft, en print de JSON zonder ernaar te handelen. Lees tien plannen. Als de briefings onderwerpen noemen in plaats van scènes, repareer dan de prompt voordat je enige integratie schrijft.
  2. Bedraad alleen de fotoslots. Eén GET /search/photos per briefing, en bewaar photo_id vanaf dag één — deduplicatie achteraf toevoegen aan 3.000 live pagina's is een migratie, geen kolom.
  3. Genereer breedte, hoogte en alt in dezelfde commit. Het is het goedkoopste Core Web Vitals-werk dat je ooit zult doen.
  4. Voeg vervolgens fase 4 toe, diagrammen vóór grafieken — Mermaid is tekst, dus het is de enige zonder ander faalscenario dan een syntaxfout.
  5. Voeg de numerieke controle toe voordat de eerste grafiek een lezer bereikt, niet erna.

Bronnen & voetnoten

1 Google Search Central, Image SEO best practices: alt-tekst is “het belangrijkste attribuut als het gaat om het verstrekken van metadata voor een afbeelding”; het advies is om “nuttige, informatierijke content te maken die zoekwoorden op een passende manier gebruikt en in context is met de content van de pagina”, om met zoekwoorden opgevulde alt-attributen te vermijden, om HTML-<img>-elementen te gebruiken in plaats van CSS-afbeeldingen, en om bestanden korte maar beschrijvende namen te geven.

2 EU AI Act, Artikel 50 — transparantieverplichtingen van toepassing vanaf 2 augustus 2026: aanbieders van systemen die synthetische afbeeldingen, audio, video of tekst genereren, moeten output markeren in een machineleesbaar formaat en detecteerbaar maken als kunstmatig gegenereerd. Het bindt AI-aanbieders en -gebruikers; het is geen regel over welke afbeeldingen een website mag publiceren.

3 Mermaid rendert diagrammen en grafieken vanuit op Markdown geïnspireerde tekstdefinities, wat de output controleerbaar en deterministisch maakt.

4 web.dev, Optimize Largest Contentful Paint: “Het is een goed idee om fetchpriority="high" in te stellen op een <img>-element als je denkt dat het waarschijnlijk het LCP-element van je pagina is”, spaarzaam gebruikt; en “Laad je LCP-afbeelding nooit lazy, want dat leidt altijd tot onnodige vertraging in het laden van resources en heeft een negatieve impact op LCP.”

5 Google Search Central, Image metadata (structured data): ImageObject vereist contentUrl plus minstens één van creator, creditText, copyrightNotice of license; acquireLicensePage wordt aanbevolen, en afbeeldingen met licentie-informatie kunnen in aanmerking komen voor de Licensable-badge in Google Afbeeldingen.

6 Google Search Central, Image sitemaps: beeld-sitemaps informeren Google over afbeeldingen op een site, inclusief die gevonden via JavaScript, en accepteren tot 1.000 afbeeldingen per pagina-URL.

7 Google Search-spambeleid — schaalmisbruik van content: het genereren van veel pagina's die primair bedoeld zijn om rankings te manipuleren en weinig waarde bieden aan gebruikers, ongeacht of ze gemaakt zijn via automatisering, menselijke inzet of een combinatie daarvan.

Veelgestelde vragen

Hoe illustreer ik automatisch artikelen die door een LLM zijn geschreven?
Voeg één modelaanroep toe tussen schrijven en publiceren: vraag het om een visueel plan terug te geven — één item per afbeeldingsslot, elk gemarkeerd als foto, grafiek, diagram of niets. Foto-items bevatten een omschrijving van 12 tot 25 woorden van een scène die een camera had kunnen vastleggen, die je naar een semantische afbeeldingzoek-API stuurt (GET /api/v1/search/photos, ongeveer 150 ms). Grafiek- en diagramitems gaan naar een model dat plotcode of Mermaid schrijft, deterministisch gerenderd. Voer nooit de artikeltitel in een afbeeldingzoekopdracht in: titels zijn abstract en geen foto beeldt ze af.
Moet een documentatiesite screenshots of stockfoto's gebruiken?
Draai de standaardinstellingen van de router om: in productdocumentatie is de eerlijke visual bijna altijd je eigen UI. Een Playwright-script dat de echte build opent, de viewport vastzet en precies de staat vastlegt die de paragraaf beschrijft, draait in dezelfde CI-job als de docs-build, zodat een screenshot nooit een versie van de interface kan tonen die niet meer bestaat. Diagrammen dragen de architectuurpagina's, en foto's verschijnen alleen op conceptuele pagina's en landingspagina's, waar een screenshot niets zou zeggen.
Hoe voorkom ik dat een AI-pipeline verkeerde cijfers in een grafiek plaatst?
Zorg dat het plan overschrijft in plaats van verzint: de router mag alleen waarden kopiëren die al in het concept voorkomen naar het data-veld van het grafiekitem. Controleer dit vervolgens in code vóór het renderen — check voor elke waarde of de string ervan in het artikel voorkomt en gooi anders een fout. Negen regels, en een grafiek die zijn eigen paragraaf tegenspreekt kan nooit bij een lezer terechtkomen.
Welke afbeeldingsmarkup moet een geautomatiseerde pipeline uitvoeren voor SEO?
Drie dingen, allemaal uit velden die het zoekresultaat al bevat. Een beschrijvende alt, geschreven in de context van de paragraaf — Google noemt alt-tekst de belangrijkste afbeeldingsmetadata en waarschuwt tegen keyword stuffing. width en height op elke afbeelding, met fetchpriority="high" op de hero en nooit loading="lazy" daarop, omdat de LCP-afbeelding niet lazy geladen mag worden. En ImageObject gestructureerde data met contentUrl, creator, creditText en license, wat een afbeelding in aanmerking laat komen voor het Licensable-label in Google Afbeeldingen.
Helpt het illustreren van AI-geschreven artikelen bij de ranking?
Niet op zichzelf, en het is de moeite waard om precies te zijn. Google's spambeleid definieert scaled content abuse als het genereren van veel pagina's voornamelijk om rankings te manipuleren met weinig waarde voor gebruikers, ongeacht of automatisering erbij betrokken is; de illustraties veranderen die beoordeling niet. Wat een goede pipeline oplevert is een kwaliteitsbodem voor pagina's die al recht van bestaan hebben: verifieerbare herkomst, toegankelijke alt-tekst, Core Web Vitals die automatisering overleven, en grafieken en screenshots die niet kunnen afwijken van de waarheid.
Hoe voer ik de illustratiestap in CI uit zonder de redactionele controle te verliezen?
Trigger de job bij een pull request die je contentbestanden raakt, laat hem de afbeeldingen en de front-matter schrijven, en laat hem een pull request openen in plaats van rechtstreeks naar de branch te committen — de pipeline stelt voor, een mens keurt goed, en elk veld dat hij schreef is te diffen. Houd drie dingen deterministisch in code in plaats van in het model: de deduplicatie op photo_id, de assertie dat elk geplot getal in het artikel voorkomt, en een harde mislukking wanneer geen enkele foto de scoredrempel haalt. Geen hero is beter dan een verkeerde.

Stop met jagen op trefwoorden. Beschrijf wat je bedoelt.

Doorzoek 9M+ vrij te gebruiken afbeeldingen op betekenis — in elke taal, in minder dan 100 ms.