Skip to main content

Generar imágenes dinámicas de Open Graph en Next.js

Publicado hace
Actualizado hace
10 minutos de lectura
Precaución

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:

Ejemplo de tarjeta de Open Graph
Ejemplo de tarjeta de Open Graph

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:

  1. Mencionado anteriormente, es la pérdida de tiempo en la que resulta crear una nueva imagen para cada post
  2. 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.

Nota

¿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 dependencies
3 */
4import { NextApiRequest, NextApiResponse } from 'next';
5
6function handler(
7 req: NextApiRequest,
8 res: NextApiResponse
9) {
10 res.status(200).json({ message: '¡Hola Mundo!' })
11}
12
13export default handler;
14
15

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

Nota

¿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 dependencies
3 */
4import { ImageResponse } from 'next/og';
5
6/**
7 * Necesario para evitar un error
8 * al ejecutar en el runtime de Node.js
9 */
10export const config = {
11 runtime: 'edge',
12};
13
14function 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}
35
36export default handler;
37
Consejo

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:

Ejemplo de ImageResponse
Imagen real generada por `ImageResponse`
Nota

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.

Importante

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.

Ejemplo de error enterrado en log
Ejemplo de error enterrado en el log

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 dependencies
3 */
4import { ImageResponse } from 'next/og';
5
6/**
7 * La etiqueta img necesita una URL absoluta
8 */
9const SITE_URL = 'https://marioaguiar.net';
10
11/**
12 * Necesario para evitar un error
13 * al ejecutar en el runtime de Node.js
14 */
15export const config = {
16 runtime: 'edge',
17};
18
19function handler() {
20 return new ImageResponse(
21 (
22 <div
23 style={{
24 backgroundColor: 'rgb(24 24 27/1)',
25 display: 'flex',
26 fontFamily: 'Raleway, sans-serif',
27 gap: 16,
28 }}
29 >
30 <div
31 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 <h1
45 style={{
46 fontWeight: 600,
47 }}
48 >
49 ¡Hola Mundo!
50 </h1>
51
52 <p>
53 marioaguiar.net
54 </p>
55 </div>
56
57 <div
58 style={{
59 display: 'flex',
60 alignItems: 'center',
61 justifyContent: 'center',
62 }}
63 >
64 <img
65 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}
79
80export default handler;
81
Ejemplo de Imagen con estilos

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 dependencies
3 */
4import { ImageResponse } from 'next/og';
5import { NextApiRequest } from 'next';
6
7/**
8 * La etiqueta img necesita una URL absoluta
9 */
10const SITE_URL = 'https://marioaguiar.net';
11
12/**
13 * Necesario para evitar un error
14 * al ejecutar en el runtime de Node.js
15 */
16export const config = {
17 runtime: 'edge',
18};
19
20async 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';
24
25 return new ImageResponse(
26 (
27 <div
28 style={{
29 backgroundColor: 'rgb(24 24 27/1)',
30 display: 'flex',
31 gap: 16,
32 fontFamily: 'Raleway, sans-serif',
33 }}
34 >
35 <div
36 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 <h1
50 style={{
51 fontWeight: 600,
52 }}
53 >
54 {
55 <span style={{
56 textTransform: 'uppercase',
57 }}>
58 {title}
59 </span>
60 }
61 </h1>
62
63 <p>
64 marioaguiar.net
65 </p>
66 </div>
67
68 <div
69 style={{
70 display: 'flex',
71 alignItems: 'center',
72 justifyContent: 'center',
73 }}
74 >
75 <img
76 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}
90
91export default handler;
92

Y finalmente, si entramos a /api/og?title=lorem ipsum tendremos nuestro resultado final:

Ejemplo con título

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 dependencies
3 */
4import { ImageResponse } from 'next/og';
5import { NextApiRequest } from 'next';
6
7/**
8 * La etiqueta img necesita una URL absoluta
9 */
10const SITE_URL = 'https://marioaguiar.net';
11
12/**
13 * Necesario para evitar un error
14 * al ejecutar en el runtime de Node.js
15 */
16export const config = {
17 runtime: 'edge',
18};
19
20async function handler(req: NextApiRequest): Promise<ImageResponse> {
21 const { searchParams } = new URL(req.url || '');
22 const title = searchParams.get('title') || 'Mario Aguiar';
23
24 // 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());
27
28 return new ImageResponse(
29 (
30 <div
31 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 <div
40 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 <h1
54 style={{
55 fontWeight: 600,
56 }}
57 >
58 {
59 <span style={{
60 textTransform: 'uppercase',
61 }}>
62 {title}
63 </span>
64 }
65 </h1>
66
67 <p>
68 marioaguiar.net
69 </p>
70 </div>
71
72 <div
73 style={{
74 display: 'flex',
75 alignItems: 'center',
76 justifyContent: 'center',
77 }}
78 >
79 <img
80 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}
98
99export default handler;
100

Y con esto, tendremos una tipografía un poco más personalizada:

Ejemplo con tipografía personalizada
Ejemplo utilizando la tipografía Raleway

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!