Volver al blog

Construyendo con Astro 5 y Tailwind CSS v4

24 de febrero de 2026

ℹ️
Este post documenta la configuración exacta que usé para construir este portfolio. Astro 5 + Tailwind v4 es una combinación relativamente nueva — algunos patrones que encontrarás en Google ya están desactualizados.

Cuando empecé a construir este portfolio, quería algo rápido, sencillo y sin demasiada magia. Astro fue la elección obvia: genera HTML estático, tiene soporte nativo para Markdown, y se lleva bien con Tailwind.

Tailwind v4: sin tailwind.config.js

La gran novedad de Tailwind CSS v4 es que ya no necesitas un archivo de configuración. La integración se hace directamente desde Vite:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  site: "https://josluistanic.com",
  vite: {
    plugins: [tailwindcss()],
  },
});

Y en tu CSS global, solo necesitas una línea:

@import "tailwindcss";
⚠️
¡Ojo con los tutoriales viejos!
La mayoría de guías de Astro + Tailwind que encontrarás usan @astrojs/tailwind o el setup con tailwind.config.js. Ambos son el approach antiguo. En v4, ni el paquete @astrojs/tailwind es necesario.

Fuentes con Fontsource

Para la tipografía uso Onest Variable desde @fontsource-variable/onest. Solo hay que instalar el paquete e importarlo en el layout:

import "@fontsource-variable/onest";

Y aplicarlo en CSS:

html {
  font-family: "Onest Variable", system-ui, sans-serif;
}

Fontsource sirve las fuentes localmente (sin petición a Google Fonts), lo que mejora privacidad y rendimiento.

Content Layer API en Astro 5

Para el blog usé la nueva Content Layer API con el loader glob. La diferencia clave respecto a Astro 4:

Astro 4 (legacy) Astro 5 (Content Layer)
src/content/blog/ implícito loader glob() explícito
post.slug post.id
post.render() render(post) desde astro:content
// src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()),
  }),
});

export const collections = { blog };

Resultado

✅ Lo que consigues con este stack

  • HTML estático, sin JS en el cliente
  • Modo oscuro sin flicker (FOUC prevention)
  • Blog en Markdown con tipado Zod
  • RSS feed incluido
  • Deploy automático a cPanel vía GitHub Actions