Chatbot con IA que atiende tu web DESDE 49€/MES. Habla con Andie aquí abajo. Chatbot IA DESDE 49€/MES — Habla con Andie aquí abajo. Ver cómo →

Cómo generar las imágenes de Open Graph de tu web sin un navegador headless

Cómo generar las imágenes de Open Graph de tu web sin un navegador headless

Cuando alguien comparte una página tuya en LinkedIn o en WhatsApp, lo que se ve es una imagen de 1200 por 630 con el título encima. Generarla parece un problema de diseño y en realidad es de tipografía: casi todo se reduce a decidir a qué tamaño cabe un titular que no sabes cuánto va a medir. Los ejemplos van en Laravel, que es una de nuestras especialidades en AndorraDev, pero el problema y las decisiones son las mismas en cualquier stack.

Un navegador entero para pintar una imagen

La solución más extendida es maquetar la tarjeta en HTML y hacerle una captura con Puppeteer o Playwright. Funciona, y tiene una lógica difícil de discutir: ya sabes escribir HTML y CSS, así que el diseño te sale en diez minutos.

El precio aparece después. Estás metiendo Node y unos trescientos megas de Chromium en un proyecto PHP, en la imagen de Docker, en el despliegue y en cualquier máquina donde alguien tenga que regenerar las tarjetas. Para un sitio que las genera una vez por artículo, es una infraestructura considerable a cambio de un rectángulo con texto.

La alternativa que usamos es más aburrida: una plantilla SVG con huecos, y un binario que la convierte a PNG.

apt install librsvg2-bin

Eso instala rsvg-convert, que rasteriza un SVG en una llamada. La otra opción es resvg, un binario de Rust autocontenido con mejor soporte de SVG2. Cualquiera de los dos es un ejecutable, no un entorno.

La plantilla es un SVG con huecos

Un SVG es texto, así que la plantilla se rellena con una sustitución de cadenas, igual que una vista.

<text x="80" y="{{title_baseline}}" font-size="{{title_font_size}}" fill="#ffffff">
  {{title_tspans}}
</text>

No hay nada mágico ahí, y esa es la gracia: el diseño lo abres en cualquier editor de vectores y lo tocas sin recompilar nada.

Medir el texto es el trabajo de verdad

Aquí está el noventa por ciento del problema. Un SVG no sabe ajustar texto: no hay overflow, no hay salto de línea automático, no hay "hazlo más pequeño si no cabe". Tienes que decidir tú cuántas líneas ocupa el titular y a qué tamaño, y escribir cada línea como un <tspan>.

Lo que casi todo el mundo hace es estimar por número de caracteres: a 82 píxeles caben veinte, a 72 caben veintitrés. Es rápido y está mal, porque las letras no miden lo mismo. Con un titular normal cuela; con uno lleno de mayúsculas o de palabras anchas, se sale de la tarjeta sin que nadie se entere hasta que la ve publicada.

La medición honesta la hace GD, que ya viene con PHP:

$box = imagettfbbox($size, 0, $fontPath, $text);
$width = abs($box[2] - $box[0]);

Y aquí hay una trampa que nos costó un rato encontrar. GD interpreta el tamaño como puntos y renderiza a 96 ppp, mientras que el font-size de un SVG está en píxeles de usuario. Los anchos salen un tercio inflados. Lo comprobamos rasterizando una muestra y midiendo la mancha de tinta del PNG:

imagettfbbox dice:      940 px
ancho renderizado real: 705 px

Exactamente 0,75, que es 72 partido por 96. Sin convertir, todos los titulares se ajustan más pequeños de lo necesario y cualquier cosa que coloques a partir de un ancho medido cae desplazada. Con la conversión, la misma muestra da 704 contra 705 reales.

Andie recomienda

Comprueba tu medición contra el render de verdad antes de fiarte de ella. Genera una imagen con una cadena conocida, ábrela con cualquier librería y mira el recuadro de tinta. Si no coincide con lo que te dice tu función de medida, tienes un factor de escala escondido y todos tus ajustes están sesgados en la misma dirección.

Colocar un bloque cuya altura no conoces

Un titular puede ocupar una línea o tres, y el SVG no sabe sumar. Si fijas la posición de la primera línea, un titular corto deja un hueco enorme debajo y uno largo se te va contra el pie.

La solución es calcular la línea base en PHP y anclar el bloque por abajo, de forma que la última línea caiga siempre sobre la misma regla. Y exponer también dónde termina el bloque, para que lo que va debajo cuelgue de ahí en vez de una posición fija:

<g transform="translate(0 {{title_bottom}})">
  <text x="80" y="52" font-size="{{subtitle_font_size}}">{{subtitle_tspans}}</text>
</g>

Con eso, un subtítulo guarda la misma distancia con un titular de una línea y con uno de tres, que es lo que hace que una colección de tarjetas parezca diseñada y no generada.

El fondo es una capa más

Esta es la parte que suele empujar a la gente de vuelta al navegador: poner una foto detrás del texto. En SVG no hace falta nada especial, porque una imagen se incrusta como URI de datos.

<image x="0" y="0" width="1200" height="630"
       preserveAspectRatio="xMidYMid slice" href="data:image/jpeg;base64,..."/>

Y como todo acaba siendo un fichero local, da igual de dónde venga: un color plano, una foto de Unsplash descargada y cacheada, o un PNG que hayas generado con un modelo de imagen. La plantilla no cambia. Encima va un degradado que oscurece más el lado del texto que el otro, y el titular se lee sobre cualquier foto.

