Toma capturas de pantalla de un sitio web con Cloudflare Browser Rendering

La empresa donde trabajo le ha dado mucha importancia al SEO, así que el grupo de desarrollo nos hemos encargado en escuchar ideas, planear proyectos, desarrollarlos y ejecutarlos.

Una de esas ideas ha sido la toma de captura de pantallas de los resultados de Google (SERP), específicamente donde los resultados de la web de la empresa aparezcan.

Para tomar una captura de pantalla de un sitio web usamos Puppeteer. Es una librería en JavaScript que ejecuta una instancia de Google Chrome permitiendo realizar varias interacciones, por ejemplo, de un sitio web podrás generar PDFs, tomar capturas de pantalla, obtener datos e incluso modificar el DOM.

Intentamos instalar la aplicación en Kinsta Apps, sin ningún éxito. Nos retornaba muchos errores de configuración y la verdad no quería perder más tiempo tratando de arreglar esos problemas (me aconsejaron usar Docker).

Fue ahi donde recordé que hace tiempo Cloudflare iba a lanzar un producto beta llamado Browser Rendering.

Cloudflare Browser Rendering

Este es un producto de Cloudflare, en beta, que te permite ejecutar Puppeteer en los servidores de Cloudflare.

Funciona bajo la plataforma de Cloudflare Workers y está desarrollado para que los desarrolladores solo se enfoquen en desarrollar sus aplicaciones, así que no necesitas configurar nada relacionado con Puppeteer.

Proyecto: Captura de pantalla a la primera página de los resultados de Google

Aunque, por este medio, no puedo compartir el código desarrollado en la empresa donde trabajo, si puedo mostrarte cómo implementar Cloudflare Browser Rendering usando nuestro propio proyecto.

Vamos a desarrollar una aplicación de Cloudflare Workers que tome captura de pantalla a la primera página de los resultados de Google usando Cloudflare Browser Rendering.

La lógica del proyecto es la siguiente:

  1. La aplicación se ejecuta haciendo un HTTP GET.
  2. Agregamos el parámetro keyword al enlace que nos dará nuestro Worker.
  3. Aplicar validaciones necesarias.
  4. Si hay algún error, enviar una respuesta JSON.
  5. Iniciar Puppeteer, entrar a la página de Google con la palabra clave indicada.
  6. Tomar la captura de pantalla.
  7. Mostrar la captura de pantalla al navegador del usuario.

Requisitos

Los siguientes requisitos nos ayudarán a desarrollar nuestra aplicación:

  • Una cuenta en Cloudflare.
  • El ID de tu cuenta Cloudflare con permisos (más detalles en su propia sección).
  • Tener instalado npm.
  • Tener instalado Node.js (versión 16.13.0 o mayor).

Permisos a tu Cloudflare ID

Antes de usar Cloudflare Browser Rendering necesitas que tu Account ID tenga acceso a ese producto.

Dentro de tu cuenta Cloudflare, entra al panel principal y del lado derecho verás la sección de API que incluye tu Account ID.

Después tendrás que ir al Discord de Cloudflare, específicamente en este hilo de comentarios, pidiendo acceso al Browser Rendering. Un moderador te dará acceso dentro de 24 horas o menos.

Si no tienes los permisos necesarios, al momento de ejecutar tu código, recibirás el siguiente error:

{
"text": "A request to the Cloudflare API ... failed",
"notes": [{
"text": "workers.api.error.no_access_to_browser_worker [code: 10088]"
}]
}

Desarrollo del proyecto

1. Crea un Cloudflare Worker

En si, todo el proyecto es un Cloudflare Worker. Un Worker es un sistema serverless, es decir, ejecutas tu código en un servidor en la nube con cero configuración en la infraestructura.

Ejecuta el siguiente comando y contesta las preguntas que aparecerán en la terminal. El proyecto puedes llamarlo browser-rendering:

npm create cloudflare@latest

Si te pregunta qué tipo de aplicación deseas crear, escoge la opción “Hello World” Worker. Además, te preguntará si usarás JavaScript o TypeScript. En nuestro caso, usaremos JavaScript.

2. Instala Puppeteer

Dentro de tu proyecto browser-rendering, instala el Puppeteer de Cloudflare. Así es, Cloudflare ha hecho un fork de la librería Puppeteer y le aplicaron algunos cambios para hacerlo funcionar con Workers.

npm install @cloudflare/puppeteer --save-dev

3. Configura el archivo wrangler.toml

Un proyecto de Cloudflare Worker siempre tiene su archivo wrangler.toml, este archivo sirve para configurar tu proyecto con los productos de Cloudflare.

El archivo en tu proyecto debe lucir así:

name = "browser-rendering"
main = "src/worker.js"
compatibility_date = "2023-03-14"
compatibility_flags = [ "nodejs_compat" ]
account_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 
browser = { binding = "MYBROWSER" }

El archivo wrangler.toml está compuesto por el nombre de variables y sus valores. En este archivo todo es importante porque declaramos valores para que nuestro proyecto sea compatible con Node.js, desde donde se ejecuta nuestro proyecto y más.

Ahora, la variable browser contiene el binding con su valor MYBROWSER. Este binding establecerá la conexión entre nuestro Worker con el Chrome ejecutado desde Cloudflare.

Algo que no mencionan al configurar el wrangler.toml, es la variable account_id. Aunque esta variable es opcional, por alguna razón cuando ejecutaba el Worker me pedía que ingresara el account_id. Esta nos permite interactuar directamente con la zona de Cloudflare donde ejecutaremos nuestro Worker.

