Construindo um Blog com Astro 6 e Tailwind


Bem-vindo ao meu blog! Neste post vou mostrar como montar um blog moderno do zero usando Astro 6 e Tailwind CSS v4, com dark mode e table of contents automático.

Por que Astro?

Astro é um framework focado em performance. Ele gera HTML estático por padrão e só envia JavaScript quando necessário — o famoso “Zero JS by default”.

Algumas vantagens que me convenceram:

  • Build super rápido (Vite por baixo)
  • Suporte nativo a Markdown e MDX
  • Content Collections com type-safety
  • Integração fácil com Tailwind, React, Vue, etc.

Comparando com Next.js

Next.js é ótimo para apps dinâmicos, mas para um blog ele manda JavaScript demais para o cliente. Astro entrega páginas quase 100% estáticas, o que resulta em um Lighthouse score muito maior.

Comparando com Hugo

Hugo é rápido, mas usar Go templates é doloroso. Astro permite componentes .astro que se parecem com JSX — muito mais familiar para devs JavaScript.

Configurando o Tailwind v4

O Tailwind v4 mudou bastante em relação ao v3. A maior diferença é que não existe mais o tailwind.config.js — tudo é feito via CSS.

@import "tailwindcss";
@plugin "@tailwindcss/typography";

/* Dark mode baseado em classe */
@custom-variant dark (&:where(.dark, .dark *));

Dark Mode com localStorage

Para persistir a preferência do usuário, salvamos no localStorage:

const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

if (stored === 'dark' || (!stored && prefersDark)) {
  document.documentElement.classList.add('dark');
}

Configurando o plugin Typography

O plugin @tailwindcss/typography estiliza automaticamente o conteúdo Markdown usando a classe prose:

<div class="prose dark:prose-invert max-w-none">
  <!-- conteúdo do post aqui -->
</div>

Content Collections no Astro 6

O Astro 6 trouxe uma nova API para Content Collections usando o glob loader:

import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

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

Diferença para o Astro 4/5

Na versão antiga, as entradas tinham slug. No Astro 6+ com glob loader, usamos id em vez de slug.

// Antes (Astro 4)
params: { slug: post.slug }

// Agora (Astro 6)
params: { slug: post.id }

Table of Contents Automático

O render() do Astro retorna o array de headings junto com o Content:

const { Content, headings } = await render(post);
// headings: { depth: 2, slug: 'por-que-astro', text: 'Por que Astro?' }[]

Passamos os headings para o layout e renderizamos uma sidebar sticky com links âncora.

Highlight da Seção Ativa

Usamos IntersectionObserver para detectar qual seção está visível e destacar o link correspondente no TOC:

const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      // remove active de todos, adiciona no atual
    }
  }
}, { rootMargin: '0px 0px -60% 0px' });

Conclusão

Astro 6 + Tailwind v4 é uma combinação excelente para blogs. Performance máxima, DX moderna e zero configuração desnecessária.

Se tiver dúvidas, me manda uma mensagem!