Una capa opcional tiene que desaparecer, no quedarse vacía

Este nos costó una integración continua en rojo y merece contarse, porque es el tipo de fallo que solo aparece en la máquina que sí tiene el binario.

Si la plantilla lleva el <image> del fondo siempre y le pasas una cadena vacía cuando no hay foto, el SVG resultante contiene esto:

<image x="0" y="0" width="1200" height="630" href="" xlink:href=""/>

Y librsvg aborta. No devuelve un error legible: el proceso muere con la señal 6, que en el mejor de los casos te llega como una excepción rara de tu librería de procesos.

La lección es de diseño, no de librería: "sin fondo" no puede significar "un fondo sin origen". La capa opcional tiene que desaparecer entera de la plantilla, con una sección condicional, no renderizarse vacía.

Las fuentes son el fallo que no avisa

rsvg-convert resuelve font-family a través de fontconfig, así que si tu tipografía de marca no está instalada en la máquina, cae a otra sin decir nada. La tarjeta se genera, no falla nada, y simplemente no se parece al resto de tu web.

Se puede instalar la fuente en la imagen de Docker, pero eso obliga a reconstruirla. La vía que menos molesta es apuntar fontconfig a las fuentes que tu proyecto ya sirve:

<fontconfig>
  <dir prefix="relative">../../public/fonts</dir>
  <dir>/usr/share/fonts</dir>
</fontconfig>

Ese fichero sustituye la configuración del sistema en vez de sumarse a ella, así que hay que listar también los directorios del sistema o te quedas sin ninguna otra fuente. El prefix="relative" resuelve rutas contra el propio fichero, con lo que vale igual en tu portátil que dentro del contenedor.

Y un detalle que se olvida: mide con la misma fuente con la que renderizas. Medir con una y pintar con otra descuadra el ajuste sin que nada lo delate.

Regenerar solo lo que ha cambiado

Con quinientas tarjetas, regenerarlas todas en cada despliegue es tiempo tirado. Lo natural es saltarse las que ya existen, y eso funciona hasta que cambias algo que estaba impreso dentro de una tarjeta que ya está en disco.

Nos pasó con la fecha de publicación: al mover la fecha de un artículo, su tarjeta seguía enseñando la vieja porque el fichero existía. La respuesta correcta no es acordarse de borrarla, es guardar una huella de todo lo que entra en el dibujo, incluida la propia plantilla. Si editas el SVG y la huella no lo contempla, cambias el diseño y no se regenera nada.

Andie recomienda

Genera las tarjetas en local y súbelas al repositorio. Hacerlo bajo demanda te mete un render en el camino crítico de una petición y una dependencia binaria en el contenedor web, a cambio de nada: una tarjeta solo cambia cuando cambia el contenido, y eso lo sabes tú antes que el servidor.

Qué hay en el mercado y en qué se diferencia

Las opciones se separan por cuánta infraestructura te piden a cambio de cuánta libertad de diseño te dan.

Puppeteer o Playwright son la referencia y maquetas en HTML, que es la mayor ventaja que puede tener una solución de estas. A cambio, Node y Chromium en todas partes.

Satori, de Vercel, convierte JSX a SVG y está muy bien resuelto, pero vive en el ecosistema de Node.

Intervention Image y GD a pelo son lo que se acaba usando en PHP para esto. Funcionan si la tarjeta es plana, y se hacen incómodos en cuanto quieres capas, degradados o recortes, porque cada elemento es una llamada de código en lugar de una etiqueta.

Laracards, que es el que mantenemos, apuesta por SVG más un binario: el diseño se edita como un vector y el ajuste de texto va medido. Si ya tienes Node en el proyecto y maquetar en HTML te compensa, Satori o Puppeteer son elecciones perfectamente razonables.

Y si no trabajas con Laravel

Los criterios no dependen del lenguaje:

  • Mide el texto de verdad y comprueba la medición contra el render. Contar caracteres se sale de la tarjeta con el primer titular ancho.
  • Calcula fuera del SVG todo lo que necesite aritmética. Un formato vectorial no sabe sumar, así que las líneas base se resuelven antes.
  • Que una capa opcional desaparezca, no que se quede vacía. Un elemento sin origen es la diferencia entre una imagen y un proceso muerto.
  • Fija las fuentes explícitamente. Un fallback silencioso no rompe nada y te arruina la coherencia de marca.
  • Decide qué regenera una tarjeta. Si es la existencia del fichero, el día que cambies la plantilla no cambiará ninguna.

Dónde está el código

El paquete está en Packagist, el código en GitHub y la documentación del paquete recoge las plantillas, los fondos y la configuración. Es MIT y no depende de nada más que de GD y un binario.

Las tarjetas de este blog están hechas así, y lo trabajamos en desarrollo Laravel y diseño web. Si compartes contenido a menudo y tus enlaces salen sin imagen o con una genérica, cuéntanos cómo publicas y te decimos qué encaja.

Escrito por
Edu Lazaro
Edu Lazaro
Founder & Lead Developer en AndorraDev

Desarrollador full-stack con más de 15 años de experiencia en Laravel, React, Node.js y arquitecturas cloud. Ayudo a empresas en Andorra a construir su presencia digital.

Partner de diseño · ionospace.
Necesitas ayuda? ×
Andie by AndorraDev
Asistente IA + equipo humano
Asistente IA de AndorraDev
Andie
Hola! Soy Andie, el asistente IA de AndorraDev. ¿En qué puedo ayudarte? Si necesitas hablar con Edu, solo pídelo.
18:32