Puedes ver más configuraciones del archivo wrangler.toml en este enlace.

4. El código

Una vez configurado el archivo wrangler.toml ya podemos empezar a desarrollar nuestra aplicación que tomará capturas de pantalla a un sitio web. El código JavaScript es el siguiente:

import puppeteer from "@cloudflare/puppeteer";
 
/**
* Send error response.
*
* @since 1.0.0
*
* @param {string} message The error message.
* @param {int} status The status code.
* @returns {Response} The response.
*/
function sendError(message, status = 400) {
const json = {
message,
success: false,
};
 
return new Response(JSON.stringify(json), {
status,
headers: { 'content-type': 'application/json;charset=UTF-8' },
});
}
 
/**
* Get the Google URL.
*
* @since 1.0.0
*
* @param {string} keyword The keyword.
* @returns {string} The Google URL.
*/
function getGoogleUrl(keyword) {
const params = {
q: keyword,
gl: 'us',
hl: 'en',
};
 
const urlSearchParams = new URLSearchParams(params);
 
return `https://www.google.com/search?${ urlSearchParams }`;
}
 
export default {
/**
* Fetch the SERP screenshot.
*
* @since 1.0.0
*
* @param {object} request The request.
* @param {object} env The environment variables.
* @returns {Promise<Response>} The response.
*/
async fetch(request, env) {
if (request.method !== 'GET') {
return sendError('Method not allowed.', 405);
}
 
const { searchParams } = new URL(request.url);
let keyword = searchParams.get('keyword');
 
if (!keyword) {
return sendError('You need to specify a keyword.');
}
 
try {
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
 
await page.goto(getGoogleUrl(keyword), { waitUntil: 'load' });
 
const screenshot = await page.screenshot();
 
await browser.close();
 
return new Response(screenshot, {
headers: {
'content-type': 'image/png',
}
});
} catch (e) {
return sendError(e.message);
}
}
}

La función sendError la ejecutamos cuando hay algún tipo de error en nuestras validaciones o cuando Puppeteer arrojó algún otro tipo de error. Por otra parte, obtendremos el enlace de Google para buscar nuestra palabra clave con la función getGoogleUrl.

Y dentro de la función fetch se ejecutará la funcionalidad de Puppeteer. Dentro de esta función ejecutamos validaciones al principio:

  1. Solo debemos aceptar peticiones HTTP GET.
  2. La variable keyword debe existir.

Ahora, la parte que ejecuta la funcionalidad de Puppeteer, preferentemente, debe estar encerrado dentro de un try/catch, para que podamos detectar cualquier error lanzado por Puppeteer.

Y acá viene lo más importante, la constante browser contiene la instancia de Chrome, la cual usaremos para abrir nuevas ventanas y cerrar la conexión. Pero, ¿ya notaste que la función puppeteer.launch() tiene un parámetro?

const browser = await puppeteer.launch(env.MYBROWSER);

Dentro de la función puppeteer.launch() ponemos el binding que hemos configurado en nuestro archivo wrangler.toml.

browser = { binding = "MYBROWSER" }

Esto permitirá la comunicación entre nuestro Worker y Chrome. Después de establecer la comunicación, ahora podemos tomar capturas de pantalla y más.

¡No olvides cerrar la conexión del navegador! Aunque si tu instancia (Chrome) no recibe algún comando durante 60 segundos entonces se cierra automáticamente.

Más información en los límites de la plataforma en este enlace.

5. Prueba la aplicación

Cloudflare Wrangler nos provee de un comando para ejecutar y probar nuestra aplicación:

wrangler dev --remote

El comando tiene que ejecutarse con la opción —remote para que se conecte con los servidores de Cloudflare y ejecute la instancia de Chrome.

Abre el localhost junto con el puerto que te ha dado el output del comando y abrelo en el navegador, por ejemplo:

http://localhost:8787/?keyword=roel%20magdaleno

Verás la imagen en tu navegador en aproximadamente 6 segundos:

Lo que he notado al usar el Cloudflare Browser Rendering es que tarda alrededor de 2 o 3 segundos en abrir la instancia de Chrome y otros 3 segundos para abrir una ventana y realizar la búsqueda.

Si tu objetivo es buscar múltiples palabras claves, entonces modifica el código para que las busque en una sola instancia de Chrome usando un bucle.

6. Sube la aplicación a Cloudflare Workers

Ahora, si deseas subir la aplicación a tu cuenta Cloudflare, solo tienes que ejecutar el siguiente comando:

wrangler deploy

En la terminal verás el enlace generado por el comando y luce así:

https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/?keyword=roel%20magdaleno

Este enlace será el que usaremos para tomar las capturas de pantalla en producción.

¡Y listo! Has desarrollado un Cloudflare Worker que toma capturas de pantallas de un sitio web, sin embargo, como ya sabes, también puedes generar archivos PDF. ¿Cómo usarás esta herramienta?

¿Problemas con el Buffer de Node.js?

Este problema ya ha sido solucionado en nuevas versiones del Cloudflare Browser Rendering pero si por alguna razón te sale un error con el Buffer, quiere decir que la librería de Puppeteer no está detectando esa funcionalidad de Node.js.

Por suerte, alguien en el Discord de Cloudflare sugirió aplicar la siguiente solución:

import { Buffer } from 'node:buffer';
import puppeteer from "@cloudflare/puppeteer";
 
globalThis.Buffer = Buffer;

Declaramos el Buffer justo antes de puppeteer y lo registramos en el globalThis de nuestra aplicación. Reiniciamos la aplicación y debe funcionar perfecto.

Referencias

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

11