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.
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:
┌─ 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.
# 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:
De laatste sectiebriefing, een volledig andere scène, in 144 ms:
Wat per foto terugkomt, is wat fase 5 mogelijk maakt — niet alleen een bestand:
{
"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.
# 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.
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.
<!-- 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.
-
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 dieloading="lazy"op elke afbeelding stempelt, hero inbegrepen, is de meest voorkomende zelf-toegebrachte Core Web Vitals-wond in geautomatiseerd publiceren. -
Genereer altijd
widthenheight— 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. Gebruikblur_hashals placeholder terwijl het bestand laadt. -
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 dealt_descriptionvan 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:
<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:
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.
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.
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.
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
- 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.
- Bedraad alleen de fotoslots. Eén
GET /search/photosper briefing, en bewaarphoto_idvanaf dag één — deduplicatie achteraf toevoegen aan 3.000 live pagina's is een migratie, geen kolom. - Genereer breedte, hoogte en alt in dezelfde commit. Het is het goedkoopste Core Web Vitals-werk dat je ooit zult doen.
- Voeg vervolgens fase 4 toe, diagrammen vóór grafieken — Mermaid is tekst, dus het is de enige zonder ander faalscenario dan een syntaxfout.
- 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.
Bronnen gecontroleerd op 17 augustus 2026: Image SEO best practices · Image metadata · Image sitemaps · Optimize LCP · Search spam policies · AI Act Article 50 · Mermaid · Pexafy API & MCP docs. Zoektimings (147 ms, 144 ms) en elke getoonde foto zijn echte API-antwoorden, dezelfde dag vastgelegd.
Veelgestelde vragen
Hoe illustreer ik automatisch artikelen die door een LLM zijn geschreven?
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?
Hoe voorkom ik dat een AI-pipeline verkeerde cijfers in een grafiek plaatst?
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?
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?
Hoe voer ik de illustratiestap in CI uit zonder de redactionele controle te verliezen?
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.