Когда-то этот сайт работал на Google Sites с собственным доменом (ещё во времена Google Apps). Сервис давно перестал быть доступным, и я долго держал https://www.ixam.net без дела. В какой-то момент решил, что пора использовать домен, поэтому устроил полную перезагрузку. Хотелось получить серверлес-структуру, независимую от сервисов, которые могут внезапно закрыться вроде Google Sites, и вот результат эксперимента, который я провёл совместно с Chat GPT.

Честно говоря, думал, что Chat GPT справится «по щелчку», но всё оказалось сложнее. Он то забывал базовые требования, то зацикливался на одном и том же при поиске проблем. В итоге пришлось сверяться с тем, что нашёл сам, например с репозиторием “https://github.com/patrickgrey/website/” на GitHub, и только так нам удалось довести дело до конца.

Ощущения такие, будто у тебя есть подчинённый с высоким уровнем технических навыков, который постоянно неправильно понимает или путает требования, и тебе приходится всё время корректировать курс. В каком-то смысле впечатляет, что ИИ уже дошёл до такого уровня.

Но как только задачи усложняются, он начинает проявлять такую глупость, что просто выводит из себя.

Возможно, в этом тоже есть доля реализма?

Пока что практика показывает: вместо того чтобы вести бесконечный диалог и пытаться заставить ИИ держать в голове весь контекст, лучше, когда разговор становится слишком тяжёлым, человеку самому всё структурировать и открыть новый поток с обновлёнными инструкциями — так получается куда продуктивнее.

Тем не менее, без него я бы не вывез ни по объёму исследований, ни по трудозатратам, ни по терпению. Так что производительность генеративного ИИ действительно впечатляет.


Цели

  • Открывать блог по адресу https://www.example.com/
  • Авторизоваться в DecapCMS через https://www.example.com/admin/ и создавать записи
  • Каждая запись должна коммититься в GitHub и автоматически деплоиться на Cloudflare
  • При этом сейчас дополнительных операционных затрат нет (плату за домен я и так вносил)

0. Предпосылки и структура каталогов

0-1. Предпосылки

  • Предполагается, что домен (example.com) уже зарегистрирован

  • Подойдёт Windows или macOS (примеры команд приведены для обеих систем)

  • Потребуются следующие сервисы (достаточно бесплатных тарифов)

0-2. Пример структуры репозитория

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

