Generar imágenes dinámicas de Open Graph en Next.js
Este tutorial está escrito utilizando el page router, es posible que los pasos a seguir sean algo diferentes en el app router.
Si alguna vez has puesto atención, hay una funcionalidad muy útil en la mayoría de sitios de social media como Facebook, o Twitter X, y es que cuando se escribe una dirección ésta automáticamente carga una vista previa del contenido de dicha dirección, siendo en la mayor parte de las veces una tarjeta con una imagen y el título de la publicación, por ejemplo:
Estas tarjetas son muy útiles porque puedes mostrar una vista previa del artículo para que tus lectores sepan fácil y rápido de que es lo que se va a tratar tu enlace, en la mayoría de los casos puedes utilizar imágenes personalizadas para cada artículo, pero esto llega a ser tedioso (además de que lógicamente consumirá tiempo,) así que para éste blog decidí que quería utilizar un template simple y que únicamente cambiara el texto que se muestra en la imagen.
Por supuesto, podría guardar el template en Photoshop y simplemente actualizarlo con un nuevo texto cada vez que publique un nuevo artículo, pero esto me trae dos problemas:
- Mencionado anteriormente, es la pérdida de tiempo en la que resulta crear una nueva imagen para cada post
- Cada imagen necesita ser subida al servidor, lo que genera pérdida de espacio
Por fortuna, encontré una forma bastante sencilla de crear imágenes dinámicas para Open Graph en Next.js, a continuación muestro cómo logré el resultado de este Blog.
¿Open Graph?
Open Graph es un protocolo originalmente creado por Facebook para estandarizar el uso de metadata (datos de un sitio,) para uso de otros sitios, en otras palabras permite que muchos sitios consuman algunos datos de tu sitio de una forma predecible, para no tener que adivinar de donde sacar cada cosa.
No entraré mucho en detalles sobre elk protocolo en sí, ya que sólo necesitamos un título y una imagen para este tutorial, pero si tienes cualquier pregunta no dudes en contactarme ☺️️.
PASO 1: CREAR LA RUTA EN REST
A grandes rasgos, lo que vamos a hacer es crear una ruta en rest que genere la imagen que necesitamos automáticamente con base en el título que enviemos, así simplemente en lazaremos la ruta con los parámetros necesarios a nuestra etiqueta de open graph.
Comenzamos creando nuestro archivo, este debe ir ubicado en pages/api/og.tsx
.
Para crear una ruta, lo único que necesitamos es agregar el siguiente código a nuestro archivo:
1/**2 * Next.js dependencies3 */4import { NextApiRequest, NextApiResponse } from 'next';56function handler(7 req: NextApiRequest,8 res: NextApiResponse9) {10 res.status(200).json({ message: '¡Hola Mundo!' })11}1213export default handler;1415
Ahora si abrimos la ruta en http://localhost:3000/api/og nos encontraremos con un pequeño código JSON, que es justo lo que hemos enviado a nuestra ruta:
1{"message": "¡Hola Mundo!"}2
Aquí es justo donde colocaremos nuestras imágenes, así que ahora que nuestra ruta está creada lo que debemos hacer es utilizar la magia de ImageResponse
1
¿Y mis estilos?
Habrás notado de nuestra respuesta en JSON en el navegador no tiene ningún otro componente o ninguno de los estilos que debería tener nuestro sitio. Esto sucede porque el folder /pages/api
es especial en Next.js, le dice al compilador que todo lo que se encuentre adentro está en una sección aparte de nuestro sitio (las rutas en rest) y que deberá formar su propia respuesta.
PASO 2: CONVERTIR UN COMPONENTE EN IMAGEN
Lo que hace ImageResponse
, a grandes rasgos, es convertir cualquier componente en una imagen antes de mostrarlo en el navegador. Esto es precisamente lo que necesitamos ya que nos permite colocar el título que queramos dentro de nuestra imagen, aquí es donde sucede la magia.
Partiendo de nuestro código anterior, haremos algunas modificaciones para regresar una imagen en lugar de nuestro JSON:
1/**2 * Next.js dependencies3 */4import { ImageResponse } from 'next/og';56/**7 * Necesario para evitar un error8 * al ejecutar en el runtime de Node.js9 */10export const config = {11 runtime: 'edge',12};1314function handler() {15 return new ImageResponse(16 (17 <div style={{18 alignItems: 'center',19 backgroundColor: 'rgb(24 24 27/1)',20 color: 'white',21 display: 'flex',22 justifyContent: 'center',23 height: '100%',24 width: '100%',25 }}>26 <h1>¡Hola Mundo!</h1>27 </div>28 ),29 {30 width: 1200,31 height: 630,32 }33 );34}3536export default handler;37
Cuando vemos esto:
1{2 width: 1200,3 height: 630,4}5
Nos referimos al tamaño que tendrá la imagen final, esto puede ser cualquier tamaño que quieras pero 1200x630
es común en Open Graph.
Ahora si volvemos a abrir nuestra página de API, podremos ver esto en el navegador:
Como seguramente the habrás dado cuenta, estamos utilizando el atributo style
directamente en componente. Esto sucede porque como no podremos cargar ningún tipo de estilos en la imagen, debemos incluirlos directamente en el componente.
Esto funciona muy bien en la mayoría de los casos, pero si tiene limitantes, asegúrate de revisar la documentación para tener una lista de propiedades funcionales.
En algunos casos, cuando regresas más de un componente, si llegas a tener un error y la imagen no carga, prueba agregando explícitamente display: flex
al elemento padre (como yo hice en el ejemplo anterior). Los errores pueden ser bastante difíciles de encontrar pero encontrarás un log en la consola si estás en modo de desarrollo.
PASO 3: DANDO ESTILO A NUESTRA IMAGEN
Ya casi tenemos todo listo, ya solo necesitamos agregar un poco más de estilo a nuestra imagen, y como simplemente convertiremos un componente en imagen, no hay nada que nos impida agregar otra imagen también:
1/**2 * Next.js dependencies3 */4import { ImageResponse } from 'next/og';56/**7 * La etiqueta img necesita una URL absoluta8 */9const SITE_URL = 'https://marioaguiar.net';1011/**12 * Necesario para evitar un error13 * al ejecutar en el runtime de Node.js14 */15export const config = {16 runtime: 'edge',17};1819function handler() {20 return new ImageResponse(21 (22 <div23 style={{24 backgroundColor: 'rgb(24 24 27/1)',25 display: 'flex',26 fontFamily: 'Raleway, sans-serif',27 gap: 16,28 }}29 >30 <div31 style={{32 alignItems: 'center',33 color: 'white',34 display: 'flex',35 flexDirection: 'column',36 fontSize: 36,37 height: 630,38 justifyContent: 'center',39 padding: 16,40 textAlign: 'center',41 width: 800,42 }}43 >44 <h145 style={{46 fontWeight: 600,47 }}48 >49 ¡Hola Mundo!50 </h1>5152 <p>53 marioaguiar.net54 </p>55 </div>5657 <div58 style={{59 display: 'flex',60 alignItems: 'center',61 justifyContent: 'center',62 }}63 >64 <img65 width={400}66 height={400}67 src={`${SITE_URL}/mariobw-og.jpg`}68 alt='Mario Aguiar'69 />70 </div>71 </div>72 ),73 {74 width: 1200,75 height: 630,76 }77 );78}7980export default handler;81
PASO 4: AGREGANDO LOS DATOS DEL POST
Por último (y el punto de todo el tutorial en realidad,) debemos agregar los datos de nuestro post a la imagen, y para ello simplemente debemos modificar un poco el código para recibir parámetros y utilizarlos en la imagen:
1/**2 * Next.js dependencies3 */4import { ImageResponse } from 'next/og';5import { NextApiRequest } from 'next';67/**8 * La etiqueta img necesita una URL absoluta9 */10const SITE_URL = 'https://marioaguiar.net';1112/**13 * Necesario para evitar un error14 * al ejecutar en el runtime de Node.js15 */16export const config = {17 runtime: 'edge',18};1920async function handler(req: NextApiRequest): Promise<ImageResponse> {21 // Recibe los parámetros de la URL.22 const { searchParams } = new URL(req.url || '');23 const title = searchParams.get('title') || 'Mario Aguiar';2425 return new ImageResponse(26 (27 <div28 style={{29 backgroundColor: 'rgb(24 24 27/1)',30 display: 'flex',31 gap: 16,32 fontFamily: 'Raleway, sans-serif',33 }}34 >35 <div36 style={{37 display: 'flex',38 flexDirection: 'column',39 fontSize: 36,40 alignItems: 'center',41 justifyContent: 'center',42 width: 800,43 height: 630,44 color: 'white',45 padding: 16,46 textAlign: 'center',47 }}48 >49 <h150 style={{51 fontWeight: 600,52 }}53 >54 {55 <span style={{56 textTransform: 'uppercase',57 }}>58 {title}59 </span>60 }61 </h1>6263 <p>64 marioaguiar.net65 </p>66 </div>6768 <div69 style={{70 display: 'flex',71 alignItems: 'center',72 justifyContent: 'center',73 }}74 >75 <img76 width={400}77 height={400}78 src={`${SITE_URL}/mariobw-og.jpg`}79 alt='Mario Aguiar'80 />81 </div>82 </div>83 ),84 {85 width: 1200,86 height: 630,87 }88 );89}9091export default handler;92
Y finalmente, si entramos a /api/og?title=lorem ipsum
tendremos nuestro resultado final:
BONUS: MEJORANDO LA TIPOGRAFÍA
Sinceramente pensaba en terminar el tutorial aquí, pero justo antes de sentarme a escribir hoy, descubrí que además que algunos pocos estilos que utilizamos, ImageResponse
también acepta una versión un poco más básica de edición de tipografía, no sé que tan recomendable sea ya que hay que hay que tener en cuenta cuestiones de performance, pero aún así mostraré como se hace.
Para esto, es necesario tener el archivo de la fuente en algún lugar de nuestro servidor (o acceso a un cdn también podría funcionar.) Una vez que escogemos nuestra fuente, debemos cargar los contenidos del archivo en Javascript, y pasarla a ImageResponse
en la configuración:
1/**2 * Next.js dependencies3 */4import { ImageResponse } from 'next/og';5import { NextApiRequest } from 'next';67/**8 * La etiqueta img necesita una URL absoluta9 */10const SITE_URL = 'https://marioaguiar.net';1112/**13 * Necesario para evitar un error14 * al ejecutar en el runtime de Node.js15 */16export const config = {17 runtime: 'edge',18};1920async function handler(req: NextApiRequest): Promise<ImageResponse> {21 const { searchParams } = new URL(req.url || '');22 const title = searchParams.get('title') || 'Mario Aguiar';2324 // Cargamos el contenido de la tipografía en buffer.25 const ralewayBlack = await fetch( new URL('./fonts/Raleway-Black.ttf', SITE_URL) )26 .then((res) => res.arrayBuffer());2728 return new ImageResponse(29 (30 <div31 style={{32 backgroundColor: 'rgb(24 24 27/1)',33 display: 'flex',34 gap: 16,35 // Especificamos la tipografía a utilizar.36 fontFamily: 'Raleway, sans-serif',37 }}38 >39 <div40 style={{41 display: 'flex',42 flexDirection: 'column',43 fontSize: 36,44 alignItems: 'center',45 justifyContent: 'center',46 width: 800,47 height: 630,48 color: 'white',49 padding: 16,50 textAlign: 'center',51 }}52 >53 <h154 style={{55 fontWeight: 600,56 }}57 >58 {59 <span style={{60 textTransform: 'uppercase',61 }}>62 {title}63 </span>64 }65 </h1>6667 <p>68 marioaguiar.net69 </p>70 </div>7172 <div73 style={{74 display: 'flex',75 alignItems: 'center',76 justifyContent: 'center',77 }}78 >79 <img80 width={400}81 height={400}82 src={`${SITE_URL}/mariobw-og.jpg`}83 alt='Mario Aguiar'84 />85 </div>86 </div>87 ),88 {89 width: 1200,90 height: 630,91 fonts: [92 // Agregamos los datos de nuestra tipografía.93 { data: ralewayBlack, name: 'Raleway,' weight: 900 },94 ]95 }96 );97}9899export default handler;100
Y con esto, tendremos una tipografía un poco más personalizada:
CONCLUSIÓN
Y con esto, hemos terminado, espero que este tutorial te haya sido de ayuda, y si tienes alguna pregunta no dudes en contactarme. ¡Nos vemos!