Publicar um portfólio parece simples até a tarefa envolver tudo ao mesmo tempo: código local, repositório remoto, build reproduzível, credenciais, domínio próprio, DNS, HTTPS e dois idiomas. O risco não estava em executar um comando de deploy. Estava em criar um sistema no qual a próxima publicação fosse previsível e não exigisse refazer decisões manualmente.
Este artigo registra a arquitetura que usei no meu próprio portfólio. O objetivo foi chegar a uma regra simples:
toda alteração aprovada entra na
main; a automação valida o projeto e publica uma única versão; os domínios e idiomas são formas diferentes de chegar a essa mesma publicação.
Não é uma receita universal. É um case de implementação com limites explícitos, incluindo o que ainda permanece como próximo passo.
O desenho final
push na main
→ GitHub Actions
→ npm ci
→ build + validação de release
→ Wrangler envia dist/ para Cloudflare Pages
→ Cloudflare atende o domínio próprio
O projeto continua sendo um site Astro estático. Não foi necessário transformar toda a aplicação
em SSR para obter deploy contínuo ou executar uma pequena decisão no edge. O build gera dist/,
e o Wrangler publica esse diretório no projeto existente da Cloudflare Pages. Esse caminho também
é documentado nos guias oficiais de deploy do Astro na Cloudflare
e de Direct Upload com Wrangler.
Uma única fonte de produção
O primeiro princípio foi evitar dois processos concorrentes. O GitHub não hospeda uma cópia e a Cloudflare outra. O GitHub mantém o código e executa a automação; a Cloudflare Pages recebe o artefato de produção.
O workflow é disparado somente por push na main ou manualmente por workflow_dispatch:
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: cloudflare-pages-production
cancel-in-progress: true
O filtro de branch torna a main o contrato de publicação. A configuração de concorrência impede
que dois deploys de produção disputem o mesmo destino. O GitHub documenta tanto os
filtros de branch para eventos de push
quanto o uso de environments e concurrency em deploys.
Validar antes de enviar
A automação não começa pelo upload. Ela reproduz a instalação e valida o que será publicado:
- name: Install dependencies
run: npm ci
- name: Build and validate release
run: npm run release:check
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=portfolio
No projeto, release:check combina o build do Astro com uma validação do resultado. Essa segunda
etapa verifica inventário bilíngue, metadados, links internos, assets e ausência das rotas
editoriais no artefato público. O deploy só começa se o comando terminar com sucesso.
O token e o identificador da conta não ficam no YAML. Eles são lidos de GitHub Actions Secrets. Além de evitar credenciais versionadas, essa separação permite limitar a permissão do token ao que o deploy realmente precisa. A documentação do GitHub cobre a criação e o uso de secrets em workflows, e suas recomendações de segurança reforçam o princípio de menor privilégio.
Domínio próprio é uma mudança de DNS, não de build
Depois que a URL pages.dev estava respondendo, o domínio principal foi conectado ao mesmo
projeto Pages. Essa ordem foi útil porque separou dois diagnósticos:
- o artefato está correto e acessível na Cloudflare?
- o domínio está delegado e associado corretamente?
Para um domínio apex, como example.com, a Cloudflare exige que a zona esteja na mesma conta do
projeto Pages e que os nameservers apontem para a Cloudflare. O fluxo correto também inclui
adicionar o domínio em Custom domains; criar apenas um CNAME manual não substitui essa
associação. A sequência está descrita na documentação de
custom domains da Cloudflare Pages.
Havia ainda um cuidado operacional: migrar o site sem apagar os registros que atendem outros serviços. Antes de trocar a autoridade DNS, revisei e preservei os registros de e-mail e de verificação existentes. Um site funcionar enquanto o e-mail deixa de funcionar não é um deploy bem-sucedido.
Uma publicação, vários domínios
Domínios adicionais não precisam criar novos builds. A arquitetura pretendida é:
- um domínio canônico para indexação e compartilhamento;
- domínios secundários como portas de entrada;
- redirecionamento permanente dos secundários para o canônico;
- uma única origem, um único certificado operacional e um único pipeline de conteúdo.
No momento deste registro, o domínio principal já está ativo. O domínio .com.br ainda é um
próximo passo porque continua sob outra autoridade DNS. Ele não é apresentado aqui como concluído.
Publicá-lo exige adicionar a zona, preservar seus registros, trocar os nameservers no registrador
e somente então ativar o redirecionamento para o domínio canônico.
O loading escondido no primeiro acesso
O problema mais visível apareceu depois do domínio entrar no ar. A rota / era um redirect
estático do Astro para /en-us/. Em conexões rápidas, isso poderia parecer aceitável. Em uma
verificação real no Safari, porém, o navegador mostrou a página intermediária “Redirecting from
/ to /en-us/” antes de carregar o site.
O requisito mudou: detectar a preferência de idioma sem trocar a URL inicial e sem obrigar o navegador a fazer uma segunda navegação.
A solução foi manter as páginas localizadas já geradas e adicionar somente uma Pages Function na raiz:
GET /
→ ler Accept-Language
→ pt: servir internamente /pt-br/artigos/
→ en ou fallback: servir internamente /en-us/insights/
→ responder 200 mantendo a URL /
Na Cloudflare Pages, functions/index.js corresponde à rota raiz. A função pode pedir um asset
estático do próprio projeto por meio de env.ASSETS.fetch(). Assim, não há Location nem uma nova
navegação no browser. A resposta também informa Content-Language e Vary: Accept-Language.
Essa abordagem usa capacidades descritas diretamente na documentação de
roteamento de Pages Functions,
na referência de env.ASSETS.fetch()
e no guia de localização por Accept-Language.
As rotas explícitas /pt-br/... e /en-us/... continuam existindo. Elas permanecem úteis para
links, compartilhamento, alternates e seleção manual. O que desaparece é o salto obrigatório no
primeiro acesso ao domínio.
Custo e escala
Para este tipo de site, requisições puramente estáticas na Pages não consomem cota de Functions.
A rota /, por usar uma Function, conta na cota do Workers. A documentação atual informa que o
plano gratuito compartilha uma franquia diária entre Workers e Pages Functions; por isso, o custo
deve ser reavaliado se o tráfego crescer ou se mais rotas dinâmicas forem adicionadas. Os detalhes
vigentes estão na página de pricing de Pages Functions.
No GitHub, o custo depende da visibilidade do repositório, do runner e do plano. Não tratei “CI gratuito” como garantia permanente; a referência correta é a página atual de billing do GitHub Actions.
Checklist que encerra o deploy
O trabalho só pode ser considerado concluído quando cada camada responde:
npm cireproduz a instalação;- o build usa a versão de Node declarada pelo projeto;
release:checktermina sem erro;- o workflow da
mainconclui com sucesso; - a URL
pages.devresponde; - o domínio canônico responde com HTTPS;
- os registros de e-mail e verificação continuam presentes;
/responde200sem cabeçalhoLocation;Accept-Language: pt-BRentrega português;Accept-Language: en-USentrega inglês;- nenhuma credencial aparece no repositório ou nos logs.
O aprendizado principal
Automação de deploy não é apenas “publicar ao dar merge”. É tornar explícitos os contratos entre código, build, credencial, infraestrutura e experiência de entrada.
O desenho ficou mais simples quando cada sistema recebeu uma responsabilidade:
- Git guarda o histórico;
maindefine o que pode ir para produção;- GitHub Actions reproduz e valida;
- Wrangler transporta o artefato;
- Cloudflare Pages serve o site;
- DNS conecta nomes sem duplicar aplicações;
- a Function da raiz resolve apenas a escolha inicial de idioma.
Essa separação evita que cada novo domínio, idioma ou publicação vire um projeto paralelo. O resultado desejado continua sendo um só: alterar, validar e publicar com evidência.














