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)

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 Hugo
  • static/admin/ … interface e configuração do DecapCMS
  • static/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

  1. Crie um repositório vazio no GitHub

    • Tela: GitHub > New repository
    • Exemplo de nome: my-hugo-blog
  2. 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
      
  3. 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 e functions/api/callback.js podem ficar como estão (eles usam url.origin, dispensando valores específicos por ambiente).

  1. 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

  1. Caminho: GitHub > Settings > Developer settings > OAuth Apps > New OAuth App

  2. 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/callbackcrucial
  3. 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

  1. Caminho: Cloudflare Dashboard > Pages > Create a project > Connect to Git

  2. Conecte-se ao GitHub e selecione o repositório my-hugo-blog

  3. 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)
  4. Clique em Save and Deploy e aguarde o primeiro deploy

    • Ao concluir, será gerada uma URL de preview terminada em *.pages.dev

2-4. Associar o domínio próprio www.example.com

  1. Caminho: Cloudflare > Pages > (projeto correspondente) > Custom domains > Set up a custom domain

  2. Informe www.example.com e adicione

  3. 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)
  4. 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

  1. Abra https://www.example.com/admin/
  2. Clique em um botão como “Login with GitHub” e autorize o OAuth
  3. 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

  1. Tela: https://www.example.com/admin/
  2. Menu esquerdo: clique em BlogNew blog
  3. Preencha: Title, Publish Date, Description, Body
  4. 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

  1. 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

  2. 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 inicial
    • layouts/_default/single.html … corpo das páginas de post
    • static/css/main.css … cores, fontes, espaçamentos
  3. 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 ou master)
  • 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 em hugo.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 se GITHUB_CLIENT_ID/SECRET estão corretos no Cloudflare
  • A home abre, mas as páginas dos posts dão 404\ → Garanta que baseURL em hugo.toml corresponde ao domínio real e verifique nos logs do Cloudflare se a pasta public/ 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.

  1. Configure sitemap e robots.txt

    • Cadastre no Google Search Console o public/sitemap.xml gerado automaticamente pelo Hugo
    • Adicione regras em static/robots.txt
  2. Defina OGP (Open Graph Protocol) e Twitter Cards

    • Inclua tags como <meta property="og:title"> e <meta property="og:image"> em layouts/_default/baseof.html
    • Isso deixa as páginas mais atraentes quando compartilhadas nas redes sociais
  3. 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
  4. Adicione categorias e tags mais ricas

    • Preencha categories e tags no Front Matter dos arquivos em content/blog/ e apresente uma nuvem de tags no template
  5. 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.