Tento web jsem kdysi provozoval na vlastní doméně přes původní Google Sites z éry Google Apps. Když Google starou službu celosvětově ukončil, nechal jsem https://www.ixam.net dlouho ležet ladem. Rozhodl jsem se, že doménu konečně znovu využiji – ideálně v serverless architektuře, která nebude záviset na službě, jež může kdykoli bez varování skončit. A tak jsem se do obnovy pustil společně s ChatGPT.

Myslel jsem si, že s ChatGPT to půjde snadno, ale realita byla jiná. Pořád jsem zapomínal na základní požadavky, při troubleshootingu jsme se točili v kruhu a nakonec jsem musel pročítat repozitáře jako https://github.com/patrickgrey/website/, abychom se posunuli dál. Výsledek je přesto společným dílem člověka a AI.

Byl to pocit, jako když vedete velmi schopného, ale často zbloudilého kolegu zpět na trať – což jen ukazuje, jak daleko už se AI dostala. Jakmile se však téma komplikovalo, frustrace z drobných chyb rostla.

Poučení? Místo nekonečného vlákna, ve kterém má AI držet všechny předpoklady v hlavě, je lepší se zastavit, sám si věci uspořádat a začít nové čisté vlákno s čerstvými instrukcemi.

Přesto bych bez generativní AI nezvládl tolik rešerší, pokusů a učení – její přínos pro produktivitu je obrovský.


Cíl

  • Blog musí být dostupný na https://www.example.com/
  • Do Decap CMS se bude možné přihlásit na https://www.example.com/admin/ a psát články
  • Nové příspěvky se commitují do GitHubu a automaticky nasazují na Cloudflare
  • Dodatečné provozní náklady zůstávají nulové (poplatek za registraci domény je mimo záběr tohoto návodu)

0. Předpoklady a struktura složek

0-1. Předpoklady

  • Doména (např. example.com) je už zaregistrovaná

  • Můžete pracovat na Windows i macOS (ukázky příkazů pro obě platformy)

  • Budeme používat tyto služby (stačí bezplatné tarify)

0-2. Ukázková struktura repozitáře

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

