Cómo revalido contenidos en este blog con Notion + Next.js + ISR

Problema e idea

Hace poco, refactoricé (palabra que no existe en español) este blog usando Notion como CMS para administrar sus contenidos. La solución anterior, a base de archivos Markdown que servían como fuente de contenido, si bien simple, tenía el problema de que requería primero escribir el contenido en archivos .md y luego realizar un deploy para publicar nuevos contenidos. En resumen: pasaron dos años sin publicar nada porque me daba pereza el proceso.

Pensé entonces, que utilizando una de las mejores features de Next.js, el ISR (Incremental Static Regeneration) junto con Notion para escribir de manera sencilla me “permitíría”, en teoría, publicar mucho más fácilmente.

El proceso mejoró, mucho. Pero me encontré con una dificultad más: cómo poder publicar y probar contenidos nuevos regenerando las páginas sin esperar los 15 días que configuré como periodo de revalidación? Acepto que puedo ser un enfermo con la idea de ahorrar recursos y 15 días es como… mucho; pero incluso 1 hora es un rato tremendamente largo si quieres hacer un cambio y probar cómo queda.

Existe la posibilidad de implementar la feature the Preview Mode, pero está más pensada para CMS's que proveen de esta funcionalidad. Notion no es un CMS per-sé, sino una herramienta de notas con super poderes, por lo tanto no soporta tal cosa (aún). Un poco inspirado en este modo de preview, se me ocurrió que podría crear una herramienta en el mismo sitio que me permita revalidar la página en cuestión. Entonces, creé un componente con una herramienta de administración que me facilitara el proceso con un sólo click.

revalidando-rutas-notion-nextjs-powered-blog-01.png

¿Cómo funciona?

  1. Debe permitirme sólo a mí ejecutar el proceso de revalidación. Esto, por medio de un token privado configurado en una variable de entorno del lado del servidor.
  2. Debe ser fácil de usar: las Admin tools se muestran en todas las páginas del sitio si en Local Storage existe una key adminTools y el token secreto en revalidateSecret.
  3. Un conveniente botón debería disparar la revalidación de la ruta actual mediante un API endpoint y devolver un mensaje con el resultado.

Manos en la masa

Guardando el secreto

Lo primero es setear una variable de entorno en tus archivos .env para almacenar el token el el servidor:

CONTENT_REVALIDATION_TOKEN={some-token-here}

Tip gratis: para generar un token fácilmente, podemos abrir las Dev Tools de tu navegador y en la consola ejecutar: crypto.randomUUID() . Voilà.

El componente con las tools

Vamos a crear un componente sencillo que será quien nos facilite la interacción con los procesos de revalidación. Vamos a obviar la parte de estilos para ir directo al grano.

// components/AdminTools/AdminTools.tsx import useLocalStorage from "@/lib/useLocalStorage"; import { useRouter } from "next/router"; import { useState } from "react"; const AdminTools = () => { const { asPath } = useRouter(); // to get the current route from the Next.js router const [isWorking, setIsWorking] = useState(false); // handle a "working" status const [hasAdminTools] = useLocalStorage("adminTools", false); // gets the flag from Local Storage const [revalidateSecret] = useLocalStorage("revalidateSecret", ""); // gets the secret token from Local Storage const [responseMessage, setResponseMessage] = useState<string>(); // we'll store the response message here // this function will perform the request to our API revalidate route const revalidate = async (path: string) => { setResponseMessage(""); // we want to clear the response each time this runs setIsWorking(true); // set working status to on. // here, we do the request passing the path and the secret in the querystring const response = await fetch( `/api/revalidate?path=${path}&secret=${revalidateSecret}` ).finally(() => setIsWorking(false)); const { message } = await response.json(); if (response.status === 200) { setResponseMessage(`Success :)`); } else { setResponseMessage(message); } }; // the event handler for the button calling the revalidate function const handleRevalidate = () => { revalidate(asPath); }; if (!hasAdminTools) return null; // if the flag is not set, render nothing return ( <section> <h2>Tools</h2> <div> {revalidateSecret && ( <div> <p> Route <code>{asPath}</code> </p> <button disabled={isWorking} // disable the button if it's already working onClick={handleRevalidate} > Revalidate </button> {/* show the response message to the user */} {responseMessage && ( <div> <code>{responseMessage}</code> </div> )} </div> )} </div> </section> ); }; export default AdminTools;

Usamos en el componente anterior un hook useLocalStorage para consumir y setear los valores en el navegador_._ Me costó bastante hacer andar esto con Next.js y sus particularidades con SSR, así que usé uno que encontré por ahí que funciona de maravillas. Si leés esto fedek6, gracias, muchas gracias.

Ahora fui a mi componente que actúa como layout principal <MainLayout /> y agregué el componente arriba de todo. Podríamos también ponerlo en una página en particular o donde más te guste.

Revalidate API route

Esta es la API route que finalmente revalidará la ruta recibida sólo sí el token que tenemos en Local Storage de nuestro navegador coincide con el configurado en el servidor. Podría darse el caso de que alguien descubra nuestra ruta, pero sin el token en cuestión no podrá abusar de ella.

// pages/api/revalidate.ts import { NextApiRequest, NextApiResponse } from 'next/types'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { // Check for secret to confirm this is a valid request if (req.query.secret !== process.env.CONTENT_REVALIDATION_TOKEN) { return res.status(401).json({ message: 'Invalid token' }) } try { await res.revalidate(req.query.path as string) return res.json({ message: 'success' }) } catch (err) { // If there was an error, Next.js will continue // to show the last successfully generated page // return res.status(500).send('Error revalidating') const message = `Error revalidating ${req.query.path}` return res.status(500).json({ message: message }); } }

Y estamos. Con esto cada vez que activemos el botón, la ruta actual será revalidada. Si la ruta en cuestión no implementa ISG o si ocurre algún error en el proceso recibiremos un mensaje de error:

revalidando-rutas-notion-nextjs-powered-blog-02.png

Esto es un ejemplo muy sencillo (pero creo que práctico también). Seguro habrá muchas más formas de implementarlo y mejorarlo; pero esta es la que me sirvió y resultó fácil de implementar a mí.

Si esto te inspira de alguna forma o implementás otra solución me gustaría que me la cuentes en los comentarios o en las redes sociales.

Nos vemos por ahí.

Publicado el 13 de abril de 2023. Esta nota es sobre: spanish, next-js, notion, ISR.