De vez en cuando un lector era el primero en avisarme que un enlace en una entrada vieja estaba muerto —un proyecto que había enlazado y que ya no existía, una página de documentación que se mudó. Esa es una mala forma de enterarse. La solución no fue un servicio ni un plugin, fue un script lo bastante chico como para caber en un solo archivo.
import { readdir, readFile } from 'node:fs/promises';
const files = await readdir('./src/content/blog');
const linkPattern = /\[.*?\]\((https?:\/\/[^)]+)\)/g;
for (const file of files) {
const text = await readFile(`./src/content/blog/${file}`, 'utf8');
for (const [, url] of text.matchAll(linkPattern)) {
const res = await fetch(url, { method: 'HEAD' }).catch(() => null);
if (!res || res.status >= 400) {
console.log(`${file}: ${url} -> ${res?.status ?? 'no response'}`);
}
}
}
No es ingenioso. Lee cada archivo markdown, extrae cada enlace externo con una expresión regular más permisiva que del todo correcta, y hace una petición HEAD contra cada uno. Corre como una verificación programada en lugar de en cada build, ya que un enlace que hoy está bien puede morir en su propio horario, sin relación con nada que yo publique.
La primera corrida real señaló seis enlaces muertos en entradas de dos años atrás, uno de los cuales llevaba tanto tiempo muerto que el dominio ahora redirigía a una tienda sin ninguna relación. Cincuenta líneas de código nada glamoroso encontraron algo que si no me habría quedado ahí, avergonzándome en silencio, hasta que un lector lo mencionara —que es más o menos el mejor retorno de cincuenta líneas que le he sacado a nada en este sitio.