попереднього разу я спробував нашвидкоруч переналаштувати генерацію блогу з архіву дописів за допомогою двигунця jekyll, але з першого разу не подужав і покинув аж на два місяці. сьогодні, маючи трохи більше часу, повернувся до цієї задачі, так само невдало: свіжовстановлена версія jekyll потребувала цілої купи модулів (gems), від яких залежить і сам jekyll, і деякі функції теми. вивчення ruby лише задля підтримки блогу в робочому стані аж ніяк не входить в мої плани, тож я здався і повернувся до думки перезібрати значно простішу конфігурацію з hugo.

скелет сайту на hugo

встановлення hugo (pacman бо arch):

> sudo pacman -S hugo

майданчик для експериментів:

> tree ~/blog/ -L2
~/blog/
├── blog.content        # вміст щоденника
│   ├── blog            # ...дописи
│   │   ├── 2001        №    ...за роками
│   │   ├── ...
│   │   ├── 2024
│   ├── drafts          # ...робочі чернетки
│   ├── images          # ...ілюстрації
│   │   ├── 2013        #    ...за роками
│   │   ├── ...
│   │   ├── 2024
│   └── pages           # ...копії сторінок поза щоденником
├── blog.jekyll         # старі файли для jekyll (тема, шаблони тощо)

важливо. теку blog/ довелося перейменувати на posts/ через дивний глюк codeberg pages (див. далі).

цього разу не хочу експериментувати з конвертуванням сайту jekyll: вже пробував, проблем чи не більше, ніж якщо починати з нуля. окрім того, я таки хочу почати з нуля, щоби спробувати зробити щонаймінімальніший сайт, без зовнішніх залежностей, без зовнішніх css-фреймворків та js-скриптів.

> hugo new site ~/blog/blog.test
...
> tree ~/blog/blog.test/ -L2
~/blog/blog.test/
├── archetypes
│   └── default.md
├── assets
├── content
├── data
├── hugo.toml
├── i18n
├── layouts
├── static
└── themes

додавання теми

створився порожній скелет для сайту. далі підказка радить вибрати тему оформлення на themes.gohugo.io; найменше, що знайшов з того, що не виїдає очі — hugo xmin (підказка з використаня):

> cd ~/blog/blog.test/themes/
> git clone https://github.com/yihui/hugo-xmin.git
...
> tree ~/blog/blog.test/themes/ -L2
~/blog/blog.test/themes/
└── hugo-xmin
    ├── archetypes
    ├── exampleSite
    ├── hugo-xmin.Rproj
    ├── images
    ├── layouts
    ├── LICENSE.md
    ├── README.md
    ├── static
    └── theme.toml

не розумію, навіщо майже всі нові теми вихваляються здатністю відмальовувати складні математичні формули: сайтів, де це справді потрібно, дуже мало. менше з тим… themes/hugo-xmin/exampleSite/ містить файли для швидкої випроби:

> tree ~/blog/blog.test/themes/hugo-xmin/exampleSite/ -L2
~/blog/blog.test/themes/hugo-xmin/exampleSite/
├── content
│   ├── about.md
│   ├── _index.markdown
│   ├── _index.Rmarkdown
│   ├── note
│   └── post
├── hugo.yaml
└── layouts
    └── partials

випроба:

