Como criar um Blog Pessoal e Portfólio com Astro 5 + Tailwind CSS v4
Este post mostra como criar um blog pessoal e portfólio exatamente como este — usando Astro 5, Tailwind CSS v4, suporte bilíngue (EN/PT) e o tema dark Tokyo Night.
O código-fonte completo está disponível em github.com/WittmannF/wittmannf.github.io.
O Stack
Antes de começar, um resumo de cada ferramenta que você vai encontrar:
Astro é um framework web — um conjunto de convenções e ferramentas que dá estrutura ao seu projeto para você não ter que construir tudo do zero. O diferencial do Astro: ele gera HTML em tempo de build e envia zero JavaScript para o navegador por padrão, o que torna as páginas muito rápidas. Ele funciona em cima do Vite — uma ferramenta de build moderna que compila, empacota e serve seus arquivos durante o desenvolvimento. Pense no Vite como o motor por baixo do capô.
Tailwind CSS é um framework CSS utility-first (utilitário primeiro). Em vez de escrever nomes de classes customizados em um arquivo de estilo separado (.card { padding: 1.5rem; }), você estiliza os elementos compondo pequenas classes utilitárias diretamente no seu HTML: class="rounded-xl p-6 font-bold". A v4 é uma reescrita completa: ela funciona como um plugin do Vite e não precisa de nenhum arquivo de configuração.
i18n é abreviação de internationalization (internacionalização) — há 18 letras entre o “i” e o “n”, daí o apelido. Significa construir um site que consegue servir conteúdo em vários idiomas. Um termo relacionado: l10n (localization/localização) — o trabalho de tradução em si. Cada variante de idioma é identificada por um locale (um código de língua + região, como en-US ou pt-BR).
Shiki é o syntax highlighter (realçador de sintaxe) que o Astro usa nos blocos de código. Ele colore o código usando os mesmos arquivos de gramática do VS Code, então os trechos de código neste blog parecem exatamente com o seu editor. Você pode trocar o tema — este site usa tokyo-night.
MDX é Markdown estendido com JSX. Permite embutir componentes interativos dentro de um post .md. Aqui usamos Markdown puro, mas o MDX está disponível se precisar.
Pré-requisitos
- Node.js 18+
- Conhecimento básico de HTML, CSS e JavaScript
1. Criando o projeto Astro
npm create astro@latest meu-blog
cd meu-blog
Escolha o template minimal. Quando perguntado sobre TypeScript, selecione strict.
Depois, adicione as integrações necessárias:
npx astro add mdx sitemap
2. Instalando o Tailwind CSS v4
O Tailwind v4 funciona como plugin do Vite — sem tailwind.config.js.
npm install tailwindcss @tailwindcss/vite @tailwindcss/typography
Em astro.config.mjs, registre o plugin:
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
integrations: [mdx(), sitemap()],
vite: {
plugins: [tailwindcss()]
}
});
No arquivo CSS (src/styles/global.css), substitua tudo por:
@import "tailwindcss";
@plugin "@tailwindcss/typography";
Pronto — nenhum arquivo de configuração adicional necessário.
3. Aplicando o tema Tokyo Night
Tokyo Night é um tema dark muito popular entre devs. Defina as cores como variáveis CSS em global.css:
@layer base {
:root {
--tn-bg: #1a1b2e;
--tn-surface: #16213e;
--tn-border: #2a2b4a;
--tn-text: #a9b1d6;
--tn-text-bright: #c0caf5;
--tn-muted: #565f89;
--tn-blue: #7aa2f7;
--tn-cyan: #7dcfff;
}
body {
background-color: var(--tn-bg);
color: var(--tn-text);
@apply antialiased min-h-screen;
}
}
Depois defina classes de componentes reutilizáveis:
@layer components {
.card {
background-color: rgba(22, 33, 62, 0.7);
border: 1px solid var(--tn-border);
@apply rounded-xl p-6 transition-all duration-300;
}
.gradient-text {
background: linear-gradient(135deg, var(--tn-blue), var(--tn-cyan));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
}
4. Configurando Content Collections
Content Collections dão acesso com tipos seguros aos seus arquivos Markdown. Crie src/content/config.ts:
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).optional(),
draft: z.boolean().default(false),
lang: z.enum(['en', 'pt']).default('en'),
}),
});
const projects = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
tech: z.array(z.string()),
github: z.string().optional(),
demo: z.string().optional(),
featured: z.boolean().default(false),
order: z.number().default(999),
}),
});
export const collections = { blog, projects };
Escreva os posts como arquivos .md em src/content/blog/:
---
title: 'Meu primeiro post'
description: 'Uma introdução rápida'
pubDate: 2026-03-06
tags: ['meta']
lang: 'pt'
---
Olá, mundo!
5. Criando os layouts
Crie src/layouts/BaseLayout.astro como a estrutura base de todas as páginas — contém <html>, <head>, Header e Footer.
Aceite uma prop lang para servir páginas em inglês e português pelo mesmo layout:
---
interface Props {
title: string;
lang?: 'en' | 'pt';
alternateUrl?: string;
}
const { title, lang = 'en', alternateUrl } = Astro.props;
---
<html lang={lang === 'en' ? 'en' : 'pt-BR'}>
<head>...</head>
<body>
<Header lang={lang} alternateUrl={alternateUrl} />
<main class="container py-12"><slot /></main>
<Footer lang={lang} />
</body>
</html>
6. Adicionando suporte bilíngue (EN/PT)
A abordagem mais simples: mantenha as páginas em inglês na raiz (/, /blog, /projects) e as páginas em português em /pt/ (/pt/, /pt/blog, /projetos).
Crie src/i18n/ui.ts com as strings de tradução:
export const ui = {
en: { 'nav.home': 'Home', 'nav.blog': 'Blog', ... },
pt: { 'nav.home': 'Início', 'nav.blog': 'Blog', ... },
} as const;
Adicione um botão alternador de idioma no Header. Cada página passa a prop alternateUrl apontando para sua tradução:
<!-- src/pages/index.astro -->
<BaseLayout lang="en" alternateUrl="/pt/">...</BaseLayout>
<!-- src/pages/pt/index.astro -->
<BaseLayout lang="pt" alternateUrl="/">...</BaseLayout>
Para filtrar os posts por idioma nas listagens:
// Blog EN: mostra só posts com lang: 'en'
const posts = (await getCollection('blog'))
.filter(p => !p.data.draft && p.data.lang === 'en');
// Blog PT: mostra só posts com lang: 'pt'
const posts = (await getCollection('blog'))
.filter(p => !p.data.draft && p.data.lang === 'pt');
7. Configurando syntax highlighting
O Astro usa Shiki para blocos de código. Em astro.config.mjs, defina o tema para combinar com o design:
markdown: {
shikiConfig: {
theme: 'tokyo-night',
wrap: true
}
}
8. Deploy
Antes de qualquer coisa, gere o build de produção:
npm run build
O output vai para dist/. A partir daqui, escolha uma das três opções abaixo.
Opção A — Cloudflare Pages (recomendado)
Via Dashboard
- Acesse
dash.cloudflare.com→ Workers & Pages → Create → aba Pages - Conecte seu repositório GitHub
- Em Build settings, selecione o preset Astro — ele preenche automaticamente:
- Build command:
npm run build - Build output directory:
dist - Deploy command: (deixe vazio)
- Build command:
- Clique em Save and Deploy — a cada push em
mainum novo deploy é disparado automaticamente
Via GitHub Actions
Se preferir mais controle no CI, configure o workflow de deploy automático:
- No dashboard Cloudflare, copie seu Account ID (Dashboard > Pages > Account ID)
- Crie um API Token em
dash.cloudflare.com/profile/api-tokenscom permissão Cloudflare Pages - Edit - Adicione as duas secrets no GitHub: Settings > Secrets and variables > Actions
CLOUDFLARE_ACCOUNT_IDCLOUDFLARE_API_TOKEN
- No arquivo
.github/workflows/deploy-cloudflare.yml, atualize o nome do projeto:command: pages deploy dist --project-name=seu-projeto-nome - Push para
main— o deploy acontece automaticamente via GitHub Actions
Via CLI (Wrangler)
Para deploys manuais sem GitHub Actions:
# Instale o Wrangler globalmente
npm install -g wrangler
# Autentique com sua conta Cloudflare
wrangler login
# Build e deploy
npm run build
wrangler pages deploy dist --project-name=seu-projeto
Domínio customizado (Cloudflare Pages)
- No seu projeto Pages → Custom domains → Set up a custom domain
- Digite seu domínio (ex:
seudominio.com) → Continue - O Cloudflare pedirá para transferir o gerenciamento de DNS — clique em Begin DNS transfer
- O Cloudflare escaneia seus registros existentes e fornece dois nameservers (ex:
cruz.ns.cloudflare.com) - Vá ao seu registrador → Nameservers → substitua pelos dois nameservers do Cloudflare → Salve
- Aguarde 15–60 minutos para propagação — o Cloudflare envia um e-mail quando estiver ativo
- De volta em Pages → Custom domains → Check DNS records — o Cloudflare adiciona o CNAME automaticamente
O domínio customizado, a transferência de DNS e o certificado SSL são todos gratuitos.
Opção B — Vercel
Via Dashboard
- Acesse vercel.com e clique em Add New Project
- Importe seu repositório GitHub — o Vercel detecta Astro automaticamente
- Clique em Deploy
Via CLI
# Instale o Vercel CLI
npm i -g vercel
# Deploy de preview
vercel
# Deploy para produção
vercel --prod
Opção C — GitHub Pages
- No repositório, vá em Settings > Pages
- Em Source, selecione GitHub Actions
- Push para
main— o workflowdeploy-github-pages.ymlfará o deploy automaticamente
Atenção: se o repositório não for
usuario.github.io, adicione a opçãobaseemastro.config.mjs:export default defineConfig({ site: 'https://seu-usuario.github.io', base: '/nome-do-repositorio', // ... });
Domínio customizado
Após o deploy, você pode conectar seu próprio domínio:
| Plataforma | Caminho |
|---|---|
| Cloudflare Pages | Dashboard > Pages > seu projeto > Custom domains |
| Vercel | Dashboard > seu projeto > Settings > Domains |
| GitHub Pages | Settings > Pages > Custom domain + arquivo public/CNAME com seu domínio |
Estrutura final do projeto
src/
content/
blog/ ← seus posts .md
projects/ ← seus projetos .md
i18n/
ui.ts ← strings de tradução EN/PT
layouts/
BaseLayout.astro
BlogPost.astro
components/
Header.astro
Footer.astro
pages/
index.astro ← home EN
blog/index.astro ← blog EN
projects.astro ← projetos EN
pt/
index.astro ← home PT
blog/index.astro
blog/[...slug].astro
projects.astro ← projetos PT
styles/
global.css
O código-fonte completo deste site está em github.com/WittmannF/wittmannf.github.io. Fique à vontade para fazer um fork e adaptar para o seu portfólio.