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.

Compartir
Un programador escribiendo en un escritorio con dos monitores llenos de código, iluminado por lámparas de colores.
Foto vía Unsplash

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:

La forma general — un artículo entra, un artículo publicable sale
┌─ 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.

El prompt enrutador — copiarlo tal cual
# 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:

GET /search/photos — brief del hero · “un desarrollador trabajando de noche en un escritorio con dos monitores…” · 147 ms
El motor ordena por significado, así que la frase larga acota el conjunto en lugar de vaciarlo. Ejecuta esta misma búsqueda →

El brief de la última sección, una escena completamente distinta, en 144 ms:

GET /search/photos — brief de sección · “dos ingenieros de pie frente a una pizarra cubierta de diagramas…” · 144 ms
El mismo artículo, la misma ejecución, una escena que nadie confundiría con el hero — porque el brief se escribió por sección, no por artículo. Ejecuta también esta →

Lo que devuelve cada foto es lo que hace posible la etapa 5 — no solo un archivo:

Un resultado, recortado a los campos que consume el ensamblador
{
  "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.

diagram.sh — el modelo escribió la especificación, la CLI la renderiza
# 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.

chart.py — grafica los datos transcritos, luego verifícalos
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.

El hero, emitido a partir de la respuesta de búsqueda — nada inventado
<!-- 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.

  1. 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 estampa loading="lazy" en cada imagen, hero incluido, es la herida autoinfligida más común en Core Web Vitals en la publicación automatizada.
  2. Emite siempre width y height — 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. Usa blur_hash como placeholder mientras el archivo carga.
  3. Escribe el alt después de la elección, nunca antes. Este es el detalle sutil. El campo alt del 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 del alt_description de 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:

ImageObject JSON-LD, rellenado desde la respuesta de la API
<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:

.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   # 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ó.

plan_to_pr.py — el pegamento: entra el plan visual, sale el front-matter
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.

shots.py — la captura de pantalla se genera, nunca se describe
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.

Una instrucción, todo el plan ejecutado
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 ImageObject que 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

  1. 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.
  2. Conecta solo los slots de foto. Un GET /search/photos por brief, y guarda photo_id desde el primer día — la deduplicación aplicada retroactivamente en 3000 páginas en producción es una migración, no una columna.
  3. 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.
  4. 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.
  5. 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.

Preguntas frecuentes

¿Cómo ilustro automáticamente artículos escritos por un LLM?
Añade una llamada a un modelo entre la redacción y la publicación: pídele que devuelva un plan visual — una entrada por cada espacio de imagen, cada una etiquetada como foto, gráfico, diagrama o nada. Las entradas de foto llevan una descripción de 12 a 25 palabras de una escena que una cámara podría haber captado, que envías a una API de búsqueda semántica de imágenes (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?
Invierte los valores predeterminados del enrutador: en la documentación de producto, el visual honesto es casi siempre tu propia interfaz. Un script de Playwright que abre la build real, fija el viewport y captura el estado exacto que describe el párrafo se ejecuta en el mismo job de CI que la build de la documentación, de modo que una captura de pantalla nunca puede mostrar una versión de la interfaz que ya no existe. Los diagramas sostienen las páginas de arquitectura, y las fotografías aparecen solo en páginas conceptuales y de landing, donde una captura de pantalla no diría nada.
¿Cómo evito que un pipeline de IA ponga números incorrectos en un gráfico?
Haz que el plan transcriba en lugar de inventar: el enrutador solo puede copiar valores que ya aparecen en el borrador al campo 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?
Tres cosas, todas a partir de campos que ya contiene la respuesta de búsqueda. Un 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?
No por sí solo, y vale la pena ser precisos. Las políticas de spam de Google definen el abuso de contenido a escala como generar muchas páginas principalmente para manipular las clasificaciones con poco valor para los usuarios, se use o no automatización; las ilustraciones no cambian esa evaluación. Lo que compra un buen pipeline es un piso de calidad en páginas que ya merecen existir: procedencia verificable, texto alternativo accesible, Core Web Vitals que sobreviven a la automatización, y gráficos y capturas de pantalla que no pueden desviarse de la verdad.
¿Cómo ejecuto el paso de ilustración en CI sin perder el control editorial?
Activa el job en un pull request que modifique tus archivos de contenido, deja que escriba las imágenes y el front-matter, y haz que abra un pull request en lugar de hacer commit directo a la rama — el pipeline propone, un humano aprueba, y cada campo que escribió es comparable mediante diff. Mantén tres cosas deterministas en el código en lugar de en el modelo: la deduplicación por 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.

Deje de buscar palabras clave. Describa lo que quiere decir.

Busque 9M+ imágenes gratuitas por significado — en cualquier idioma, en menos de 100 ms.