> cd ~/blog/blog.test
> cp -r themes/hugo-xmin/exampleSite/content/* content/
> cp -r themes/hugo-xmin/exampleSite/layouts/* layouts/
> cp -r themes/hugo-xmin/exampleSite/hugo.yaml ./
> hugo server
...
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop

поки що все гаразд: у веб-оглядачі можна бачити згенерований взірець (http://127.0.0.1:1313).

конвертування дописів

спершу я гадав, що hugo має суттєво інший формат заголовків файлів markdown, тому почав шукати дієвий спосіб конвертувати свої більш як 2 тис. дописів; офіційна підказка згадує три; скрипти на pythonʼі не спрацювали, але штатна команда hugo import jekyll ..., що я нею вже скористався попереднього разу — так, хоч і з прикрим нюансом: вона конвертує ввесь сайт, а не лише дописи markdown, і відмовляється працювати, поки не знайде всю структуру тек сайту:

> cd ~/blog/
# затягнув копію сайту з сервера
> mkdir blog.jekyll
> scp tivasyk@server.lan:/home/jekyll/blog/\* blog.jekyll/
> cp blog.test/hugo.yaml blog.test/hugo.yaml.backup     # hugo import переписує!
> hugo import jekyll --force ./blog.jekyll/ ./blog.test/
> cp blog.test/hugo.yaml.backup blog.test/hugo.yaml     # відновлення налаштувань
> hugo server --source ~/blog/blog.test                 # локальний сервер з теки
...

працює. порівнюю файли дописів до і після конвертування… рукалице: як вчитатися, зрозуміло, що тут hugo конвертував лише формат дати (і поігнорував час) і попереставляв місцями блоки в заголовку.

> cd ~/blog/
> diff -y --suppress-common-lines blog.content/blog/2024/2024-05-29-freecad-beginning.markdown blog.test/content/post/2024/2024-05-29-freecad-beginning.markdown
layout: post                                        <
title:  "freecad: ресурси для вивчення"             <
date:   2024-05-29 17:30                            <
tags:                                               | date: "2024-05-29T00:00:00Z"
  - щоденник                                        | description: підказка з початків використання…
  - комп'ютери                                      |   моделей для друку на 3d принтері.
  - 3d друк                                         | tags:
  - підказки                                        | - щоденник
description: "підказка з початків використання…     | - комп'ютери
                                                    > - 3d друк
                                                    > - підказки
                                                    > title: 'freecad: ресурси для вивчення'

гірше, що не конвертовано внутрішні посилання на інші сторінки блогу {% blog_post 2024/2024-05-25-tasco %} — це формат jekyll, і hugo його не розуміє (див. документацію). конветор hugo перекладає ілюстрації до static/assets/images/; це незручно, я хочу тримати їх поруч дописів, у content/images/; це потребуватиме ще однієї зміни за допомогою awk до всіх посилань на зображення.

все це можна відносно легко робити старим як світ awk — три рядки, не рахуючи моїх коментарів:

# скрипт awk для конвертування файлів markdown у форматі jekyll до формату hugo
# використання: awk -f j2h.awk <path-to-Jekyll-post(s)>
{
    # перетворення дати
    #   2024-11-30 01:23 -> "2024-11-30T01:23:00"
    $0 = gensub(/^date:(\s+)([0-9-]{10})\s+([0-9:]{5})/, "date:\\1\"\\2T\\3:00\"", "g", $0)

    # перетворення крос-посилань
    #   {% post_url post/2024/2024-11-30-test_1 %} -> {{< ref "blog/2024/2024-11-30-test-1" >}}
    $0 = gensub(/\{%\s+post_url\s+([^ ]+)\s+%\}/, "{{< ref \"blog/\\1\" >}}", "g", $0)

    # перетворення посилань на зображення
    # ![назва](/images/2024/2024-11-30-image.jpg) -> ![назва](images/2024/2024-11-30-image.jpg)
    $0 = gensub(/!\[(.+)\]\(\/assets(.+)\)/, "![\\1](\\2)", "g", $0)

    print $0
}

awk не вміє рекурсивно обробляти цілі теки, але ж є find; випробування на blog.test:

> find ~/blog/blog.test/ -path '*/blog/*' -name '*.markdown' -exec awk -i inplace -f j2h.awk {} +

працює. ну от хто для такого розчохляє цілий python? ех, молодь…

резервна копія blog.content, і можна конвертувати:

> cd ~/blog/
> mkdir backup
> tar --exclude='.git' -zcvf backup/backup_blog.content_20241201.tar.gz blog.content
> find ~/blog/blog.content/ -path '*/blog/*' -name '*.markdown' -exec awk -i inplace -f j2h.awk {} +

збирання сайту з двох репозиторіїв

час спробувати «зібрати» все разом з двох окремих репозиторіїв. тестова тека blog.test стане порожньою основою для hugo; треба спорожнити content/ та public/:

> cd ~/blog/
> mv blog.test blog.hugo
> rm -rf blog.hugo/content/*
> rm -rf blog.hugo/public/*
> tree ~/blog/blog.hugo -L3
blog.hugo/
├── archetypes
│   └── default.md
├── content             # <- сюди підключатиметься blog.content/
├── data
├── hugo.yaml
├── i18n
├── layouts
│   └── ...
├── public              # <- сюди hugo генерує статичний сайт
├── static
│   └── assets
│       └── ...
└── themes
    └── hugo-xmin
        └── ...

локальна випроба, підключеня конвертованого вмісту (blog.content) до основи (blog.hugo); скористаюся mount --bind, бо канонічно і правильно:

> cd ~/blog/
> (sudo) mount --bind /home/tivasyk/blog/blog.content/ ./blog/blog.hugo/content/
> hugo server --source ~/blog/blog.hugo

it’s alive! відключаю…

> cd ~/blog/
> (sudo) umount ./blog.hugo/content/

тепер для blog.content та blog.hugo потрібно створити репозиторії; в моєму випадку — на codeberg.org. шпаргалка з налаштування й виштовхування (push), на прикладі blog.hugo:

> cd ~/blog/blog.hugo
> git init
> git remote add origin git@codeberg.org:tivasyk/blog.hugo.git
> git checkout -b master
> git add .
> git commit -m "add: перший коміт шаблону для сайту на hugo /ti"
> git push -u origin master

вивантаження згенерованого сайту на codeberg

локальний тестовий сервер запускається; залишилося навчитися автоматично виштовхувати згененований статичний сайт (вміст blog.test/pulic/) до третього репозиторію, з якого codeberg сам роздаватиме його. цю частину я вже випробував, але не автоматизував.

можу помилятися, але git автоматично розрізняє вкладені репозиторії як субмодулі. отже, якщо підключити вже існуючий репозиторій pages до blog.test/public/…

> git clone  git@codeberg.org:tivasyk/pages.git ~/blog/blog.test/public/

git затягнув увесь старий (jekyll) сайт з codeberg’а, він більше не потрібен (я маю копію); видалити (опційно), перегенерувати й виштовхнути до master на codeberg:

> rm -rf ~/blog/blog.test/public/*
> hugo --source ~/blog/blog.test
> cd ~/blog/blog.test/public/
> git add .
> git commit -m "add: видалення старого вмісту, перегенерування з hugo /ti"
> git push

щось працює! перестворений на hugo щоденник тепер доступний з codeberg – хоч і страшний, як смертний гріх, бо я не торкався теми hugo-xmin, а вона, виявляється, зовсім не підходить для блогів з великою кількістю дописів. але це вже інша задача. проявилася інша проблема…

глюк codeberg pages

codeberg відмовляється роздавати внутрішні сторіки блогу з теки blog/. чому? схоже на глюк codeberg, повʼязаний з одним із форматів urlʼу: https://<username>.codeberg.page/<repo>/... — тут <repo> може вказувати не репозиторій в профілі usernameʼу, якщо такий репозиторій існує. в мене такого немає, але посилання виду https://tivasyk.codeberg.page/blog/2024/2024-11-29-hugo-from-zero не працюють; і сторінками поза підтекою blog/ проблем немає.

довелося знову перейменувати blog/ на posts/, і прогнати ще один скрипт awk, щоби виправити всі внутрішні посилання; після цього всі сторінки відображаються.

автоматичне поновлення

можна спробувати автоматизувати поновлення сайту. найпростіший скрипт міг би працювати приблизно так:

зрозуміло, що для скриптування роботи з codeberg треба, щоби безпарольні ssh-ключі було зареєстровано на сервері. скрипт може бути якийсь такий:

#!/usr/bin/env bash
# tivasyk <tivasyk@gmail.com>
# мінімалістичний скрипт для перегенерування щоденника tivasyk@home і публікації на codeberg pages.

# безпечні опції: вихід за найменшої помилки
set -o errexit
set -o pipefail
set -o nounset

# репозиторії для збирання сайту
git_content="git@codeberg.org:tivasyk/blog.content.git"
git_hugo="git@codeberg.org:tivasyk/blog.hugo.git"
git_pages="git@codeberg.org:tivasyk/pages.git"

# локальні теки для збирання
work_dir=""
content_dir="content"
target_dir="public"

# константи
lock_file="/tmp/.republish_lock"
marker="◆"

# прибрати робочу теку й лок-файл на виході
function cleanup() {      
    if [[ -d ${work_dir} ]]; then
        printf "${marker} %s\n" "Removing work dir (${work_dir})..."
        rm -rf "${work_dir}"
    fi
    if [[ -f ${lock_file} ]]; then
        printf "${marker} %s\n" "Removing lock file (${lock_file})..."
        rm "${lock_file}"
    fi
}

# уникати запуску другої копії, якщо лок-файл вже існує
if [[ -s ${lock_file} ]]; then
    printf "${marker} %s\n" "A lock file (${lock_file}) exists, is another copy running ($(cat ${lock_file}))?"
    exit 0
fi

# зареєструвати cleanup() (важливо: після перевірки лок-файлу)
trap cleanup EXIT ERR

# створити лок-файл
printf "${marker} %s\n" "Creating lock file (${lock_file})..."
date -Iseconds > ${lock_file}

# тимчасова робоча тека для збрання
work_dir="$(mktemp -d)"

# клонувати шаблон
printf "${marker} %s\n" "Cloning template repo (${git_hugo}) into work dir (${work_dir})..."
git clone --depth 1 ${git_hugo} ${work_dir}

# клонувати вміст content
printf "${marker} %s\n" "Cloning content repo (${git_content}) into content dir (${work_dir}/${content_dir})..."
git clone --depth 1 ${git_content} ${work_dir}/${content_dir}

# клонувати цільову теку public
printf "${marker} %s\n" "Cloning target repo (${git_pages}) into target dir (${work_dir}/${target_dir})..."
git clone --depth 1 ${git_pages} ${work_dir}/${target_dir}

# перегенерувати сайт
printf "${marker} %s\n" "Running $(which hugo) to regenerate the site in ${work_dir}..."
hugo --source ${work_dir} || exit 3

# виштовхнути перегенерований сайт
cd ${work_dir}/${target_dir}
printf "${marker} %s\n" "Pushing the regenerated site to $(git remote get-url --all origin) from ${work_dir}/${target_dir}"
git add . \
    && git commit -m "mod: перегенеровано автоматично $(date -Iseconds)" \
    && git push

цей франкенштайн працює.

підсумки

чому так довго? я витратив досить багато часу на ці експерименти, щоби краще зрозуміти, що я роблю; коли вже знаєш — створити сайт на codeberg pages з нуля, чи конвертувати старі дописи — питання може на пів години.

чи можна без мороки з git і codeberg? так, я міг би відтворити зараз свій старий підхід, коли на постійно включеному серверочку лежала тека з цілим сайтом, туди з owncloud затягувалась копія лише теки з дописами (cron + rsync), генератор (hugo) детектував би зміни й перестворював public, а легенький веб-сервер (lighttpd) роздавав би сайт.

тоді навіщо?.. тому що git — це складно і непотрібно лише для людини, котра ще не зрозуміла життя. коли зрозуміє — це буде лише складно =)

чи можна максимально просто, і без самохосту? найпростіше: тримати на своєму компʼютері теку з сайтом (шаблон, дописи), але згенероване хостити на codeberg pages; знадобиться лише один репозиторій і спрощення скрипта — від нього знадобиться лише запускати hugo і виштовхувати public на codeberg; тоді створив новий допис в теці content, прогнав скрипт, все.

що далі?

використана документація та підказки: