Guia passo a passo para criar um blog serverless com domínio próprio usando Hugo + GitHub + Decap CMS + Cloudflare Pages
Este site antes rodava no Google Sites (da era Google Apps) com domínio próprio, mas o serviço deixou de funcionar há bastante tempo e eu acabei abandonando https://www.ixam.net. Resolvi que estava na hora de dar algum uso ao domínio e parti para uma reformulação completa. Queria um ambiente serverless e livre de serviços que possam sumir de repente como o Google Sites, então aqui está o resultado da experiência que toquei junto com o Chat GPT.
Para ser sincero, imaginei que o Chat GPT resolveria tudo com facilidade, mas não foi o que aconteceu. Ele ignorava requisitos básicos, ficava girando em círculos durante a depuração e, no fim, precisei recorrer a repositórios que encontrei por conta própria — como “https://github.com/patrickgrey/website/” no GitHub — para conseguirmos concluir o trabalho em conjunto.
A sensação é parecida com ter um subordinado muito habilidoso, mas que entende tudo errado ou confunde os requisitos, e você precisa trazê-lo para os trilhos a cada passo. Em certo sentido, é impressionante que a IA já tenha chegado a esse nível.
Só que, quando as coisas ficam complexas, ela começa a exibir um grau de burrice capaz de irritar qualquer um.
Talvez isso também seja realista de alguma forma?
Pelo que percebi, em vez de manter uma conversa infinita e tentar fazer a IA lembrar de todos os pressupostos, é melhor, quando o diálogo ficar enrolado demais, o humano reorganizar tudo e abrir um novo tópico com instruções atualizadas. Assim rende bem mais.
Ainda assim, sem a ajuda dela eu dificilmente daria conta do volume de pesquisa, do trabalho e da paciência envolvidos. A produtividade da IA generativa é realmente assustadora.
Objetivos
- Exibir o blog em
https://www.example.com/
- Acessar o DecapCMS por
https://www.example.com/admin/
e criar posts - Fazer com que cada post seja commitado no GitHub e implantado automaticamente no Cloudflare
- No momento não há custos extras de operação (a taxa de registro de domínio já existia)
0. Pré-requisitos e estrutura de pastas
0-1. Pré-requisitos
-
Pressupõe-se que o domínio (
example.com
) já esteja registrado -
Windows ou macOS funcionam (os exemplos de comandos cobrem ambos)
-
Serviços necessários (os planos gratuitos dão conta)
- Conta no GitHub
- Conta no Cloudflare (iremos usar o Pages)
- Git (instalado localmente)
- Hugo (para pré-visualização local e para a build no Cloudflare)
0-2. Estrutura principal do repositório (exemplo)
hugo.toml
content/
blog/
posts/
data/
functions/
api/
auth.js
callback.js
layouts/
_default/
baseof.html
index.html
list.html
single.html
static/
_headers
admin/
config.yml
index.html
css/
main.css
Cada parte cumpre um papel:
hugo.toml
… configurações globais do site (lembre-se de trocar o baseURL pelo endereço de produção)functions/api/*.js
… Cloudflare Pages Functions (usadas pelo GitHub OAuth nos endpoints/api/auth
e/api/callback
)layouts/_default/*.html
… templates do Hugostatic/admin/
… interface e configuração do DecapCMSstatic/css/main.css
… estilo (CSS)static/_headers
… configuração de cabeçalhos HTTP do Cloudflare Pages (opcional)
※ Sempre que for preciso substituir algum valor de acordo com o ambiente, esta documentação marca o trecho como “checagem de substituição”.
1. Preparação inicial
1-1. Criar uma conta no GitHub
- Abra o GitHub no navegador e faça o cadastro
1-2. Criar uma conta no Cloudflare
- Abra o Cloudflare no navegador e faça o cadastro
1-3. Instalar Git e Hugo
-
Windows (PowerShell)
PS C:\Users\alice> winget install Git.Git PS C:\Users\alice> winget install Hugo.Hugo.Extended
-
macOS (Terminal)
mac:~ dev$ brew install git mac:~ dev$ brew install hugo
2. Processo principal (até o deploy)
2-1. Preparar o repositório localmente
-
Crie um repositório vazio no GitHub
- Tela: GitHub > New repository
- Exemplo de nome:
my-hugo-blog
-
Clone o repositório para a máquina local
-
Windows
PS C:\work> git clone https://github.com/<YOUR_GH_USERNAME>/my-hugo-blog.git PS C:\work> cd .\my-hugo-blog
-
macOS
mac:~/work dev$ git clone https://github.com/<YOUR_GH_USERNAME>/my-hugo-blog.git mac:~/work dev$ cd my-hugo-blog
-
-
Copie todas as pastas e arquivos listados acima para a raiz do repositório
Checagem de substituição (obrigatória)
-
hugo.toml
baseURL = "https://www.example.com" # ← substitua pelo endereço de produção (ex.: https://www.example.com) languageCode = "ja-jp" title = "Example Blog" publishDir = "public" [permalinks] blog = "/blog/:year/:month/:slug/"
-
static/admin/config.yml
backend: name: github repo: <YOUR_GH_USERNAME>/my-hugo-blog # ← troque pelo seu repositório no GitHub branch: master # ← nome do branch padrão (main ou master) base_url: https://www.example.com # ← substitua pelo endereço de produção auth_endpoint: /api/auth # ← endpoint das Functions (fixo) media_folder: static/uploads public_folder: /uploads
Observação:
functions/api/auth.js
efunctions/api/callback.js
podem ficar como estão (eles usamurl.origin
, dispensando valores específicos por ambiente).
-
Primeiro commit e push
-
Windows
PS C:\work\my-hugo-blog> git add -A PS C:\work\my-hugo-blog> git commit -m "Initial commit: Hugo + [DecapCMS](https://decapcms.org/) + CF Pages" PS C:\work\my-hugo-blog> git push -u origin master
-
macOS
mac:~/work/my-hugo-blog dev$ git add -A mac:~/work/my-hugo-blog dev$ git commit -m "Initial commit: Hugo + [DecapCMS](https://decapcms.org/) + CF Pages" mac:~/work/my-hugo-blog dev$ git push -u origin master
-
2-2. Criar o GitHub OAuth App para login no DecapCMS
-
Caminho: GitHub > Settings > Developer settings > OAuth Apps > New OAuth App
-
Preencha os campos:
- Application name:
[DecapCMS](https://decapcms.org/) for my-hugo-blog
- Homepage URL:
https://www.example.com
- Authorization callback URL:
https://www.example.com/api/callback
← crucial
- Application name:
-
Após criar, serão exibidos:
- Client ID
- Client Secret (gere um novo valor e guarde em local seguro)
Esses valores devem ser configurados nas variáveis de ambiente do Cloudflare Pages.
2-3. Configurar o projeto no Cloudflare Pages
-
Caminho: Cloudflare Dashboard > Pages > Create a project > Connect to Git
-
Conecte-se ao GitHub e selecione o repositório
my-hugo-blog
-
Defina as configurações de build:
-
Framework preset:
None
(ou deixe o Cloudflare detectar automaticamente) -
Build command:
hugo
-
Build output directory:
public
-
Variáveis de ambiente:
HUGO_VERSION
=0.128.0
(exemplo; alinhar com a versão usada localmente traz mais segurança)GITHUB_CLIENT_ID
= (valor obtido na etapa anterior)GITHUB_CLIENT_SECRET
= (valor obtido na etapa anterior)
-
-
Clique em Save and Deploy e aguarde o primeiro deploy
- Ao concluir, será gerada uma URL de preview terminada em
*.pages.dev
- Ao concluir, será gerada uma URL de preview terminada em
2-4. Associar o domínio próprio www.example.com
-
Caminho: Cloudflare > Pages > (projeto correspondente) > Custom domains > Set up a custom domain
-
Informe
www.example.com
e adicione -
Caso o domínio não esteja sob o gerenciamento do Cloudflare:
- Anote os registros DNS exibidos pelo Cloudflare (ex.:
CNAME www -> <project>.pages.dev
) - No painel do registrador onde o domínio está hospedado, configure os registros seguindo essas instruções (cada registrador tem seu fluxo)
- Anote os registros DNS exibidos pelo Cloudflare (ex.:
-
Depois da propagação, verifique se
https://www.example.com/
abre corretamente
Dica: se o domínio já estiver no Cloudflare, basta um clique para criar os registros DNS necessários automaticamente.
2-5. Acessar o painel do DecapCMS
- Abra
https://www.example.com/admin/
- Clique em um botão como “Login with GitHub” e autorize o OAuth
- Na primeira vez, o GitHub perguntará se você aprova o aplicativo — confirme em Authorize
Se o login falhar, confira novamente o Callback URL do OAuth e as variáveis
GITHUB_CLIENT_ID/SECRET
no Cloudflare.
3. Operação contínua (criação de posts, sync local, ajustes de template)
3-1. Criar posts pelo CMS
- Tela:
https://www.example.com/admin/
- Menu esquerdo: clique em Blog → New blog
- Preencha:
Title
,Publish Date
,Description
,Body
- Ao clicar em Publish, é feito um commit no GitHub e o Cloudflare dispara o deploy automaticamente
O Markdown gerado fica em
content/blog/
.
3-2. Trazer o repositório local para a versão mais recente após postar via CMS
-
Windows
PS C:\work\my-hugo-blog> git pull origin master
-
macOS
mac:~/work/my-hugo-blog dev$ git pull origin master
Assim você garante que as alterações feitas pelo CMS também estão na sua máquina antes de editar templates ou CSS.
3-3. Ajustar design e templates localmente
-
Inicie o servidor de desenvolvimento local
-
Windows
PS C:\work\my-hugo-blog> hugo server -D
-
macOS
mac:~/work/my-hugo-blog dev$ hugo server -D
-
Acesse
http://localhost:1313/
no navegador para conferir
-
-
Pontos típicos de alteração (exemplos)
layouts/_default/baseof.html
…<head>
, cabeçalho e rodapélayouts/_default/index.html
… lista de “posts recentes” na página iniciallayouts/_default/single.html
… corpo das páginas de poststatic/css/main.css
… cores, fontes, espaçamentos
-
Faça commit e push das mudanças
-
Windows
PS C:\work\my-hugo-blog> git add -A PS C:\work\my-hugo-blog> git commit -m "Update theme/layouts" PS C:\work\my-hugo-blog> git push
-
macOS
mac:~/work/my-hugo-blog dev$ git add -A mac:~/work/my-hugo-blog dev$ git commit -m "Update theme/layouts" mac:~/work/my-hugo-blog dev$ git push
-
Depois de alguns segundos ou minutos o Cloudflare fará um novo deploy
-
3-4. Dicas rápidas sobre branches
- Certifique-se de que o branch padrão configurado em
config.yml
do DecapCMS e nas configurações do Cloudflare seja o mesmo (main
oumaster
) - Para visualizar previews, abra um Pull Request no GitHub e o Cloudflare Pages criará um ambiente de preview automaticamente
4. Complementos úteis (opcional)
4-1. Exemplo de static/_headers
(evitar cache no painel administrativo)
/admin/*
Cache-Control: no-store
4-2. robots e sitemap (se necessário)
- Forneça um
static/robots.txt
para controlar crawlers - Acrescente
outputs
emhugo.toml
caso queira expandir a geração de RSS e sitemap
5. Problemas comuns e soluções
- O login do CMS fica girando sem parar\
→ Confira se o Callback URL do GitHub OAuth está em
https://www.example.com/api/callback
e seGITHUB_CLIENT_ID/SECRET
estão corretos no Cloudflare - A home abre, mas as páginas dos posts dão 404\
→ Garanta que
baseURL
emhugo.toml
corresponde ao domínio real e verifique nos logs do Cloudflare se a pastapublic/
foi gerada - /admin aparece completamente em branco\
→ Verifique se o carregamento do DecapCMS em
static/admin/index.html
não está sendo bloqueado (experimente desativar extensões do navegador e recarregar)
6. Checklist de substituições e configurações
Arquivo/configuração | Chave | Valor de exemplo |
---|---|---|
hugo.toml |
baseURL |
https://www.example.com |
static/admin/config.yml |
repo |
<YOUR_GH_USERNAME>/my-hugo-blog |
static/admin/config.yml |
branch |
Ajuste para main ou master , conforme o repositório |
static/admin/config.yml |
base_url |
https://www.example.com |
Cloudflare Pages | HUGO_VERSION |
Ex.: 0.128.0 |
Cloudflare Pages | GITHUB_CLIENT_ID |
Client ID do aplicativo GitHub OAuth |
Cloudflare Pages | GITHUB_CLIENT_SECRET |
Client Secret do aplicativo GitHub OAuth |
GitHub OAuth App | Callback URL | https://www.example.com/api/callback |
Cloudflare Pages | Custom domain | Adicione www.example.com |
7. Apêndice: exemplos de arquivos (substitua o necessário e reutilize)
7-1. functions/api/auth.js
// Cloudflare Pages Functions (/api/auth)
export async function onRequest(context) {
const { request, env } = context;
const client_id = env.GITHUB_CLIENT_ID;
try {
const url = new URL(request.url);
const redirectUrl = new URL('https://github.com/login/oauth/authorize');
redirectUrl.searchParams.set('client_id', client_id);
redirectUrl.searchParams.set('redirect_uri', `${url.origin}/api/callback`);
redirectUrl.searchParams.set('scope', 'repo user');
redirectUrl.searchParams.set('state', crypto.getRandomValues(new Uint8Array(12)).join(''));
return Response.redirect(redirectUrl.href, 302);
} catch (err) {
return new Response(String(err?.message || err), { status: 500 });
}
}
7-2. functions/api/callback.js
function renderBody(status, content) {
const html = `
<script>
const receiveMessage = (message) => {
window.opener.postMessage(
'authorization:github:${status}:${JSON.stringify(content)}',
message.origin
);
window.removeEventListener("message", receiveMessage, false);
}
window.addEventListener("message", receiveMessage, false);
window.opener.postMessage("authorizing:github", "*");
</script>`;
return new Blob([html]);
}
export async function onRequest(context) {
const { request, env } = context;
const client_id = env.GITHUB_CLIENT_ID;
const client_secret = env.GITHUB_CLIENT_SECRET;
try {
const url = new URL(request.url);
const code = url.searchParams.get('code');
const response = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'content-type': 'application/json', 'user-agent': 'cf-pages-oauth', 'accept': 'application/json' },
body: JSON.stringify({ client_id, client_secret, code }),
});
const result = await response.json();
if (result.error) {
return new Response(renderBody('error', result), { headers: { 'content-type': 'text/html;charset=UTF-8' }, status: 401 });
}
const token = result.access_token;
const provider = 'github';
return new Response(renderBody('success', { token, provider }), { headers: { 'content-type': 'text/html;charset=UTF-8' }, status: 200 });
} catch (error) {
return new Response(error.message, { headers: { 'content-type': 'text/html;charset=UTF-8' }, status: 500 });
}
}
7-3. static/admin/config.yml
backend:
name: github
repo: <YOUR_GH_USERNAME>/my-hugo-blog
branch: main
base_url: https://www.example.com
auth_endpoint: /api/auth
media_folder: static/uploads
public_folder: /uploads
collections:
- name: 'blog'
label: 'Blog'
folder: 'content/blog'
create: true
slug: '{{slug}}'
editor:
preview: false
fields:
- { label: 'Title', name: 'title', widget: 'string' }
- { label: 'Publish Date', name: 'date', widget: 'datetime' }
- { label: 'Description', name: 'description', widget: 'string' }
- { label: 'Body', name: 'body', widget: 'markdown' }
9. Pontos de SEO e otimização de performance
Depois de montar o blog, siga estes passos para melhorar ainda mais o ranqueamento e a velocidade de carregamento.
-
Configure sitemap e robots.txt
- Cadastre no Google Search Console o
public/sitemap.xml
gerado automaticamente pelo Hugo - Adicione regras em
static/robots.txt
- Cadastre no Google Search Console o
-
Defina OGP (Open Graph Protocol) e Twitter Cards
- Inclua tags como
<meta property="og:title">
e<meta property="og:image">
emlayouts/_default/baseof.html
- Isso deixa as páginas mais atraentes quando compartilhadas nas redes sociais
- Inclua tags como
-
Converta imagens para WebP
- Use
resources/_gen/images/
e o pipeline do Hugo para gerar WebP automaticamente - Ajuda a reduzir o tempo de carregamento das páginas
- Use
-
Adicione categorias e tags mais ricas
- Preencha
categories
etags
no Front Matter dos arquivos emcontent/blog/
e apresente uma nuvem de tags no template
- Preencha
-
Implemente dados estruturados
- Forneça as informações dos artigos em JSON-LD para aumentar as chances de rich results
8. Conclusão
- Crie o repositório no GitHub, conecte-o ao Cloudflare Pages, configure o GitHub OAuth para o DecapCMS e associe o domínio próprio para finalizar o setup
- Os posts são criados via
/admin/
, comitam no GitHub e são implantados automaticamente pelo Cloudflare - Ajuste templates e design localmente com
hugo server
e envie as mudanças via Git
Assim você mantém um ambiente serverless baseado em Hugo + GitHub + Decap CMS + Cloudflare funcionando sem dor de cabeça.
Por enquanto o site de baixo custo está de pé, mas busca interna, categorias, tags e nuvem de tags ainda ficam atrás de blogs já estabelecidos. Vou operá-lo desse jeito e, aos poucos, ampliar tudo com a ajuda do ChatGPT.