Tato struktura plní následující role:

  • hugo.toml — Globální konfigurace webu (baseURL nahraďte produkční adresou)
  • functions/api/*.js — Cloudflare Pages Functions (/api/auth a /api/callback pro GitHub OAuth)
  • layouts/_default/*.html — Šablony pro Hugo
  • static/admin/ — UI a konfigurace Decap CMS
  • static/css/main.css — Vzhled (CSS)
  • static/_headers — Volitelné HTTP hlavičky pro Cloudflare Pages

V návodu zvýrazňuji místa, která je nutné upravit podle vlastního prostředí, jako „kontrolní body k nahrazení“.


1. Příprava

1-1. Založte si GitHub účet

  • V prohlížeči přejděte na GitHub a dokončete registraci

1-2. Založte si Cloudflare účet

  • V prohlížeči přejděte na Cloudflare a dokončete registraci

1-3. Nainstalujte Git a 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. Hlavní postup (až po první nasazení)

2-1. Připravte si repozitář lokálně

  1. Na GitHubu založte prázdný repozitář

    • Cesta v UI: GitHub > New repository
    • Například: my-hugo-blog
  2. Naklonujte repozitář do počítače

    • 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. Do kořene repozitáře zkopírujte složky a soubory z výše uvedené struktury

Kontrolní body k nahrazení (nezapomeňte upravit)

  • hugo.toml

    baseURL = "https://www.example.com"   # ← nahraďte produkční URL (např. 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   # ← nahraďte vlastním repozitářem na GitHubu
      branch: master                          # ← jméno výchozí větve (main nebo master)
      base_url: https://www.example.com       # ← nahraďte produkční URL
      auth_endpoint: /api/auth                # ← endpoint pro Functions (neměňte)
    media_folder: static/uploads
    public_folder: /uploads
    

Poznámka: functions/api/auth.js a functions/api/callback.js stačí ponechat, jak jsou. Díky url.origin neobsahují žádné hodnoty závislé na prostředí.

  1. První commit a 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. Vytvořte GitHub OAuth App (pro přihlášení do Decap CMS)

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

  2. Vyplňte:

    • Application name: [DecapCMS](https://decapcms.org/) for my-hugo-blog
    • Homepage URL: https://www.example.com
    • Authorization callback URL: https://www.example.com/api/callbackzásadní údaj
  3. Po vytvoření si poznamenejte:

    • Client ID
    • Client Secret (vygenerujte a bezpečně uložte)

Tyto hodnoty uložíte jako proměnné prostředí v Cloudflare Pages.


2-3. Založte projekt v Cloudflare Pages

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

  2. Propojte GitHub a vyberte repozitář my-hugo-blog

  3. Nastavte build:

    • Framework preset: None (nebo nechte Cloudflare autodetekci)

    • Build command: hugo

    • Build output directory: public

    • Proměnné prostředí:

      • HUGO_VERSION = 0.128.0 (příklad – doporučuji sladit s lokální verzí)
      • GITHUB_CLIENT_ID = (z předchozího kroku)
      • GITHUB_CLIENT_SECRET = (z předchozího kroku)
  4. Klikněte na Save and Deploy a počkejte na první nasazení

    • Po úspěchu získáte náhledovou adresu *.pages.dev

2-4. Přiřaďte vlastní doménu www.example.com

  1. V UI: Cloudflare > Pages > (daný projekt) > Custom domains > Set up a custom domain

  2. Zadejte www.example.com a přidejte ji

  3. Pokud nemáte doménu převedenou pod Cloudflare:

    • Zapište si DNS záznamy, které Cloudflare zobrazí (např. CNAME www -> <project>.pages.dev)
    • V rozhraní svého registrátora nastavte odpovídající záznamy přesně podle pokynů (postup závisí na registrátorovi)
  4. Po propagaci ověřte, že https://www.example.com/ načte váš web

Tip: Pokud doménu spravujete přímo v Cloudflare, nastaví se potřebné DNS záznamy jedním klikem.


2-5. Přihlaste se do administrace Decap CMS

  1. Otevřete v prohlížeči https://www.example.com/admin/
  2. Klikněte na tlačítko typu „Login with GitHub“ a povolte GitHub OAuth
  3. Při prvním přístupu GitHub zobrazí „Authorize this app?“ – potvrďte kliknutím na Authorize

Pokud přihlášení selže, znovu zkontrolujte Callback URL v OAuth aplikaci a proměnné prostředí GITHUB_CLIENT_ID/SECRET v Cloudflare.


3. Provoz (tvorba článků, synchronizace, úprava šablon)

3-1. Vytváření článků přes CMS

  1. Rozhraní: https://www.example.com/admin/
  2. V levém menu vyberte BlogNew blog
  3. Vyplňte Title, Publish Date, Description, Body
  4. Stiskněte Publish – Decap CMS provede commit do GitHubu a Cloudflare automaticky nasadí změny

Vzniklé Markdown soubory najdete v content/blog/.

3-2. Aktualizujte lokální repozitář (po publikaci přes CMS)

  • Windows

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

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

Takto si stáhnete nejnovější články a můžete bezpečně upravovat šablony či CSS.

3-3. Lokální úpravy designu a šablon

  1. Spusťte lokální vývojový server

    • Windows

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

      mac:~/work/my-hugo-blog dev$ hugo server -D
      
    • Náhled zobrazíte na http://localhost:1313/

  2. Co můžete měnit (příklady)

    • layouts/_default/baseof.html<head>, hlavička, patička
    • layouts/_default/index.html — výpis „Nejnovější články“ na úvodní stránce
    • layouts/_default/single.html — tělo článku
    • static/css/main.css — barvy, písmo, odsazení
  3. Commitujte a pushujte změny

    • 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
      
    • Po pár desítkách sekund až minut Cloudflare změny automaticky nasadí

3-4. Tip k práci s branchí

  • Dbejte na to, aby výchozí větev (main nebo master) byla shodně nastavená v config.yml pro Decap CMS i v nastavení Cloudflare
  • Pokud chcete náhled před nasazením, vytvořte Pull Request na GitHubu – Cloudflare Pages automaticky připraví Preview prostředí

4. Užitečné doplňky (volitelné)

4-1. Příklad static/_headers (zakáže cache pro administraci)

/admin/*
  Cache-Control: no-store

4-2. robots a sitemap (podle potřeby)

  • Připravte static/robots.txt pro řízení indexace
  • V hugo.toml můžete v sekci outputs zapnout generování RSS a sitemap

5. Časté problémy a řešení

  • CMS se při loginu jen točí
    → Zkontrolujte, zda má GitHub OAuth správné Callback URL https://www.example.com/api/callback a jestli proměnné GITHUB_CLIENT_ID/SECRET v Cloudflare obsahují správné hodnoty
  • Úvodní stránka funguje, ale články vracejí 404
    → Ověřte, že baseURL v hugo.toml odpovídá skutečné doméně. V build logu Cloudflare sledujte, zda vzniká složka public/
  • /admin je prázdné
    → Prověřte, jestli načítání Decap CMS ve static/admin/index.html neblokuje prohlížeč nebo rozšíření; zkuste je vypnout a načíst stránku znovu

6. Přehled kontrolních míst (rekapitulace)

Soubor / nastavení Klíč Co nastavit (příklad)
hugo.toml baseURL https://www.example.com
static/admin/config.yml repo <YOUR_GH_USERNAME>/my-hugo-blog
static/admin/config.yml branch Nastavte na main nebo master podle reality
static/admin/config.yml base_url https://www.example.com
Cloudflare Pages HUGO_VERSION Např. 0.128.0
Cloudflare Pages GITHUB_CLIENT_ID Client ID z GitHub OAuth
Cloudflare Pages GITHUB_CLIENT_SECRET Client Secret z GitHub OAuth
GitHub OAuth App Callback URL https://www.example.com/api/callback
Cloudflare Pages Custom domain Přidejte www.example.com

7. Přílohy: ukázkové soubory (nahraďte vlastními údaji)

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 a výkon: co doladit po zprovoznění

Po spuštění blogu můžete vylepšit pozice ve vyhledávání i rychlost načítání těmito kroky:

  1. Sitemap a robots.txt

    • Zaregistrujte public/sitemap.xml, který Hugo generuje, do Google Search Console
    • Do static/robots.txt doplňte pravidla pro indexaci
  2. OGP a Twitter Cards

    • Do layouts/_default/baseof.html přidejte <meta property="og:title">, <meta property="og:image"> apod.
    • Sdílené odkazy na sociálních sítích pak vypadají lépe
  3. Konverze obrázků do WebP

    • Využijte resources/_gen/images/ a pipeline v Hugu pro automatický převod do WebP
    • Výrazně tím zrychlíte načítání
  4. Kategorie a tagy

    • Do Front Matter souborů v content/blog/ přidejte categories a tags, v šablonách můžete zobrazit tag cloud
  5. Strukturovaná data

    • Pomocí JSON-LD popište články pro vyhledávače a získejte šanci na rich results

8. Shrnutí

  • Založte repozitář na GitHubu, napojte ho na Cloudflare Pages, nastavte GitHub OAuth pro Decap CMS a přiřaďte vlastní doménu – tím je základ hotový
  • Články vytvářejte na /admin/ → commitují se do GitHubu → Cloudflare je nasadí
  • Šablony a design upravujte lokálně s hugo server, změny nasazujte přes Git

Takto získáte kompletní serverless prostředí pro blog na stacku Hugo + GitHub + Decap CMS + Cloudflare.

Základní kostru s minimálními provozními náklady máme. Vyhledávání, kategorie, tagy a tag cloud zatím zaostávají za běžnými blogy, ale to je momentálně daň za rychlost. Postupně je chci rozšiřovat společně s ChatGPT.