Каждая часть отвечает за своё:

  • hugo.toml … общие настройки сайта (baseURL обязательно заменить на боевой URL)
  • functions/api/*.js … Cloudflare Pages Functions (обработчики /api/auth и /api/callback для GitHub OAuth)
  • layouts/_default/*.html … шаблоны Hugo
  • static/admin/ … интерфейс и конфигурация DecapCMS
  • static/css/main.css … оформление (CSS)
  • static/_headers … настройка HTTP-заголовков для Cloudflare Pages (опционально)

※ В инструкции все места, где нужно подставить значения под ваш проект, отмечены как «проверка на замену».


1. Подготовительные шаги

1-1. Создать учётную запись GitHub

  • Зайдите на GitHub в браузере и зарегистрируйтесь

1-2. Создать учётную запись Cloudflare

  • Перейдите на сайт Cloudflare и зарегистрируйтесь

1-3. Установить Git и 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. Основной процесс (до деплоя)

2-1. Подготовка репозитория локально

  1. Создайте пустой репозиторий на GitHub

    • Вкладка: GitHub > New repository
    • Пример имени: my-hugo-blog
  2. Клонируйте репозиторий локально

    • 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. Скопируйте упомянутые выше каталоги и файлы в корень репозитория

Проверка на замену (обязательно выполните правки)

  • hugo.toml

    baseURL = "https://www.example.com"   # ← замените на боевой URL (например, 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   # ← замените на свой репозиторий GitHub
      branch: master                          # ← актуальное имя основной ветки (main или master)
      base_url: https://www.example.com       # ← замените на боевой URL
      auth_endpoint: /api/auth                # ← конечная точка Functions (фиксированная)
    media_folder: static/uploads
    public_folder: /uploads
    

На заметку: functions/api/auth.js и functions/api/callback.js менять не нужно (они используют url.origin, поэтому не требуют жёстко прописанных URL).

  1. Первый коммит и пуш

    • 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. Создать GitHub OAuth App для входа в DecapCMS

  1. Перейдите: GitHub > Settings > Developer settings > OAuth Apps > New OAuth App

  2. Заполните поля:

    • Application name: [DecapCMS](https://decapcms.org/) for my-hugo-blog
    • Homepage URL: https://www.example.com
    • Authorization callback URL: https://www.example.com/api/callbackкритически важно
  3. После создания вы увидите:

    • Client ID
    • Client Secret (сгенерируйте и сохраните в надёжном месте)

Эти значения нужно добавить в переменные окружения Cloudflare Pages.


2-3. Настроить проект Cloudflare Pages

  1. Навигация: Cloudflare Dashboard > Pages > Create a project > Connect to Git

  2. Подключите GitHub и выберите репозиторий my-hugo-blog

  3. Настройте сборку:

    • Framework preset: None (или доверьтесь автоматическому определению Cloudflare)

    • Build command: hugo

    • Build output directory: public

    • Переменные окружения:

      • HUGO_VERSION = 0.128.0 (пример; лучше совпадение с локальной версией Hugo)
      • GITHUB_CLIENT_ID = (значение из предыдущего шага)
      • GITHUB_CLIENT_SECRET = (значение из предыдущего шага)
  4. Нажмите Save and Deploy и дождитесь первого деплоя

    • После успеха появится превью-URL вида *.pages.dev

2-4. Подключить собственный домен www.example.com

  1. Навигация: Cloudflare > Pages > (нужный проект) > Custom domains > Set up a custom domain

  2. Добавьте www.example.com

  3. Если домен не управляется через Cloudflare:

    • Выпишите DNS-записи, которые покажет Cloudflare (например, CNAME www -> <project>.pages.dev)
    • Зайдите в панель регистратора, где обслуживается домен, и создайте записи по этим данным (процедура зависит от конкретного регистратора)
  4. После применения убедитесь, что сайт открывается по https://www.example.com/

Подсказка: если домен уже находится в Cloudflare, нужные DNS-записи создаются автоматически по нажатию кнопки.


2-5. Войти в панель управления DecapCMS

  1. Откройте https://www.example.com/admin/
  2. Нажмите кнопку вроде «Login with GitHub» и разрешите OAuth-авторизацию
  3. При первом входе GitHub спросит, одобряете ли вы приложение — нажмите Authorize

Если вход не удался, проверьте Callback URL OAuth и переменные окружения Cloudflare (GITHUB_CLIENT_ID/SECRET).


3. Эксплуатационные шаги (создание статей, синхронизация, правки шаблонов)

3-1. Создание записи через CMS

  1. Зайдите на https://www.example.com/admin/
  2. В левом меню выберите BlogNew blog
  3. Заполните поля: Title, Publish Date, Description, Body
  4. После нажатия Publish запись коммитится в GitHub → Cloudflare выполняет автодеплой

Сгенерированный Markdown сохраняется в content/blog/.

3-2. Обновить локальный репозиторий после публикации через CMS

  • Windows

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

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

Так вы подтянете свежие файлы и сможете спокойно править шаблоны или CSS.

3-3. Правка дизайна и шаблонов локально

  1. Запустите локальный сервер

    • Windows

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

      mac:~/work/my-hugo-blog dev$ hugo server -D
      
    • Откройте http://localhost:1313/ в браузере

  2. Возможные точки изменений:

    • layouts/_default/baseof.html<head>, шапка и подвал
    • layouts/_default/index.html … список «последних записей» на главной
    • layouts/_default/single.html … шаблон страницы записи
    • static/css/main.css … цвета, шрифты, отступы
  3. Зафиксируйте изменения и отправьте их

    • 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 произведёт новый деплой

3-4. Пара советов по работе с ветками

  • Убедитесь, что имя основной ветки в config.yml DecapCMS и в настройках Cloudflare совпадает (main или master)
  • Если нужен предпросмотр, создайте Pull Request в GitHub — Cloudflare Pages автоматически поднимет preview-окружение

4. Полезные дополнения (по желанию)

4-1. Пример static/_headers (исключить кеширование панели управления)

/admin/*
  Cache-Control: no-store

4-2. robots и sitemap (при необходимости)

  • Подготовьте static/robots.txt, чтобы управлять поведением краулеров
  • Добавьте секцию outputs в hugo.toml, чтобы расширить выдачу RSS и sitemap

5. Частые проблемы и их решения

  • В CMS бесконечная авторизация\ → Проверьте, что Callback URL GitHub OAuth равен https://www.example.com/api/callback, и что переменные GITHUB_CLIENT_ID/SECRET в Cloudflare заданы корректно
  • Главная открывается, а страницы записей дают 404\ → Убедитесь, что baseURL в hugo.toml совпадает с реальным доменом, и посмотрите в логах Cloudflare, что каталог public/ генерируется
  • /admin отображается пустой страницей\ → Проверьте, не блокирует ли браузер загрузку DecapCMS из CDN в static/admin/index.html; отключите расширения и обновите страницу

6. Сводная таблица замен и настроек

Файл/настройка Ключ Что задать (пример)
hugo.toml baseURL https://www.example.com
static/admin/config.yml repo <YOUR_GH_USERNAME>/my-hugo-blog
static/admin/config.yml branch main или master — по фактической основной ветке
static/admin/config.yml base_url https://www.example.com
Cloudflare Pages HUGO_VERSION Например, 0.128.0
Cloudflare Pages GITHUB_CLIENT_ID Client ID GitHub OAuth-приложения
Cloudflare Pages GITHUB_CLIENT_SECRET Client Secret GitHub OAuth-приложения
GitHub OAuth App Callback URL https://www.example.com/api/callback
Cloudflare Pages Custom domain Добавьте www.example.com

7. Приложение: примеры файлов (замените нужные части и используйте)

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 и оптимизации производительности

После запуска блога можно дополнительно улучшить поисковый рейтинг и скорость загрузки следующими шагами.

  1. Настройте sitemap и robots.txt

    • Зарегистрируйте public/sitemap.xml, который Hugo генерирует автоматически, в Google Search Console
    • Добавьте правила в static/robots.txt
  2. Добавьте OGP (Open Graph Protocol) и Twitter Cards

    • Пропишите в layouts/_default/baseof.html теги <meta property="og:title">, <meta property="og:image">
    • Так страницы будут привлекательнее при публикации в соцсетях
  3. Конвертируйте изображения в WebP

    • Используйте resources/_gen/images/ и пайплайн Hugo для автоматического преобразования в WebP
    • Это ускоряет загрузку страниц
  4. Расширьте работу с категориями и тегами

    • Добавьте categories и tags во Front Matter файлов content/blog/, выведите облако тегов в шаблонах
  5. Подключите структурированные данные

    • Передавайте сведения об articles в формате JSON-LD, чтобы повысить шансы на rich results

8. Итоги

  • Создаём репозиторий на GitHub, подключаем его к Cloudflare Pages, настраиваем GitHub OAuth для DecapCMS и привязываем собственный домен — и всё готово
  • Записи создаются через /admin/, коммитятся в GitHub и автоматически деплоятся на Cloudflare
  • Шаблоны и дизайн редактируются локально через hugo server, затем изменения доставляются через Git

Таким образом, вы получаете рабочую серверлес-среду на базе Hugo + GitHub + Decap CMS + Cloudflare.

Пока что получилась лёгкая в поддержке площадка, но поиск по сайту, категории, теги и облако тегов всё ещё уступают полноценным блоговым платформам. Придётся временно смириться и постепенно развивать проект дальше вместе с ChatGPT.