Timp de ani buni am operat acest site pe un domeniu propriu legat de vechiul Google Sites inclus în Google Apps. Când acel serviciu a fost retras, am lăsat https://www.ixam.net nefolosit. În cele din urmă am decis că este momentul să îi dau din nou viață. Ținta a fost o arhitectură serverless care să nu depindă de o platformă ce poate dispărea peste noapte, iar reconstrucția am realizat-o împreună cu ChatGPT.

Presupuneam că ChatGPT va transforma proiectul într-o plimbare ușoară, însă realitatea a fost diferită. Am tot uitat cerințe, depanarea o lua mereu de la capăt, iar la final am ajuns să răscolesc depozite precum https://github.com/patrickgrey/website/ pentru a debloca situația. A fost multă muncă, dar am dus totul la capăt prin colaborare.

Experiența a semănat cu ghidarea unui coleg foarte capabil, dar care scapă frecvent din vedere detaliile: îl readuci pe traseu, ceea ce dovedește că IA poate fi într-adevăr utilă. În același timp, cu cât lucrurile deveneau mai complexe, cu atât era mai ușor să mă frustrez din cauza greșelilor.

Concluzia mea: mai degrabă decât să obligi IA să țină minte un istoric uriaș într-o singură conversație, e mai eficient să faci o pauză, să rezumi tu situația și apoi să deschizi un fir nou cu instrucțiuni proaspete.

Chiar și așa, nu aș fi putut atinge singur volumul necesar de documentare, încercări și învățare. IA generativă este un accelerator de productivitate incredibil.


Obiectiv

  • Să public blogul la https://www.example.com/
  • Să mă pot autentifica în Decap CMS la https://www.example.com/admin/ și să creez articole
  • Articolele noi să fie comise în GitHub și distribuite automat pe Cloudflare
  • Costuri operaționale suplimentare zero (taxa de domeniu este în afara discuției)

0. Condiții prealabile și structură de directoare

0-1. Condiții prealabile

  • Domeniul (de exemplu, example.com) este deja înregistrat

  • Poți lucra pe Windows sau macOS (exemple de comenzi pentru ambele)

  • Servicii utilizate (nivelul gratuit este suficient)

0-2. Structura exemplu de depozit

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

