Я веду базу знаний в Obsidian, а этот сайт работает на Hugo. Копировать заметки вручную, перекладывать картинки и править ссылки - это путь к тому, чтобы забросить блог через неделю. В этой статье разберу работу Python-скрипта, который работает в Docker-контейнере на моем Worker-узле и делает всю нудную работу по публикации заметок за меня. Вчера доработал скрипт поддержкой внутренних ссылок и корректной обработкой вложений, считаю его законченным, решил описать принципы работы.
Задача
Obsidian и Hugo по-разному работают с файлами:
- Картинки: В Obsidian они могут лежать где угодно (у меня это папка
3 Бэкенди подпапки). Hugo требует, чтобы картинки лежали строго в/static/images. - Ссылки: Obsidian использует
[Wiki Links](/blog/blogwiki-links/). Hugo понимает только стандартный Markdown[Link](/blog/url/). - Отбор: Не все заметки из базы должны попадать на сайт.
Архитектура решения
Скрипт (script.py) упакован в Docker-контейнер obsidian-publisher. Он мониторит папку с заметками, которая синхронизируется на мой сервер через Syncthing.
Логика работы (цикл каждые 15 минут):
- Pull: Забирает свежие изменения из Git-репозитория сайта.
- Индексация: Сканирует папку ассетов и строит карту путей.
- Процессинг: Ищет файлы с тегом
публикация, обрабатывает их и копирует в папку контента Hugo. - Push: Если есть изменения, коммитит их и пушит в GitLab. Дальше в дело вступает CI/CD пайплайн.
Реализация
1. Глобальный поиск ассетов
Самая большая проблема - найти картинку. В заметке написано ![[screen.png]], но физически файл может лежать в 3 Бэкенд/Проекты/Скриншоты/screen.png.
Чтобы не хардкодить пути, скрипт сначала строит индекс:
ASSET_MAP = {}
def build_asset_map():
# Рекурсивно проходим по всем папкам источника
for root, dirs, files in os.walk(SOURCE_DIR):
if ".git" in root: continue
for file in files:
# Сохраняем абсолютный путь к каждому файлу
ASSET_MAP[file] = os.path.join(root, file)Теперь, встретив ссылку на файл, мы мгновенно находим его полный путь в словаре ASSET_MAP.
2. Регулярные выражения
Скрипт преобразует синтаксис Obsidian в синтаксис Hugo.
Картинки:
Превращает ![[image.png]] или  в , попутно копируя файл в нужную папку сайта.
Внутренние ссылки:
Превращает Заметка (здесь должны быть двойные квадратные скобки, но скрипт их обработает) в [Заметка](/blog/blogзаметка/).
3. Проблема “процентов” и кириллицы
Obsidian при вставке ссылок кодирует пробелы как %20. Если просто перевести это в slug, получится тест%20ссылки, который дополнительно придется обрабатывать уже в Hugo.
Для генерации красивых URL используется такая функция:
def slugify(text):
# 1. Сначала декодируем URL (убираем %20)
text = urllib.parse.unquote(text)
# 2. Приводим к нижнему регистру
text = text.lower().strip()
# 3. Заменяем пробелы на дефисы
text = re.sub(r'[\s_]+', '-', text)
# 4. Оставляем только буквы (включая русские), цифры и дефис
text = re.sub(r'[^\w\-]', '', text)
return textТеперь ссылка [Тест интеграции](/blog/test-integratsii/) превращается в чистый URL /blog/test-integratsii/.
4. Управление датой и сортировкой
Чтобы статьи на сайте шли в правильном хронологическом порядке, скрипт использует приоритеты:
- Если в заметке есть YAML-поле
date- берется оно. - Если нет - берется время изменения файла (
mtime).
Время форматируется в ISO 8601 (YYYY-MM-DDTHH:MM:SS), чтобы Hugo мог точно отсортировать записи.
Итог
Теперь процесс публикации выглядит так:
- Пишу заметку в Obsidian.
- Ставлю тег
публикация. - …Всё.
Через 15 минут (или быстрее, если триггернуть вручную) скрипт обработает файл, запушит в Git, GitLab CI соберет сайт, и статья появится на сайте. Ссылки работают, картинки на месте, файлы прикреплены. Абсолютно также работает и редактирование: стоит мне внести любые изменения (как здесь, это я пишу уже через два дня после публикации) - изменения появятся на сайте. Возможно, стоит добавить в начале статьи поля “дата публикации” и “дата изменения”, чтобы Hugo сортировал только по дате публикации, но над этим я подумаю позже.
Возможная проблема - если вы используете прекоммит в Git, нужно добавить этот скрипт в исключения, потому что в нем используются двойные пробелы, которые критичны для синтаксиса Hugo, а прекоммит их убирает.
Самостоятельное развертывание
Если вдруг вы набрели на этот сайт и хотите развернуть данный скрипт у себя - исходники во вложении или в моем GitHub. В начале скрипта - переменные, которые можете настроить самостоятельно под ваши данные.
Docker Compose (используются переменные Jinja2, можете захардкодить свои):
obsidian-publisher:
build: {{ root_dir }}/obsidian_publisher
container_name: obsidian-publisher
restart: unless-stopped
user: "{{ puid }}:{{ pgid }}" # или "1000:1000" по умолчанию
environment:
- TZ={{ timezone }}
- SITE_REPO_URL=ssh://git@gitlab.example:2222/user/site.git # Ваша ссылка на Git
- GIT_SSH_COMMAND=ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i /ssh_keys/id_ed25519 # Git SSH Command
volumes:
- {{ data_drive_path }}/obsidian:/data/obsidian:ro # Исходники заметок
- site_repo_data:/app/site-repo # Репозиторий сайта
- /home/{{ sys_user }}/.ssh/id_ed25519:/ssh_keys/id_ed25519:ro # SSH ключСам архив со скриптом и Dockerfile: 📎 obsidian_publisher