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:

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-Templates
  • static/admin/ – UI und Konfiguration von Decap CMS
  • static/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

  1. In GitHub ein leeres Repository erstellen

    • Oberfläche: GitHub > New repository
    • Beispielname: my-hugo-blog
  2. 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
      
  3. 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 und functions/api/callback.js müssen nicht angepasst werden (sie verwenden url.origin).

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

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

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

  1. Oberfläche: Cloudflare-Dashboard > Pages > Create a project > Connect to Git

  2. GitHub verbinden und Repository my-hugo-blog wählen

  3. 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)
  4. Save and Deploy anklicken und auf das erste Deployment warten

    • Bei Erfolg erhält man eine *.pages.dev-Vorschau-URL

2-4. Eigene Domain www.example.com zuweisen

  1. Menü: Cloudflare > Pages > (Projekt auswählen) > Custom domains > Set up a custom domain

  2. www.example.com eintragen

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

  1. https://www.example.com/admin/ im Browser öffnen
  2. Mit „Login with GitHub“ einloggen und OAuth freigeben
  3. 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

  1. Oberfläche: https://www.example.com/admin/
  2. Linke Navigation: BlogNew blog
  3. Felder: Title, Publish Date, Description, Body
  4. 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

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

  2. Typische Anpassungspunkte

    • layouts/_default/baseof.html<head> sowie Header/Footer
    • layouts/_default/index.html – Startseite mit „Neueste Beiträge“
    • layouts/_default/single.html – Artikelansicht
    • static/css/main.css – Farben, Typografie, Abstände
  3. Ä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 oder master) muss in static/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 über outputs 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)? Sind GITHUB_CLIENT_ID/SECRET in Cloudflare korrekt?
  • Startseite lädt, Artikel-URL liefert 404
    → Ist baseURL in hugo.toml identisch mit der Produktiv-Domain? Prüfe im Cloudflare-Build-Log, ob public/ erzeugt wurde.
  • /admin bleibt leer
    → Wird die Decap CMS-App in static/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:

  1. 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
  2. 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
  3. Bilder in WebP umwandeln

    • resources/_gen/images/ bzw. die Hugo-Pipeline nutzen, um automatisch WebP zu erzeugen
    • Verbessert die Ladezeit
  4. Kategorien- und Tag-Features ausbauen

    • categories und tags im Front Matter pflegen, Tag-Clouds im Template darstellen
  5. 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.