Această structură are următoarele roluri:

  • hugo.toml — configurarea întregului site (înlocuiește baseURL cu domeniul de producție)
  • functions/api/*.js — Cloudflare Pages Functions (/api/auth și /api/callback pentru OAuth GitHub)
  • layouts/_default/*.html — șabloanele Hugo
  • static/admin/ — interfața și configurația Decap CMS
  • static/css/main.css — stilurile vizuale
  • static/_headers — antete HTTP opționale pentru Cloudflare Pages

De-a lungul ghidului marchez zonele ce necesită personalizare ca „pune aici valoarea ta”.


1. Pregătiri

1-1. Creează un cont GitHub

  • Deschide GitHub în browser și înscrie-te

1-2. Creează un cont Cloudflare

  • Deschide Cloudflare în browser și înscrie-te

1-3. Instalează Git și 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. Procedura principală (până la primul deployment)

2-1. Configurează depozitul local

  1. Creează un depozit gol pe GitHub

    • Navigare: GitHub > New repository
    • Exemplu de nume: my-hugo-blog
  2. Clonează-l 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. Plasează structura de directoare și fișiere prezentată mai sus direct în depozit

Puncte obligatorii de înlocuit

  • hugo.toml

    baseURL = "https://www.example.com"   # ← înlocuiește cu URL-ul de producție (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   # ← înlocuiește cu depozitul tău GitHub
      branch: master                          # ← numele ramurii implicite (main sau master)
      base_url: https://www.example.com       # ← înlocuiește cu URL-ul de producție
      auth_endpoint: /api/auth                # ← endpointul funcției (fix)
    media_folder: static/uploads
    public_folder: /uploads
    

Referință: functions/api/auth.js și functions/api/callback.js folosesc url.origin, așa că nu necesită editări specifice mediului.

  1. Fă primul commit și push

    • Windows

      PS C:\work\my-hugo-blog> git add -A
      PS C:\work\my-hugo-blog> git commit -m "Initial commit: Hugo + DecapCMS + 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 + CF Pages"
      mac:~/work/my-hugo-blog dev$ git push -u origin master
      

Valorile din pasul următor trebuie configurate ca variabile de mediu în Cloudflare Pages.


2-2. Creează o aplicație GitHub OAuth (pentru autentificarea în Decap CMS)

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

  2. Completează:

    • Application name: DecapCMS for my-hugo-blog
    • Homepage URL: https://www.example.com
    • Authorization callback URL: https://www.example.com/api/callbackesențial
  3. După creare notează:

    • Client ID
    • Client Secret (generează un secret nou și salvează-l)

Aceste valori vor fi folosite ca variabile de mediu în Cloudflare Pages.


2-3. Creează un proiect Cloudflare Pages

  1. Navigare: Cloudflare dashboard > Pages > Create a project > Connect to Git

  2. Conectează GitHub și alege depozitul my-hugo-blog

  3. Setări de build:

    • Framework preset: None (sau lasă Cloudflare să detecteze automat)

    • Build command: hugo

    • Build output directory: public

    • Variabile de mediu:

      • HUGO_VERSION = 0.128.0 (exemplu — aliniază cu versiunea locală pentru siguranță)
      • GITHUB_CLIENT_ID = valoarea din secțiunea anterioară
      • GITHUB_CLIENT_SECRET = valoarea din secțiunea anterioară
  4. Apasă Save and Deploy și așteaptă primul deployment

    • La succes primești un URL de previzualizare *.pages.dev

2-4. Atașează domeniul personalizat www.example.com

  1. Navigare: Cloudflare > Pages > (proiectul) > Custom domains > Set up a custom domain

  2. Introdu www.example.com

  3. Dacă domeniul nu este administrat integral în Cloudflare:

    • Copiază detaliile de DNS afișate în Cloudflare (ex.: CNAME www -> <project>.pages.dev)
    • Configurează acele înregistrări la registrarul tău, conform documentației lor
  4. După propagarea DNS, verifică dacă https://www.example.com/ servește site-ul

Sfat: dacă domeniul este deja pe Cloudflare, înregistrările DNS necesare se creează cu un singur clic.


2-5. Autentifică-te în panoul Decap CMS

  1. Accesează https://www.example.com/admin/
  2. Apasă Login with GitHub și aprobă accesul OAuth
  3. La prima rulare GitHub cere Authorize — permite accesul

Dacă autentificarea eșuează, verifică din nou OAuth callback URL și variabilele de mediu GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET din Cloudflare.


3. Operare (creare conținut, sincronizare locală, modificări de șabloane)

3-1. Creează articole din CMS

  1. Mergi la https://www.example.com/admin/
  2. În navigația din stânga selectează BlogNew blog
  3. Completează Title, Publish Date, Description și Body
  4. Apasă Publish pentru a face commit în GitHub → Cloudflare face deploy automat

Fișierele Markdown generate apar în content/blog/.

3-2. Actualizează local după ce publici din CMS

  • Windows

    PS C:\work\my-hugo-blog> git pull origin master
    
  • macOS

    mac:~/work/my-hugo-blog dev$ git pull origin master
    

Astfel menții depozitul local sincronizat și poți modifica în siguranță șabloanele sau CSS-ul.

3-3. Ajustează designul și șabloanele local

  1. Pornește serverul local de dezvoltare

    • Windows

      PS C:\work\my-hugo-blog> hugo server -D
      
    • macOS

      mac:~/work/my-hugo-blog dev$ hugo server -D
      
    • Deschide http://localhost:1313/ în browser pentru previzualizare

  2. Puncte obișnuite de personalizare

    • layouts/_default/baseof.html — conținutul <head>, antet, subsol
    • layouts/_default/index.html — lista articolelor recente de pe prima pagină
    • layouts/_default/single.html — șablonul pentru corpul articolelor
    • static/css/main.css — culori, fonturi, spațiere
  3. Fă commit și push modificărilor

    • 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
      
    • Cloudflare redeployează automat în câteva secunde sau minute

3-4. Recomandări pentru managementul ramurilor

  • Asigură-te că ramura implicită (main sau master) este aceeași în static/admin/config.yml și în setările proiectului Cloudflare
  • Când deschizi un pull request pe GitHub, Cloudflare Pages creează automat o previzualizare

4. Îmbunătățiri opționale

4-1. Exemplu static/_headers (dezactivează cache-ul pentru panoul de administrare)

/admin/*
  Cache-Control: no-store

4-2. Robots și sitemap

  • Folosește static/robots.txt pentru a controla roboții
  • Adaugă outputs în hugo.toml dacă vrei formate suplimentare precum RSS sau sitemap

5. Capcane frecvente și soluții

  • Panoul CMS se blochează într-o buclă de login
    → Verifică dacă callback URL-ul OAuth este exact https://www.example.com/api/callback și dacă variabilele GITHUB_CLIENT_ID și GITHUB_CLIENT_SECRET din Cloudflare sunt corecte
  • Pagina principală funcționează, dar articolele afișează 404
    → Confirmă că baseURL din hugo.toml corespunde domeniului real. Consultă logurile de build Cloudflare pentru a te asigura că public/ a fost generat
  • /admin afișează o pagină goală
    → Asigură-te că scripturile Decap CMS din static/admin/index.html nu sunt blocate de extensii sau politici CSP; dezactivează extensiile și reîncarcă

6. Listă de verificare „înlocuiește aici”

Fișier / setare Cheie Exemplu de valoare de configurat
hugo.toml baseURL https://www.example.com
static/admin/config.yml repo <YOUR_GH_USERNAME>/my-hugo-blog
static/admin/config.yml branch Potrivește cu ramura implicită (main/master)
static/admin/config.yml base_url https://www.example.com
Cloudflare Pages HUGO_VERSION de ex. 0.128.0
Cloudflare Pages GITHUB_CLIENT_ID Client ID-ul aplicației OAuth GitHub
Cloudflare Pages GITHUB_CLIENT_SECRET Client secret-ul aplicației OAuth GitHub
GitHub OAuth App Callback URL https://www.example.com/api/callback
Cloudflare Pages Domeniu personalizat Adaugă www.example.com

7. Anexă: fișiere exemplu (înlocuiește secțiunile marcate)

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. Recomandări pentru SEO și performanță

După ce blogul este online, pașii de mai jos pot îmbunătăți vizibilitatea și viteza:

  1. Sitemaps și robots.txt

    • Trimite public/sitemap.xml (generat de Hugo) în Google Search Console
    • Ajustează comportamentul roboților cu static/robots.txt
  2. OGP (Open Graph Protocol) și carduri Twitter

    • Adaugă etichete precum <meta property="og:title"> și <meta property="og:image"> în layouts/_default/baseof.html
    • Îmbunătățește aspectul postărilor partajate pe rețelele sociale
  3. Conversie imagini în WebP

    • Folosește resources/_gen/images/ și Hugo Pipes pentru conversie automată în WebP
    • Accelerează încărcarea paginilor
  4. Adaugă categorii și etichete

    • Include categories și tags în front matter-ul fișierelor din content/blog/ și afișează-le în șabloane (de exemplu ca tag cloud)
  5. Folosește date structurate

    • Oferă metadate în format JSON-LD pentru a crește șansele de rich results

8. Concluzii

  • Creezi un depozit GitHub, îl conectezi la Cloudflare Pages, configurezi OAuth GitHub pentru Decap CMS și mapezi domeniul personalizat — astfel ajungi la rezultat
  • Autorii publică din /admin/ → GitHub face commit → Cloudflare distribuie automat
  • Ajustezi șabloanele și designul local cu hugo server, apoi publici prin Git

Atât îți trebuie pentru a opera un blog serverless bazat pe Hugo, GitHub, Decap CMS și Cloudflare.

Acum am fundamentul ultraușor pe care mi-l doream, deși funcții precum căutarea internă, categoriile și norii de etichete rămân în urma marilor platforme de blogging. Planul este să extind treptat infrastructura împreună cu ChatGPT.