Migrando jaimebuilds.com a Astro 7, Tailwind 4 y TypeScript 6
Subí el portfolio al filo del stack de Astro. Cinco cosas se rompieron durante la migración. El peso de la página cayó de unos 100 KB a 13.6 KB. Acá el changelog y los fixes.
Este sitio ahora corre sobre Astro 7.0.3, Tailwind 4.3.1 y TypeScript 6.0.3. La branch del rediseño salió hoy más temprano y el live proof strip del home se estabiliza en 13.6 KB de peso total y 313 ms de carga en un browser desktop.
El upgrade tomó una tarde. Lo interesante no fue subir los version numbers en package.json. Fueron los cinco breaking changes pequeños que el bump arrastró. Ninguno estaba en los release notes que leí al inicio. Todos aparecieron como errores de build o como layout silenciosamente roto.
Comparto el changelog porque alguien más está por hacer este mismo upgrade.
El punto de partida
Antes de la migración el sitio corría sobre:
- Astro 5.16.6 con
output: 'server'en Vercel - Tailwind 3.4 con la integración
@astrojs/tailwindy untailwind.config.ts - React 19.2.3 islands
- TinaCMS para edición de contenido
lenispara smooth scroll,motionpara animaciones, un toggle dark/light,astro-icon,astro-i18next
Acababa también de terminar un rediseño completo (layout editorial suizo, paleta cream warm, copy Shopify Plus-forward) en una rama aparte. Entonces upgrade y rediseño salieron juntos.
1. LegacyContentConfigError
El primer build failure después del bump:
[LegacyContentConfigError] Found legacy content config file in
"src/content/config.ts". Please move this file to "src/content.config.ts"
and ensure each collection has a loader defined.Astro 6 movió las content collections fuera de src/content/config.ts y obligó a que cada colección declarara un loader explícito. La nueva ubicación es src/content.config.ts (sin el directorio content/). El nuevo shape reemplaza type: 'content' por un loader que se importa de astro/loaders.
Antes:
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({ title: z.string(), date: z.coerce.date() }),
});
export const collections = { blog };Después:
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
schema: z.object({ title: z.string(), date: z.coerce.date() }),
});
export const collections = { blog };El sitio tiene nueve colecciones (blog, work, lab por tres idiomas), así que fueron nueve líneas loader: glob(...) y un rename de archivo.
2. post.render() desapareció
Adentro de los [slug].astro, la API antigua era:
const { post } = Astro.props;
const { Content } = await post.render();post.render() se eliminó en Astro 6. El reemplazo es una función render top-level:
import { getCollection, render } from 'astro:content';
const { post } = Astro.props;
const { Content } = await render(post);Mismo comportamiento, distinto import. Nueve archivos [slug].astro necesitaron el cambio.
3. post.slug ahora es post.id
Misma migración. Cuando lees entries con la nueva API de loaders, la propiedad que antes era post.slug ahora es post.id. El shape cambió porque los loaders pueden producir entries que no vienen de archivos, donde “slug” no tenía sentido.
Donde antes escribía:
<a href={`/blog/${post.slug}/`}>{post.data.title}</a>Ahora escribo:
<a href={`/blog/${post.id}/`}>{post.data.title}</a>Para glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }), el id es el path del archivo relativo a base sin extensión, que es exactamente lo que era slug. Cero cambios de URL, solo un rename.
4. @astrojs/tailwind no soporta Astro 6 ni 7
La integración @astrojs/tailwind tiene su peer dependency capada en Astro 5. Para Astro 6+ el camino soportado es el plugin Vite de Tailwind 4: @tailwindcss/vite.
En la práctica:
// astro.config.mjs (antes)
import tailwind from '@astrojs/tailwind';
export default defineConfig({
integrations: [tailwind({ applyBaseStyles: false })],
});
// astro.config.mjs (después)
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
vite: { plugins: [tailwindcss()] },
});Tailwind 4 también eliminó el archivo de config JavaScript a favor de una config CSS-first. El tailwind.config.ts ya no existe. Los design tokens ahora viven dentro de un bloque @theme en tu CSS principal, y Tailwind genera las utilities automáticamente:
@import "tailwindcss";
@plugin "@tailwindcss/forms";
@plugin "@tailwindcss/typography";
@theme {
--color-v2-bg: #f4f1ea;
--color-v2-ink: #161310;
--color-v2-accent: #8a2417;
--font-v2-sans: "Geist", -apple-system, sans-serif;
--font-v2-serif: "Newsreader", Georgia, serif;
}--color-v2-bg: #f4f1ea genera automáticamente bg-v2-bg, text-v2-bg, border-v2-bg y sus variantes. --font-v2-sans genera font-v2-sans. Los plugins se cargan con directivas @plugin en el mismo archivo. El config JS entero desapareció.
5. El que casi hizo ship a un sitio roto
Después de que el build estaba verde y el deploy era live, abrí la preview URL y el home se veía a medio estilizar. Colores y fonts funcionaban. El grid no. El container no tenía max-width. El menú de navegación era invisible. Los CTAs estaban apilados full width en lugar de lado a lado.
El culpable era astro-compress. Corre una segunda pasada de minificación de CSS después de Vite. Con Tailwind 3 era inofensiva. Con Tailwind 4 parece que dropea o rompe utilities que no reconoce como referenciadas, específicamente las arbitrary values (max-w-[1280px], lg:grid-cols-[1.4fr_1fr]) y las variants responsive (hidden md:flex). Las utilities simples como font-medium o border sobrevivían. Las complejas no.
El fix:
compress({
CSS: false,
HTML: true,
Image: true,
JavaScript: true,
SVG: true,
}),Vite ya minifica el bundle CSS correctamente. La segunda pasada era redundante. Con CSS: false el layout volvió.
Vale flaggearlo porque el build estaba verde. El deploy era live. El error no apareció en ningún log. La única señal fue un screenshot de la preview.
Qué se cayó como resultado
El upgrade se convirtió en un buen momento para limpiar la casa. Removí nueve dependencias que el rediseño ya no usaba:
lenisymotion: sin smooth scroll, sin librería de animaciónastro-icony@iconify-json/mdi: solo SVG inlineastro-i18nextyi18next: el rediseño usa paths de idioma hardcoded@astrojs/tailwind: reemplazado por@tailwindcss/vite- El CSS de dark mode y el theme toggle: el rediseño es light only
También borré diecisiete componentes V1 que los nuevos layouts dejaron obsoletos.
Resultado: el live proof strip del home mide 13.6 KB de bytes transferidos para HTML, CSS y JS combinados, y 313 ms de domInteractive en un browser desktop. El sitio anterior estaba en el rango de 80 a 120 KB. Cerca de 7x a 9x de reducción, sobre un rediseño visualmente más pesado que el que reemplazó.
La migración en cinco bullets
- Renombra
src/content/config.tsasrc/content.config.tsy agregaloader: glob({...})a cada colección. - Reemplaza
await post.render()porawait render(post)e importarenderdesde'astro:content'. - Renombra
post.slugapost.iden todos los lugares donde sea un entry de colección (el URLparams: { slug }no cambia). - Saca
@astrojs/tailwind, instala@tailwindcss/vite, borratailwind.config.ts, mueve los tokens a@themeen CSS. - Pon
CSS: falseenastro-compress. Confía en Vite.
Si estás haciendo el mismo upgrade, ese orden fue el que funcionó. La rama completa es pública por si quieres ver el diff: github.com/jaimesolis/jaimebuilds.
Las notas que no entran acá (y el rant ocasional sobre theme builds de Shopify Plus) las publico en X. Si este tipo de debugging te sirve, me puedes seguir ahí: @jaimesolis. Si estás en medio de un upgrade de Astro o una migración a Hydrogen y quieres un segundo par de ojos, el formulario de contacto está a dos clicks.