Einen serverlosen Blog mit eigener Domain und CMS bauen – Schritt für Schritt mit Hugo, GitHub, Decap CMS und Cloudflare Pages
Diese Seite lief früher über eine eigene Domain auf dem klassischen Google Sites (aus den Google-Apps-Zeiten). Als Google diesen Dienst einstellte, lag https://www.ixam.net lange brach. Schließlich wollte ich die Domain wieder nutzen – mit einer serverlosen Architektur, die nicht von einem plötzlich eingestellten Dienst abhängt. Also habe ich mich gemeinsam mit ChatGPT an den Neuaufbau gemacht.
Ich hatte gehofft, dass ChatGPT das fast von allein erledigt, aber weit gefehlt. Immer wieder fehlten grundlegende Anforderungen, beim Troubleshooting drehten wir uns im Kreis, und ich musste nebenbei Repos wie https://github.com/patrickgrey/website/ durchforsten. Am Ende war es doch echte Teamarbeit.
Es fühlte sich an wie ein sehr fähiges, aber oft falsch liegendes Teammitglied, das man behutsam auf Kurs bringt – beeindruckend, wie weit KI inzwischen ist. Je komplexer das Thema wurde, desto schneller stieg allerdings der Frust über schlichte Patzer.
Mein Fazit: Statt eine endlose Konversation laufen zu lassen und der KI alle Prämissen aufzubürden, lieber selbst ordnen, kurz stoppen und mit einem sauberen Thread neu ansetzen.
Allein hätte ich den Recherche-, Arbeits- und Lernaufwand kaum geschafft – die Produktivitätsspritze durch generative KI ist gewaltig.
Ziel
https://www.example.com/
soll den Blog ausspielen.https://www.example.com/admin/
bietet Login in Decap CMS zur Beitragserstellung.- Beiträge werden in GitHub eingecheckt und automatisch nach Cloudflare deployt.
- Zusätzliche Betriebskosten: aktuell 0 (die Domain war ohnehin registriert).
0. Voraussetzungen und Verzeichnisstruktur
0-1. Voraussetzungen
-
Die Domain (
example.com
) ist bereits registriert. -
Windows oder macOS sind gleichermaßen nutzbar (Befehle für beide Systeme angegeben).
-
Wir verwenden ausschließlich kostenlose Pläne:
- GitHub-Account
- Cloudflare-Account (für Pages)
- Git lokal installiert
- Hugo (für die lokale Vorschau und den Cloudflare-Build)
0-2. Beispielhafte Repository-Struktur
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
Die Struktur erfüllt folgende Aufgaben:
hugo.toml
– globale Site-Konfiguration (baseURL durch die Produktiv-URL ersetzen)functions/api/*.js
– Cloudflare Pages Functions (/api/auth
und/api/callback
für GitHub OAuth)layouts/_default/*.html
– Hugo-Templatesstatic/admin/
– UI und Konfiguration von Decap CMSstatic/css/main.css
– Erscheinungsbild (CSS)static/_headers
– optionale HTTP-Header für Cloudflare Pages
Differenz-Check: Stellen, die unbedingt projektspezifisch ersetzt werden müssen, markiere ich mit „Anpassung notwendig“.
1. Vorbereitung
1-1. GitHub-Account anlegen
- Im Browser GitHub öffnen und registrieren.
1-2. Cloudflare-Account anlegen
- Cloudflare im Browser öffnen und registrieren.
1-3. Git und Hugo installieren
-
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. Hauptteil – bis zum ersten Deployment
2-1. Repository lokal aufsetzen
-
In GitHub ein leeres Repository erstellen
- Oberfläche: GitHub > New repository
- Beispielname:
my-hugo-blog
-
Lokal klonen
-
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
-
-
Die oben gezeigte Ordnerstruktur unterhalb des Repos platzieren
Anpassung notwendig (unbedingt ersetzen)
-
hugo.toml
baseURL = "https://www.example.com" # ← durch die produktive Domain ersetzen (z. B. 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 # ← eigenes GitHub-Repo eintragen branch: master # ← tatsächlichen Standardbranch (main/master) setzen base_url: https://www.example.com # ← produktive Domain eintragen auth_endpoint: /api/auth # ← Functions-Endpunkt (fix) media_folder: static/uploads public_folder: /uploads
Hinweis:
functions/api/auth.js
undfunctions/api/callback.js
müssen nicht angepasst werden (sie verwendenurl.origin
).
-
Erster Commit und Push
-
Windows
PS C:\work\my-hugo-blog> git add -A PS C:\work\my-hugo-blog> git commit -m "Initial commit: Hugo + Decap CMS + 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 + Decap CMS + CF Pages" mac:~/work/my-hugo-blog dev$ git push -u origin master
-
2-2. GitHub OAuth App für Decap CMS anlegen
-
Menü: GitHub > Settings > Developer settings > OAuth Apps > New OAuth App
-
Eingaben:
- Application name:
[Decap CMS](https://decapcms.org/) for my-hugo-blog
- Homepage URL:
https://www.example.com
- Authorization callback URL:
https://www.example.com/api/callback
← wichtig
- Application name:
-
Nach dem Anlegen angezeigt:
- Client ID
- Client Secret (neu generieren und sicher notieren)
Diese Werte werden später als Cloudflare-Pages-Umgebungsvariablen gesetzt.
2-3. Cloudflare-Pages-Projekt erstellen
-
Oberfläche: Cloudflare-Dashboard > Pages > Create a project > Connect to Git
-
GitHub verbinden und Repository
my-hugo-blog
wählen -
Build-Einstellungen:
-
Framework preset:
None
(oder Automatik) -
Build command:
hugo
-
Build output directory:
public
-
Environment variables:
HUGO_VERSION
=0.128.0
(Beispiel, mit der lokalen Version abgleichen)GITHUB_CLIENT_ID
= (Wert aus Schritt 2-2)GITHUB_CLIENT_SECRET
= (Wert aus Schritt 2-2)
-
-
Save and Deploy anklicken und auf das erste Deployment warten
- Bei Erfolg erhält man eine
*.pages.dev
-Vorschau-URL
- Bei Erfolg erhält man eine
2-4. Eigene Domain www.example.com
zuweisen
-
Menü: Cloudflare > Pages > (Projekt auswählen) > Custom domains > Set up a custom domain
-
www.example.com
eintragen -
Wenn die Domain nicht bei Cloudflare verwaltet wird:
- DNS-Daten (z. B.
CNAME www -> <project>.pages.dev
) aus dem Cloudflare-Dialog notieren - Beim Domain-Registrar gemäß Anleitung die Einträge setzen
- DNS-Daten (z. B.
-
Nach der DNS-Aktualisierung prüfen, ob
https://www.example.com/
die Site ausliefert
Tipp: Liegt die Domain bereits bei Cloudflare, lassen sich die DNS-Records per Klick setzen.
2-5. Decap CMS bedienen
https://www.example.com/admin/
im Browser öffnen- Mit „Login with GitHub“ einloggen und OAuth freigeben
- Beim ersten Mal GitHub-Zugriff mit Authorize bestätigen
Login schlägt fehl? Callback-URL sowie
GITHUB_CLIENT_ID
/SECRET
in Cloudflare prüfen.
3. Betrieb – Beiträge, Synchronisierung, Templates
3-1. Beiträge im CMS schreiben
- Oberfläche:
https://www.example.com/admin/
- Linke Navigation: Blog → New blog
- Felder:
Title
,Publish Date
,Description
,Body
- Publish löst GitHub-Commit und automatisches Cloudflare-Deployment aus
Das erzeugte Markdown landet unter
content/blog/
.
3-2. Lokales Repo nach CMS-Posts aktualisieren
-
Windows
PS C:\work\my-hugo-blog> git pull origin master
-
macOS
mac:~/work/my-hugo-blog dev$ git pull origin master
So holt man neue Inhalte lokal ab und bearbeitet Templates/CSS risikofrei.
3-3. Design und Templates lokal anpassen
-
Lokalen Entwicklungsserver starten
-
Windows
PS C:\work\my-hugo-blog> hugo server -D
-
macOS
mac:~/work/my-hugo-blog dev$ hugo server -D
-
Vorschau unter
http://localhost:1313/
-
-
Typische Anpassungspunkte
layouts/_default/baseof.html
–<head>
sowie Header/Footerlayouts/_default/index.html
– Startseite mit „Neueste Beiträge“layouts/_default/single.html
– Artikelansichtstatic/css/main.css
– Farben, Typografie, Abstände
-
Änderungen committen und pushen
-
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
-
Nach Sekunden/Minuten deployt Cloudflare automatisch
-
3-4. Hinweise zur Branch-Strategie
- Standardbranch (
main
odermaster
) muss instatic/admin/config.yml
und in Cloudflare konsistent sein. - Für Vorschauen bietet ein GitHub-Pull-Request automatisch eine Cloudflare-Pages-Preview.
4. Nützliche Ergänzungen (optional)
4-1. Beispiel für static/_headers
(CMS nicht cachen)
/admin/*
Cache-Control: no-store
4-2. robots & Sitemap (bei Bedarf)
static/robots.txt
für Crawler-Regeln anlegen- In
hugo.toml
überoutputs
zusätzliche Ausgaben (RSS, Sitemap) definieren
5. Häufige Stolperfallen & Lösungen
- CMS-Login dreht sich im Kreis
→ Stimmt die OAuth-Callback-URL (https://www.example.com/api/callback
)? SindGITHUB_CLIENT_ID/SECRET
in Cloudflare korrekt? - Startseite lädt, Artikel-URL liefert 404
→ IstbaseURL
inhugo.toml
identisch mit der Produktiv-Domain? Prüfe im Cloudflare-Build-Log, obpublic/
erzeugt wurde. - /admin bleibt leer
→ Wird die Decap CMS-App instatic/admin/index.html
geblockt (z. B. durch Browser-Extensions)? Deaktiviere Add-ons und lade neu.
6. Übersicht: Stellen für individuelle Anpassungen (Wiederholung)
Datei/Setting | Schlüssel | Beispielwert |
---|---|---|
hugo.toml |
baseURL |
https://www.example.com |
static/admin/config.yml |
repo |
<YOUR_GH_USERNAME>/my-hugo-blog |
static/admin/config.yml |
branch |
main oder master |
static/admin/config.yml |
base_url |
https://www.example.com |
Cloudflare Pages | HUGO_VERSION |
z. B. 0.128.0 |
Cloudflare Pages | GITHUB_CLIENT_ID |
Client ID aus der GitHub OAuth App |
Cloudflare Pages | GITHUB_CLIENT_SECRET |
Client Secret aus der GitHub OAuth App |
GitHub OAuth App | Callback URL | https://www.example.com/api/callback |
Cloudflare Pages | Custom domain | www.example.com hinzufügen |
7. Anhang: Beispielfiles (kopieren und anpassen)
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. SEO- und Performance-Optimierung
Nach dem Grundaufbau lassen sich Ranking und Ladezeit mit folgenden Maßnahmen verbessern:
-
Sitemap und robots.txt pflegen
- Die automatisch erstellte
public/sitemap.xml
in der Google Search Console registrieren - In
static/robots.txt
Crawler-Anweisungen ergänzen
- Die automatisch erstellte
-
OGP- und Twitter-Card-Metadaten
- In
layouts/_default/baseof.html
Tags wie<meta property="og:title">
und<meta property="og:image">
hinterlegen - So wirken geteilte Links in sozialen Netzwerken attraktiver
- In
-
Bilder in WebP umwandeln
resources/_gen/images/
bzw. die Hugo-Pipeline nutzen, um automatisch WebP zu erzeugen- Verbessert die Ladezeit
-
Kategorien- und Tag-Features ausbauen
categories
undtags
im Front Matter pflegen, Tag-Clouds im Template darstellen
-
Strukturierte Daten hinterlegen
- Artikelinformationen als JSON-LD ausgeben, um Rich Results in Suchmaschinen zu ermöglichen
8. Fazit
- GitHub-Repository anlegen, mit Cloudflare Pages verbinden, GitHub OAuth für Decap CMS konfigurieren und die Domain verknüpfen – fertig.
- Beiträge entstehen im
/admin/
, landen als Commit in GitHub und werden automatisch deployt. - Templates und Design lokal per
hugo server
bearbeiten und via Git übernehmen.
Damit steht eine schlanke, serverlose Blog-Umgebung auf Basis von Hugo + GitHub + Decap CMS + Cloudflare.
Die Basis funktioniert, aber Features wie Suche, Kategorien, Tags oder Tag-Clouds fehlen noch. Im Vergleich zu etablierten Blog-Plattformen wirkt das mager – doch der Ausbau läuft gemeinsam mit ChatGPT Schritt für Schritt.