Marcos Ramírez BETA
Editor de código mostrando un componente de un generador de sitios estáticos con un enlace interno resaltado

FutureLink: enlazar en Astro posts que aún no has publicado

· ⏱ 8+ min lectura

Tengo el blog lleno de posts programados que se enlazan entre sí. Series, secuelas, referencias cruzadas. Y eso, que sobre el papel es buenísimo para el lector y para el SEO, esconde una trampa de la que no te das cuenta hasta que ya la has metido.

El problema es de calendario. Si un post sale el martes y enlaza a otro que sale el jueves, durante dos días ese enlace apunta a una página que todavía no existe. Un 404 interno. Justo el tipo de detalle que nadie ve hasta que Google sí lo ve.

Me hartó lo suficiente como para montarme un componente. Se llama FutureLink, ocupa cuatro líneas contadas y resuelve el problema de raíz. Te cuento por qué lo hice y cómo funciona.

El problema: enlazar a algo que aún no está publicado

Este blog corre sobre Astro y publica con un calendario. Los borradores se programan a una fecha futura y el sitio se reconstruye solo para que cada post salga a su hora.

Eso choca de frente con los enlaces cruzados. Hace nada escribí dos posts que se referencian mutuamente: uno sobre GTD y time blocking y otro sobre cómo dejar que otros agenden reuniones contigo. El primero sale un día, el segundo al día siguiente. Me interesaba que cada uno enlazara al otro, porque se complementan.

Y ahí está el lío. El día que se publica el primero, el segundo todavía es un borrador. Si meto un enlace normal, ese enlace lleva a una URL que aún no existe. Durante 24 horas, cualquiera que haga clic se come un 404, y cualquier rastreador que pase por ahí registra un enlace interno roto.

Lo mismo me pasaba con una serie larga de dieciséis entregas, una cada sábado. El índice los enlaza todos desde el primer día, pero quince de ellos no existen aún cuando el índice se publica.

Por qué un 404 interno es peor de lo que parece

Podría parecer una tontería. “Total, en dos días el enlace ya funciona.” Pero los enlaces internos rotos hacen más daño del que aparentan.

Para el lector es una mala experiencia directa: clica esperando algo y aterriza en una página de error. Pierde la confianza, y a veces se va.

Para el SEO es peor, porque es silencioso. Los rastreadores siguen tus enlaces internos para entender la estructura del sitio y repartir autoridad entre páginas. Un enlace que apunta a un 404 es autoridad que se tira a la basura y una señal de que el sitio no está bien mantenido. Si encima el rastreo coincide justo con esa ventana en la que el destino no existe, te quedas con el enlace marcado como roto hasta el siguiente pase.

No me apetecía tener que acordarme de volver a editar el post para “activar” el enlace cuando el destino saliera. Eso es trabajo manual, y el trabajo manual que depende de tu memoria, antes o después, se te olvida.

La idea: preguntar en build si el destino ya existe

La clave es que Astro genera el sitio en tiempo de build, y en ese momento tiene acceso a toda la colección de posts. O sea, puedo preguntarle: “¿el post al que quiero enlazar ya está publicado?”. Y decidir en función de la respuesta.

La lógica es simple:

  • Si el post destino existe en la colección y su fecha de publicación ya ha pasado, está vivo: pinto un enlace normal.
  • Si todavía no (sigue en borradores o tiene fecha futura), pinto solo el texto, sin enlace.

Así el contenido se lee igual en los dos casos. La única diferencia es que el texto va envuelto en una etiqueta de enlace o no. Cero 404, cero enlaces rotos, cero mantenimiento manual.

El código entero

Esto es todo. Lo tienes también en este gist por si lo quieres copiar de un sitio limpio.

---
// Enlace interno a un post que puede no estar publicado todavía.
import { getCollection } from 'astro:content';

interface Props {
  href: string;
  class?: string;
}
const { href, class: className } = Astro.props;

const targetSlug = href.replace(/^\//, '').replace(/\/$/, '');

const livePosts = await getCollection('posts', (p) => p.data.date <= new Date());
const isLive = livePosts.some((p) => {
  const slug =
    p.data.slug ??
    p.id.replace(/^\d{4}\/\d{4}-\d{2}-\d{2}-/, '').replace(/\.mdx$/, '');
  return slug === targetSlug;
});
---
{isLive ? (
  <a href={href} class={className}><slot /></a>
) : (
  <slot />
)}

Vamos por las partes que importan.

getCollection('posts', ...) trae todos los posts, y el filtro p.data.date <= new Date() se queda solo con los que ya tienen fecha de publicación pasada. Eso descarta de golpe los que están programados para el futuro, que es justo lo que quiero.

Luego comparo el slug del destino con el de cada post vivo. Uso el campo slug del frontmatter si existe, y si no, lo deduzco del nombre del archivo quitándole la fecha y la extensión. Ese fallback me cubre los posts antiguos que no llevan slug explícito en el frontmatter.

Y el final es lo más bonito por lo tonto que es: si isLive es verdadero, un ancla normal; si no, un simple <slot />, que en Astro es el contenido que metas entre las etiquetas. El texto se renderiza tal cual, sin enlace, indistinguible del resto del párrafo.

Se activa solo, sin que yo toque nada

Aquí está la parte que lo hace redondo, y que depende de cómo despliego.

El sitio no se construye solo cuando hago push. Se reconstruye también por un cron cada pocos minutos. Eso significa que la comprobación de “¿está vivo el destino?” no se hace una vez y para siempre: se rehace en cada build.

¿La consecuencia? El día que el post destino se publica, en el siguiente build el isLive pasa a ser verdadero, y ese texto plano se convierte en un enlace de verdad. Solo. Sin que yo edite nada, sin acordarme de volver, sin un script aparte.

Escribo el enlace una vez, con su texto definitivo, y me olvido. Mientras el destino no exista, es texto. En cuanto existe, es enlace. Esa es toda la magia, y no hay magia: es solo aprovechar que el build se repite.

Cómo lo uso

Lo metí primero en esos dos posts que se enlazaban con un día de diferencia. Y luego me di cuenta de que tenía un componente viejo, hecho a medida para la serie de posts, que intentaba resolver lo mismo pero peor: cuando el destino no estaba, en vez de texto plano pintaba un “disponible próximamente” con un estilo apagado.

No me gustaba. Ensuciaba la lectura y cantaba demasiado. Lo que buscaba era justo lo contrario: que cuando el enlace no estuviera disponible, no se notara nada. Solo el texto, limpio.

Así que jubilé el componente viejo y dejé FutureLink como el único para esto. Ahora lo usa la serie financiera entera y cualquier referencia cruzada que escriba de aquí en adelante. Un solo componente, un solo comportamiento, y enlaces que se cosen solos a medida que el calendario avanza.

La moraleja, si es que hay alguna: cuando una herramienta te obliga a acordarte de algo a mano, casi siempre puedes mover ese “acuérdate” al build y olvidarte para siempre. Cuatro líneas bien puestas valen más que una nota mental que vas a ignorar.

Preguntas frecuentes

¿FutureLink sirve solo para Astro?

La idea sirve para cualquier generador de sitios estáticos que tenga acceso a la colección de contenidos en tiempo de build (Astro, Eleventy, Hugo y similares). El código concreto que muestro usa la API de Astro (getCollection y astro:content), así que para otro framework tendrías que adaptar esa parte, pero la lógica de “comprueba si el destino está vivo y decide entre enlace o texto” es la misma.

¿Por qué no usar simplemente un enlace normal y arreglarlo después?

Porque “arreglarlo después” depende de que te acuerdes, y el trabajo manual basado en memoria se olvida. Además, durante la ventana en la que el enlace está roto, un rastreador puede pasar y registrar el 404 interno. FutureLink elimina esa ventana por completo, sin intervención.

¿Qué pasa si el post destino no se publica nunca?

Nada malo: se queda como texto plano para siempre, que es exactamente el comportamiento seguro. Nunca vas a tener un enlace roto, pase lo que pase con el destino.

¿El cron es imprescindible para que funcione?

Para la activación automática, sí ayuda mucho. Si tu sitio solo se reconstruye cuando haces push, el enlace se activará en el primer build posterior a la publicación del destino. Con un cron periódico, esa activación es automática sin que tengas que hacer nada.

Fuentes

Compártelo si te ha resultado útil, sobre todo con alguien que lleve un blog con calendario editorial.

¿Tú cómo manejas los enlaces a contenido que aún no has publicado, o nunca te habías parado a pensarlo? Cuéntame.

Y… el resto de enlaces, que se cosan solos.

Artículos relacionados

Un único almacén central de paquetes enlazado por flechas a varios proyectos de Node.js

⚡ Por qué deberías usar pnpm en vez de npm y migrar hoy mismo

Llevo años viendo discos llenos de carpetas node_modules clonadas mil veces y esperas eternas en cada npm install. pnpm resuelve las dos cosas: guarda cada dependencia una sola vez en disco y enlaza por hard links, instala en una fracción del tiempo y te protege de las phantom dependencies. Te cuento por qué deberías cambiarte, qué tiene de truco y cómo dejar que tu memoria muscular siga escribiendo npm con un alias que funciona en todos los sistemas operativos.

08:30 7 min Marcos Ramírez Lucía