Пошаговое руководство по созданию серверлес-блога с кастомным доменом на стеке Hugo + GitHub + Decap CMS + Cloudflare Pages
Когда-то этот сайт работал на 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 (примеры команд приведены для обеих систем)
-
Потребуются следующие сервисы (достаточно бесплатных тарифов)
- Учётная запись GitHub
- Учётная запись Cloudflare (будем использовать Pages)
- Git (на локальной машине)
- Hugo (для локального превью и для сборки на Cloudflare)
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
… шаблоны Hugostatic/admin/
… интерфейс и конфигурация DecapCMSstatic/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. Подготовка репозитория локально
-
Создайте пустой репозиторий на GitHub
- Вкладка: GitHub > New repository
- Пример имени:
my-hugo-blog
-
Клонируйте репозиторий локально
-
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
-
-
Скопируйте упомянутые выше каталоги и файлы в корень репозитория
Проверка на замену (обязательно выполните правки)
-
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).
-
Первый коммит и пуш
-
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
-
Перейдите: GitHub > Settings > Developer settings > OAuth Apps > New OAuth App
-
Заполните поля:
- 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
← критически важно
- Application name:
-
После создания вы увидите:
- Client ID
- Client Secret (сгенерируйте и сохраните в надёжном месте)
Эти значения нужно добавить в переменные окружения Cloudflare Pages.
2-3. Настроить проект Cloudflare Pages
-
Навигация: Cloudflare Dashboard > Pages > Create a project > Connect to Git
-
Подключите GitHub и выберите репозиторий
my-hugo-blog
-
Настройте сборку:
-
Framework preset:
None
(или доверьтесь автоматическому определению Cloudflare) -
Build command:
hugo
-
Build output directory:
public
-
Переменные окружения:
HUGO_VERSION
=0.128.0
(пример; лучше совпадение с локальной версией Hugo)GITHUB_CLIENT_ID
= (значение из предыдущего шага)GITHUB_CLIENT_SECRET
= (значение из предыдущего шага)
-
-
Нажмите Save and Deploy и дождитесь первого деплоя
- После успеха появится превью-URL вида
*.pages.dev
- После успеха появится превью-URL вида
2-4. Подключить собственный домен www.example.com
-
Навигация: Cloudflare > Pages > (нужный проект) > Custom domains > Set up a custom domain
-
Добавьте
www.example.com
-
Если домен не управляется через Cloudflare:
- Выпишите DNS-записи, которые покажет Cloudflare (например,
CNAME www -> <project>.pages.dev
) - Зайдите в панель регистратора, где обслуживается домен, и создайте записи по этим данным (процедура зависит от конкретного регистратора)
- Выпишите DNS-записи, которые покажет Cloudflare (например,
-
После применения убедитесь, что сайт открывается по
https://www.example.com/
Подсказка: если домен уже находится в Cloudflare, нужные DNS-записи создаются автоматически по нажатию кнопки.
2-5. Войти в панель управления DecapCMS
- Откройте
https://www.example.com/admin/
- Нажмите кнопку вроде «Login with GitHub» и разрешите OAuth-авторизацию
- При первом входе GitHub спросит, одобряете ли вы приложение — нажмите Authorize
Если вход не удался, проверьте Callback URL OAuth и переменные окружения Cloudflare (
GITHUB_CLIENT_ID/SECRET
).
3. Эксплуатационные шаги (создание статей, синхронизация, правки шаблонов)
3-1. Создание записи через CMS
- Зайдите на
https://www.example.com/admin/
- В левом меню выберите Blog → New blog
- Заполните поля:
Title
,Publish Date
,Description
,Body
- После нажатия 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. Правка дизайна и шаблонов локально
-
Запустите локальный сервер
-
Windows
PS C:\work\my-hugo-blog> hugo server -D
-
macOS
mac:~/work/my-hugo-blog dev$ hugo server -D
-
Откройте
http://localhost:1313/
в браузере
-
-
Возможные точки изменений:
layouts/_default/baseof.html
…<head>
, шапка и подвалlayouts/_default/index.html
… список «последних записей» на главнойlayouts/_default/single.html
… шаблон страницы записиstatic/css/main.css
… цвета, шрифты, отступы
-
Зафиксируйте изменения и отправьте их
-
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 и оптимизации производительности
После запуска блога можно дополнительно улучшить поисковый рейтинг и скорость загрузки следующими шагами.
-
Настройте sitemap и robots.txt
- Зарегистрируйте
public/sitemap.xml
, который Hugo генерирует автоматически, в Google Search Console - Добавьте правила в
static/robots.txt
- Зарегистрируйте
-
Добавьте OGP (Open Graph Protocol) и Twitter Cards
- Пропишите в
layouts/_default/baseof.html
теги<meta property="og:title">
,<meta property="og:image">
- Так страницы будут привлекательнее при публикации в соцсетях
- Пропишите в
-
Конвертируйте изображения в WebP
- Используйте
resources/_gen/images/
и пайплайн Hugo для автоматического преобразования в WebP - Это ускоряет загрузку страниц
- Используйте
-
Расширьте работу с категориями и тегами
- Добавьте
categories
иtags
во Front Matter файловcontent/blog/
, выведите облако тегов в шаблонах
- Добавьте
-
Подключите структурированные данные
- Передавайте сведения об articles в формате JSON-LD, чтобы повысить шансы на rich results
8. Итоги
- Создаём репозиторий на GitHub, подключаем его к Cloudflare Pages, настраиваем GitHub OAuth для DecapCMS и привязываем собственный домен — и всё готово
- Записи создаются через
/admin/
, коммитятся в GitHub и автоматически деплоятся на Cloudflare - Шаблоны и дизайн редактируются локально через
hugo server
, затем изменения доставляются через Git
Таким образом, вы получаете рабочую серверлес-среду на базе Hugo + GitHub + Decap CMS + Cloudflare.
Пока что получилась лёгкая в поддержке площадка, но поиск по сайту, категории, теги и облако тегов всё ещё уступают полноценным блоговым платформам. Придётся временно смириться и постепенно развивать проект дальше вместе с ChatGPT.