El texto es la parte fácil: el pipeline que ilustra artículos escritos por IA
Generar el texto está resuelto. Ilustrarlo no. El pipeline de principio a fin — prompt enrutador, búsqueda de fotos, diagramas Mermaid, gráficos verificados, y el texto alternativo, crédito y marcado ImageObject que lo convierten en SEO.
Eres un desarrollador con un objetivo de contenido: decenas de artículos al mes, quizá cientos. El modelo de escritura se encarga del borrador, el esquema, la meta descripción, los enlaces internos. Luego el pipeline llega al único paso que no tiene modelo propio — las imágenes — y se detiene. No porque las imágenes sean difíciles de obtener, sino porque nada en la pila sabe cuál imagen, de dónde, con qué licencia, descrita cómo.
Este artículo es ese paso que falta, conectado de principio a fin: qué tipo de visual producir para cada tipo de sección, el prompt que convierte un borrador terminado en briefs de búsqueda, la llamada a la API que devuelve las fotos, el segundo modelo que dibuja lo que una fotografía no puede, y el ensamblador que emite el markup que Google realmente puede leer. Dos flujos de trabajo completos al final, listos para copiar.
El texto está resuelto. La ilustración es donde se estanca.
Haz la auditoría honesta de un pipeline de artículos automatizado. Esquema: resuelto. Borrador: resuelto. Título, meta descripción, esquema de datos, enlaces internos, traducción: resuelto, todo por el mismo modelo, todo en texto. Luego:
| Paso del pipeline | Estado | Qué bloquea realmente |
|---|---|---|
| Esquema y borrador | Resuelto | Una llamada al modelo, un prompt |
| Títulos, meta, esquema de datos, enlaces | Resuelto | Entra texto, sale texto |
| Imagen del hero | Bloqueado | Necesita un archivo real, una licencia, dimensiones y texto alternativo — nada de lo cual puede producir un modelo de texto |
| Imágenes de sección | Bloqueado | De tres a cinco por artículo, cada una distinta, ninguna repetida en todo el sitio |
| Gráficos y diagramas | Bloqueado | Deben ser precisos — el único visual que una búsqueda no puede devolver y un generador no debe inventar |
El fallo no es estético, es estructural: el artículo se publica con un marcador de posición genérico, o
con la misma foto que los últimos doce, o con una imagen generada cuya mano de seis dedos es lo
primero que ve el lector. Y la imagen no es decoración — la propia documentación de imágenes de Google
lo dice sin rodeos: el texto alternativo “es el atributo más importante a la hora de proporcionar metadatos
para una imagen”, y la guía indica usar elementos <img> reales con
alt descriptivo en lugar de fondos CSS, para que la imagen pueda ser encontrada y
comprendida.1
Cuatro tipos de visual, cuatro tipos de modelo
El mayor error de diseño es tratar “la imagen” como un solo problema con un solo proveedor. Son cuatro, y el enrutador entre ellos es una línea de prompt, no un servicio:
| La sección necesita… | Produce esto con | Por qué no las otras opciones |
|---|---|---|
| Una escena del mundo realhero, situaciones humanas, lugares, objetos, gestos | Búsqueda semántica de fotos (Pexafy) | Un generador inventa los detalles; un gráfico no tiene nada que representar |
| Números que realmente tienesbenchmarks, precios, resultados de encuestas, latencia | Un modelo que escribe código de graficado, ejecutado en un sandbox | No se puede confiar a un modelo de imágenes un valor; una foto no puede transportar datos |
| Una estructura o un flujoarquitectura, secuencia, máquina de estados | Un modelo que escribe Mermaid / Graphviz, renderizado de forma determinista | Los bancos de fotos gratuitos no tienen un diagrama de tu sistema |
| Tu producto en pantalladocs, changelog, tutoriales | Una captura de pantalla programada (Playwright) | Nada más puede mostrar una interfaz que solo existe en tu build |
¿Y la ilustración generada? Conserva una función legítima: la escena que no se puede fotografiar y no es un dato — un mecanismo abstracto, un producto que aún no existe, un estilo de ilustración propio de la casa. (El argumento completo a favor de la fotografía real frente a la generación —velocidad a escala, precisión, el problema de la uniformidad— está desarrollado aquí.) El precio marca la dirección: desde el 2 de agosto de 2026, el Artículo 50 de la Ley de IA de la UE exige a los proveedores de sistemas generativos marcar los resultados sintéticos en un formato legible por máquina.2 Es una obligación para los proveedores y desplegadores de IA, no una norma sobre lo que un blog puede publicar — pero por eso la procedencia de la imagen en la cabecera de tu artículo es cada vez más algo que un lector puede verificar en lugar de dar por hecho.
El pipeline, de principio a fin
Cinco etapas. Solo la etapa 3 toca una API de imágenes, y solo la etapa 4 es opcional:
┌─ 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
└───────────────────────────────────────────────────────────────┘
Dos propiedades importan más que el diagrama. La etapa 2 es un enrutador: decide, por cada slot, qué productor se ejecuta, de manera que nunca pides un gráfico de barras a un banco de fotos. Y la etapa 5 es donde está el SEO — todo lo que Google documenta sobre imágenes (alt descriptivo, metadatos de licencia, un hero seguro para el LCP) se emite aquí, a partir de campos que la respuesta de búsqueda ya traía.
Etapa 2: convertir el borrador en briefs visuales
Una llamada al modelo sobre el borrador terminado, y lo que devuelve es un plan en lugar de una consulta. Cómo escribir un único brief fotográfico —por qué el título del artículo es la peor entrada posible, cómo es un brief de cámara de entre 12 y 25 palabras, y de qué maneras falla— se explica en detalle en la guía para ilustrar un solo artículo, y no se repite aquí. Lo que añade una canalización es el enrutamiento: la misma llamada tiene que decidir, hueco a hueco, qué productor se ejecuta — y una entrada de gráfico lleva datos donde una entrada de foto lleva una escena.
# 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:
Escribe un brief de cámara: una escena que una cámara podría haber
captado, de 12 a 25 palabras, en inglés, acorde con el tono de la
sección. Nombra lo que está en el encuadre, nunca el tema. Sin texto,
logotipos, marcas ni personas famosas; sin metáforas invisibles.
(Reglas completas, con ejemplos y casos fallidos:
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": "…" }
]
}
La línea kind hace la mayor parte del trabajo, y la instrucción data
hace el resto: una entrada de gráfico solo puede llevar números que ya aparecen en el borrador,
de modo que el modelo transcribe en lugar de inventar. Aquí está el enrutador aplicado a tres
secciones reales de un artículo para desarrolladores:
| Sección | kind | Lo que devolvió el enrutador |
|---|---|---|
| Hero — “Por qué nuestro build nocturno tarda 40 minutos” | photo |
“un desarrollador trabajando de noche en un escritorio con dos monitores y un teclado mecánico en una habitación oscura iluminada por las pantallas” |
| “A dónde va realmente el tiempo” | chart |
data copiado del párrafo: instalación 480 s, compilación 1080 s, tests 720 s, subida 120 s |
| “Cómo dividimos el grafo” | diagram |
flowchart LR del grafo de dependencias de los jobs |
| “Qué cambiamos, y qué volveríamos a hacer” | photo |
“dos ingenieros de pie frente a una pizarra cubierta de diagramas resolviendo un problema juntos” |
Etapa 3: las fotos vuelven acreditadas
Cada entrada kind: "photo" es una única petición. El brief del hero de arriba, ejecutado contra la
API pública, devuelve esto en 147 ms:
El brief de la última sección, una escena completamente distinta, en 144 ms:
Lo que devuelve cada foto es lo que hace posible la etapa 5 — no solo un archivo:
{
"photo_id": "019e1c7f-0063-759e-b498-33ce1714e6c9", // guárdalo: sin repeticiones
"urls": { "small": "…?w=400", "regular": "…?w=1080",
"large": "…?w=1920" },
"width": 3000, "height": 1688, // → sin salto de layout
"blur_hash": "LJ8gjv9rVq-6OFxanNNFI7xco$Na", // → placeholder real
"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 (…)" }
}
Etapa 4: lo que una fotografía no puede decir
Dos slots del plan no son buscables, y es exactamente ahí donde un segundo modelo se ganan su sitio — no para dibujar una imagen, sino para escribir código que la dibuje. La distinción importa: el código es revisable, determinista y no puede alucinar la altura de una barra.
Diagramas: entra texto, sale SVG
Mermaid renderiza diagramas a partir de una definición en texto plano,3 lo que lo convierte en el destino más seguro para un modelo: la salida es inspeccionable, se puede comparar en git y se renderiza de la misma manera cada vez. El enrutador ya devolvió la especificación.
# the "spec" field of a diagram entry, written to 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 puedes revisar en el PR, no una imagen que hay que confiar a ciegas
Gráficos: solo números que el artículo ya contiene
Mismo principio, una salvaguarda adicional. El enrutador copió los valores del borrador; el modelo escribe el código de graficado; el código se ejecuta en un sandbox; el ensamblador vuelve a comprobar los valores renderizados contra los números de origen antes de que el gráfico pueda acercarse a una página.
import re, matplotlib
matplotlib.use("Agg") # sin interfaz: sin display en CI
import matplotlib.pyplot as plt
def assert_in_draft(value, draft: str) -> None:
"""A plotted number must appear in the article AS A NUMBER."""
# La trampa aquí es la coincidencia de subcadenas: "120" está dentro de
# "1200", y dentro de "?w=1200" — un `str(v) in draft` ingenuo acepta cualquier cosa.
# Compara en límites de palabra, y acepta 1 234 / 1,234 / 1234.
body = re.sub(r"(?<=\d)[ ,](?=\d{3}\b)", "", draft) # quita separadores
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) # valor alucinado → sin gráfico
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) # artefacto determinista y revisable
return out
La salvaguarda es corta, pero escríbela con cuidado: una comprobación por subcadena no funciona.
"120" in draft es verdadero para un artículo que contiene 1200 o
?w=1200, así que la versión ingenua pasa con todo y no protege nada. Ancla en
los límites de los dígitos, normaliza los separadores de millar, y la clase de error que hace que
un artículo técnico sea destrozado en los comentarios — un gráfico que contradice su propio párrafo — no puede llegar a
producción. Las capturas de pantalla siguen el mismo principio: un page.screenshot() programado
contra tu build real es la única fuente de verdad para tu propia interfaz, y sigue siendo cierta mientras la interfaz
cambia.
Etapa 5: el ensamblado es donde vive el SEO
Todo lo anterior produjo archivos y campos. Esta etapa los convierte en markup — y merece la pena ser preciso aquí, porque tres comportamientos documentados se decinden en estas pocas líneas.
<!-- Los bytes vienen de otro origen: paga el handshake pronto -->
<link rel="preconnect" href="https://images.unsplash.com" crossorigin>
<!-- Elemento LCP: nunca lazy, siempre alta prioridad -->
<figure>
<img src="{urls.regular}"
width="{width}" height="{height}" <!-- elimina el layout shift -->
alt="{alt}" <!-- escrito DESPUÉS de la elección -->
fetchpriority="high" decoding="async"
style="background:{color_hex}"> <!-- color dominante, 1 campo -->
<figcaption>{attribution.html}</figcaption>
</figure>
<!-- Imágenes de sección, bajo el pliegue: los ajustes opuestos -->
<img src="{urls.regular}" width="{width}" height="{height}"
alt="{alt}" loading="lazy" decoding="async">
¿Hotlink o rehospedar? El fragmento anterior hace hotlink, que es lo más rápido de
publicar y la razón del preconnect: un hero remoto cuesta una búsqueda DNS y un handshake TLS
en la ruta crítica, y eso puede comerse la ganancia que acabas de comprar con
fetchpriority. Rehospedar elimina por completo el origen de terceros, te permite servir
AVIF/WebP en tus propios breakpoints y sobrevive a que una URL de origen cambie — al coste de
almacenamiento, un paso de descarga en el pipeline y la factura de tu propia CDN. Sea cual sea tu elección,
color_hex te da un placeholder para un único campo (un blur hash es más bonito, pero
hay que decodificarlo primero a un data URI — no es un color CSS). Copiar un hero aproximado
directamente en background: es donde la mayoría de los pipelines terminan publicando en silencio una caja gris vacía.
-
Nunca hagas lazy-load del hero. web.dev es inequívoco: “Never lazy-load your LCP
image, as that will always lead to unnecessary resource load delay, and will have a negative
impact on LCP”, y recomienda
fetchpriority="high"en el elemento que probablemente sea el LCP — usado con moderación, en una sola imagen.4 Un pipeline que estampaloading="lazy"en cada imagen, hero incluido, es la herida autoinfligida más común en Core Web Vitals en la publicación automatizada. -
Emite siempre
widthyheight— vuelven en la respuesta, así que no hay excusa; ese único par de atributos es lo que permite al navegador reservar el espacio y evita que el layout salte. Usablur_hashcomo placeholder mientras el archivo carga. -
Escribe el alt después de la elección, nunca antes. Este es el detalle sutil.
El campo
altdel plan describe la escena que pediste; la foto que obtuviste es la coincidencia más cercana, no esa escena. Publicar el texto del brief como alt es exactamente el fallo de accesibilidad que este pipeline debería evitar — una descripción de una imagen que no está en la página. Construye el alt a partir delalt_descriptionde la foto elegida, refinado contra el párrafo en el que se encuentra. La guía de Google es “focus on creating useful, information-rich content that uses keywords appropriately and is in context of the content of the page”, y advierte que rellenar los atributos alt con palabras clave “results in a negative user experience and may cause your site to be seen as spam”.1
La parte que casi nadie automatiza: los metadatos de licencia
Google admite datos estructurados ImageObject para la licencia de imágenes. Requiere
contentUrl más al menos uno de creator, creditText,
copyrightNotice o license, recomienda acquireLicensePage,
y las imágenes con información de licencia se vuelven aptas para la insignia Licensable en Google
Images.5 Cada uno de esos campos ya está en la respuesta de búsqueda
— así que emitirlo es una plantilla, no un proyecto:
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "ImageObject",
"contentUrl": "{urls.large}", // requerido
"creator": { "@type": "Person",
"name": "{photographer_full_name}" },
"creditText": "{photographer_full_name} on {source}",
"license": "{LICENSE_URL[source]}", // la propia página del banco de imágenes
"acquireLicensePage": "{source_image_url}" // la página de la foto
}
</script>
# LICENSE_URL mapea el campo `source` a la licencia que realmente rige
# la foto — unsplash.com/license, pexels.com/license, pixabay.com/…
Un detalle que vale la pena hacer bien: license debe apuntar a la licencia que
rige esa foto — la propia página de licencia del banco de imágenes de origen — no a una página de resumen en
tu dominio. Google la lee para decidir la elegibilidad de la insignia, y una URL autorreferencial es a la vez
más débil como señal y difícil de defender como algo distinto de un enlace hacia ti mismo. Mantén tu
propio resumen de licencia como una página interna para los lectores; pon la canónica en el markup.
Y ya que estás en el ensamblador: da al archivo un nombre corto y descriptivo en lugar de
IMG_0042.jpg, y añade la imagen a un sitemap — el formato de sitemap de imágenes de Google acepta
hasta 1000 imágenes por URL de página.6 Ambas cosas son una línea cada una en un
pipeline y ninguna se hace nunca a mano.
Dos pipelines para copiar
Las mismas cinco etapas, dos formas muy distintas — una para artículos que escribes, otra para documentación que debe coincidir con un producto en producción. Elige la que reconozcas por su modo de fallo.
1 · El blog de desarrollo en CI — Markdown en el repositorio
Los artículos viven como Markdown, las imágenes se confirman junto a ellos, y todo se ejecuta al hacer push. Determinista, revisable en el PR, sin dependencia en tiempo de ejecución de ninguna API:
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 # las imágenes llegan en el PR
with: { commit-message: "chore(content): illustrate" }
Un humano sigue aprobando el PR, que es la clave: el pipeline propone, el revisor dispone, y el front-matter que escribió se puede comparar en el diff.
La mitad de búsqueda es un único GET — la solicitud, su score_threshold
y los campos que devuelve están escritos línea por línea en
la guía de un solo
artículo, así que aquí se importa en lugar de reimprimirse. Lo que añade este archivo es todo
lo que un plan necesita y una sola foto no: el reintento ante un brief que no devolvió
nada, la reserva de un photo_id para que ninguna página comparta imagen con otra, y
el texto alternativo escrito a partir de la foto que llegó.
import frontmatter
from photo_search import search # un único GET /search/photos; omite los ids en `used`
def find_photo(entry: dict, used: set) -> dict | None:
"""Search; if the brief was too specific, widen it once, then give up."""
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"]) # la reservamos: sin repeticiones en el sitio
return photo
return None # el llamador decide: saltar el slot o fallar
def widen(query: str) -> str:
"""Drop the last clause — usually the over-specific one."""
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: # ningún hero es mejor que uno malo
raise SystemExit(f"{path}: no photo above threshold — rewrite the brief")
post["hero"] = { # todo lo que necesita la plantilla
"src": hero["urls"]["regular"], "w": hero["width"], "h": hero["height"],
# El alt describe la foto que OBTUVIMOS, nunca la escena que pedimos.
"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 as the base, trimmed to ~125 chars for screen readers.
Send it back through the model with the paragraph if you want better."""
base = photo.get("alt_description") or photo.get("description", "")
return base[:125].rstrip(" ,;")
2 · Docs y changelog — capturas de pantalla primero, fotos al final
Invierte los valores por defecto del enrutador. En la documentación de producto, el visual honesto es casi siempre tu propia interfaz: un script de Playwright que abre el build real, fija un viewport y captura el estado exacto que el párrafo describe. Los diagramas cubren las páginas de arquitectura, y las fotografías aparecen solo en las páginas conceptuales y de aterrizaje — donde una captura de pantalla no diría nada.
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) # nitidez 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()
# Se ejecuta en el mismo job de CI que el build de la documentación → la captura de pantalla nunca puede
# describir una versión de la interfaz que ya no existe.
Dos variantes que merece la pena nombrar aunque no merezcan su propia receta. El SEO
programático invierte el bucle: con cientos de páginas generadas desde una base de datos no buscas
por página — una petición devuelve hasta 100 fotos, así que buscas por clúster de tema y asignas desde un
pool, con una restricción de unicidad que hace la deduplicación
(esa arquitectura, en detalle).
Y los newsletters y las tarjetas sociales necesitan la misma foto en cuatro recortes: guarda el
photo_id, solicita desde urls el tamaño que necesites, y solo vuelve a buscar
cuando un recorte realmente falla.
El mismo pipeline, como un solo agente
Si un modelo ya está redactando el borrador, el camino más corto es darle directamente la herramienta
de búsqueda en lugar de transportar JSON entre procesos. Pexafy ejecuta un servidor
Model Context Protocol alojado en mcp.pexafy.com/mcp. La
configuración del conector y las herramientas que expone se tratan en otro lugar — la
configuración de escritorio y editor en
la guía de un solo artículo,
la variante headless para un runner de CI en
el artículo sobre contenido a escala,
y cómo es el mercado más amplio de conectores — quién ofrece un servidor de imágenes MCP, en qué
condiciones — en el
estudio sobre infraestructura de búsqueda de imágenes para agentes de IA.
Lo que vale la pena mostrar aquí es qué le ocurre a este pipeline una vez que el agente
dispone de las herramientas: deja de ser cinco etapas y se convierte en una sola instrucción.
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"
Esa última línea es la razón por la que vale la pena tener un agente en este bucle en lugar de un script puro: el modo de fallo de la ilustración automatizada es un brief malo, y reescribir un brief malo es exactamente para lo que sirve un modelo de lenguaje. Mantén las partes deterministas — asignación, deduplicación, la salvaguarda numérica — en código.
Lo que cuesta un artículo ilustrado
Tres slots de foto por artículo significan tres peticiones de búsqueda, así que el plan gratuito (5,000 peticiones/mes) cubre 1,666 artículos al mes antes de que se plantee siquiera pagar — y si agrupas las búsquedas por clúster de tema en lugar de por artículo, ese techo se mueve otro orden de magnitud. El desglose plan por plan, y la arquitectura de agrupación que lo hace irrelevante, están en el artículo complementario sobre ilustrar contenido a gran escala.
Las dos llamadas al modelo por artículo — una para el borrador, otra para el plan visual — son unos pocos miles de tokens y serán la línea más económica del pipeline; los renders de Mermaid y matplotlib no cuestan nada más que segundos de CI. El dato que sorprende a la gente es qué línea no es económica: generar cuatro imágenes por artículo, cuatrocientas imágenes al mes, más los intentos que no sirvieron — y la salida sigue sin llevar ni fotógrafo, ni fecha, ni URL de origen.
Lo que este pipeline no soluciona
Una sección honesta, porque el fallo que previene es costoso. Un artículo bien ilustrado sigue siendo un artículo: fotografía real, gráficos precisos y markup correcto mejoran una página que merece existir. No hacen que el contenido superficial y producido en masa posicione. Las políticas de spam de Google nombran el abuso de contenido a escala — generar muchas páginas principalmente para manipular el posicionamiento y ofreciendo poco valor a los usuarios, haya o no automatización involucrada — y la calidad de las ilustraciones no es un factor en ese juicio.7
Así que el planteamiento que se sostiene: este pipeline es un suelo de calidad que controlas, aplicado a páginas que ya tienen una razón para publicarse. Donde demuestra su valor:
- Procedencia que un lector puede verificar. Una línea de crédito con un fotógrafo real y una
URL de origen es una afirmación que se puede comprobar — y los mismos campos alimentan el markup
ImageObjectque Google lee. - Accesibilidad y Core Web Vitals. Texto alternativo real, dimensiones en cada imagen, un hero que nunca hace lazy-load. Multiplícalo por cada artículo que publiques y esta es la historia de calidad de imagen del sitio.
- Precisión donde es comprobable. Un gráfico cuyos números se verifican contra el artículo, una captura de pantalla generada desde el build en producción, un diagrama revisable como texto en el PR — tres visuales que no pueden desviarse de la verdad sin que falle una prueba.
Sobre la atribución, el punto específico de la canalización es acotado:
el argumento a favor
de mostrar siempre el crédito se desarrolla en otro lugar, y lo que añade un ensamblador
automatizado es que la misma cadena attribution que imprime es el
creditText que necesitan los datos estructurados. Un campo, dos lugares, emitidos en
la misma pasada de la plantilla — razón de más para que una canalización tenga aún menos excusa
que una persona para omitirlo.
Por dónde empezar
- Añade el prompt enrutador a lo que ya escribe tus borradores, e imprime el JSON sin actuar sobre él. Lee diez planes. Si los briefs nombran temas en lugar de escenas, arregla el prompt antes de escribir ninguna integración.
- Conecta solo los slots de foto. Un
GET /search/photospor brief, y guardaphoto_iddesde el primer día — la deduplicación aplicada retroactivamente en 3000 páginas en producción es una migración, no una columna. - Emite width, height y alt en el mismo commit. Es el trabajo de Core Web Vitals más económico que harás jamás.
- Luego añade la etapa 4, diagramas antes que gráficos — Mermaid es texto, así que es la que no tiene otro modo de fallo más allá de un error de sintaxis.
- Añade la salvaguarda numérica antes de que el primer gráfico llegue a un lector, no después.
Referencias y notas al pie
1 Google Search Central, Image SEO best practices: el texto alternativo
es “el atributo más importante a la hora de proporcionar metadatos para una imagen”; la guía
indica crear “contenido útil, rico en información, que use palabras clave de forma adecuada y esté en el contexto
del contenido de la página”, evitar atributos alt rellenos de palabras clave, usar elementos HTML
<img> en lugar de imágenes CSS, y dar a los archivos nombres cortos pero descriptivos.
2 Ley de IA de la UE, artículo 50 — obligaciones de transparencia aplicables desde el 2 de agosto de 2026: los proveedores de sistemas que generan imagen, audio, vídeo o texto sintéticos deben marcar las salidas en un formato legible por máquina y hacerlas detectables como generadas artificialmente. Vincula a proveedores y desplegadores de IA; no es una norma sobre qué imágenes puede publicar un sitio web.
3 Mermaid renderiza diagramas y gráficos a partir de definiciones de texto inspiradas en Markdown, lo que hace que la salida sea revisable y determinista.
4 web.dev, Optimize Largest Contentful Paint: “It's a good
idea to set fetchpriority="high" on an <img> element if you think
it's likely to be your page's LCP element”, usado con moderación; y “Never lazy-load your LCP image,
as that will always lead to unnecessary resource load delay, and will have a negative impact on
LCP.”
5 Google Search Central, Image metadata (structured data):
ImageObject requiere contentUrl más al menos uno de
creator, creditText, copyrightNotice o
license; se recomienda acquireLicensePage, y las imágenes con información de licencia
pueden volverse aptas para la insignia Licensable en Google Images.
6 Google Search Central, Image sitemaps: los sitemaps de imágenes informan a Google sobre las imágenes de un sitio, incluidas las encontradas mediante JavaScript, y aceptan hasta 1000 imágenes por URL de página.
7 Políticas de spam de Google Search — abuso de contenido a escala: generar muchas páginas principalmente para manipular el posicionamiento y ofreciendo poco valor a los usuarios, ya se hayan creado mediante automatización, esfuerzo humano o una combinación de ambos.
Fuentes verificadas el 17 de agosto de 2026: Image SEO best practices · Image metadata · Image sitemaps · Optimize LCP · Search spam policies · AI Act Article 50 · Mermaid · Pexafy API & MCP docs. Los tiempos de búsqueda (147 ms, 144 ms) y cada foto mostrada son respuestas reales de la API capturadas el mismo día.
Preguntas frecuentes
¿Cómo ilustro automáticamente artículos escritos por un LLM?
GET /api/v1/search/photos, unos 150 ms). Las entradas de gráfico y diagrama van a un modelo que escribe código de graficado o Mermaid, renderizado de forma determinista. Nunca uses el título del artículo en una búsqueda de imágenes: los títulos son abstractos y ninguna fotografía los representa.¿Un sitio de documentación debería usar capturas de pantalla o fotos de stock?
¿Cómo evito que un pipeline de IA ponga números incorrectos en un gráfico?
data de la entrada del gráfico. Luego valídalo en código antes de renderizar — para cada valor, comprueba que su cadena de texto aparece en el artículo y lanza un error si no. Nueve líneas, y un gráfico que contradice su propio párrafo nunca podrá llegar a un lector.¿Qué marcado de imagen debería emitir un pipeline automatizado para SEO?
alt descriptivo escrito en el contexto del párrafo — Google llama al texto alternativo el metadato de imagen más importante y advierte contra el relleno de palabras clave. width y height en cada imagen, con fetchpriority="high" en el hero y nunca loading="lazy" en él, porque la imagen LCP no debe cargarse de forma diferida. Y datos estructurados ImageObject con contentUrl, creator, creditText y license, que es lo que hace que una imagen sea elegible para la insignia Licenciable en Google Imágenes.¿Ilustrar artículos escritos por IA ayuda a que posicionen mejor?
¿Cómo ejecuto el paso de ilustración en CI sin perder el control editorial?
photo_id, la verificación de que cada número graficado aparece en el artículo, y un fallo forzado cuando ninguna foto supera el umbral de puntuación. Es mejor no tener imagen principal que tener una incorrecta.