Cum să creezi un blog serverless cu domeniu personalizat și CMS folosind stiva Hugo + GitHub + Decap CMS + Cloudflare Pages
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)
- Cont GitHub
- Cont Cloudflare (folosim Pages)
- Git instalat local
- Hugo (pentru previzualizare locală și build-uri în Cloudflare)
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ștebaseURL
cu domeniul de producție)functions/api/*.js
— Cloudflare Pages Functions (/api/auth
și/api/callback
pentru OAuth GitHub)layouts/_default/*.html
— șabloanele Hugostatic/admin/
— interfața și configurația Decap CMSstatic/css/main.css
— stilurile vizualestatic/_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
-
Creează un depozit gol pe GitHub
- Navigare: GitHub > New repository
- Exemplu de nume:
my-hugo-blog
-
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
-
-
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
șifunctions/api/callback.js
folosescurl.origin
, așa că nu necesită editări specifice mediului.
-
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)
-
Navigare: GitHub > Settings > Developer settings > OAuth Apps > New OAuth App
-
Completează:
- Application name:
DecapCMS for my-hugo-blog
- Homepage URL:
https://www.example.com
- Authorization callback URL:
https://www.example.com/api/callback
← esențial
- Application name:
-
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
-
Navigare: Cloudflare dashboard > Pages > Create a project > Connect to Git
-
Conectează GitHub și alege depozitul
my-hugo-blog
-
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ă
-
-
Apasă Save and Deploy și așteaptă primul deployment
- La succes primești un URL de previzualizare
*.pages.dev
- La succes primești un URL de previzualizare
2-4. Atașează domeniul personalizat www.example.com
-
Navigare: Cloudflare > Pages > (proiectul) > Custom domains > Set up a custom domain
-
Introdu
www.example.com
-
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
- Copiază detaliile de DNS afișate în Cloudflare (ex.:
-
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
- Accesează
https://www.example.com/admin/
- Apasă Login with GitHub și aprobă accesul OAuth
- 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
- Mergi la
https://www.example.com/admin/
- În navigația din stânga selectează Blog → New blog
- Completează
Title
,Publish Date
,Description
șiBody
- 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
-
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
-
-
Puncte obișnuite de personalizare
layouts/_default/baseof.html
— conținutul<head>
, antet, subsollayouts/_default/index.html
— lista articolelor recente de pe prima paginălayouts/_default/single.html
— șablonul pentru corpul articolelorstatic/css/main.css
— culori, fonturi, spațiere
-
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
saumaster
) este aceeași înstatic/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
înhugo.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 exacthttps://www.example.com/api/callback
și dacă variabileleGITHUB_CLIENT_ID
șiGITHUB_CLIENT_SECRET
din Cloudflare sunt corecte - Pagina principală funcționează, dar articolele afișează 404
→ Confirmă căbaseURL
dinhugo.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 dinstatic/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:
-
Sitemaps și robots.txt
- Trimite
public/sitemap.xml
(generat de Hugo) în Google Search Console - Ajustează comportamentul roboților cu
static/robots.txt
- Trimite
-
OGP (Open Graph Protocol) și carduri Twitter
- Adaugă etichete precum
<meta property="og:title">
și<meta property="og:image">
înlayouts/_default/baseof.html
- Îmbunătățește aspectul postărilor partajate pe rețelele sociale
- Adaugă etichete precum
-
Conversie imagini în WebP
- Folosește
resources/_gen/images/
și Hugo Pipes pentru conversie automată în WebP - Accelerează încărcarea paginilor
- Folosește
-
Adaugă categorii și etichete
- Include
categories
șitags
în front matter-ul fișierelor dincontent/blog/
și afișează-le în șabloane (de exemplu ca tag cloud)
- Include
-
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.