Org-roam. Экспорт в Hugo
Экспорт в Hugo из org-roam.
Содержание
1 Общая информация
- Базируется на пакете ox-hugo (см. Org-mode. Экспорт в Hugo).
2 Ошибки при экспорте
2.1 ox-hugo: broken links
2.1.1 Подкаталоги в каталоге org-roam
- Если каталог для org-roam содержит подкаталоги, то при экспорте теряются ссылки.
- Для ликвидации этой ошибки следует добавить в настройки:
(require 'org-roam-export)
2.1.2 Экспорт из поддерева
ox-hugo предлагает два варианта экспорта:
- экспорт всего файла;
- экспорт из поддерева.
При экспорте из поддерева ссылки типа
id_linkне отображаются.Происходит поиск таких ссылок только в текущем поддереве.
Для реализации поиска в других фалах нужно добавить их в переменную
org-id-extra-files:(setq org-id-extra-files (directory-files-recursively org-roam-directory ".*\.org$" t))- Впрочем, у меня это не сработало.
Также можно сделать хак для игнорирования ошибок:
(with-eval-after-load 'ox-hugo (setq org-hugo--preprocess-buffer nil))- Вот это у меня сработало.
3 Миграция на hugo page bundle
3.1 Свойства для экспорта
Пример задания свойств для экспорта:
:PROPERTIES: :export_hugo_section: blog/2026 :export_hugo_bundle: 2026-03-03--hugo-multilingual-support :export_file_name: index.ru.md :export_language: ru :END:При экспорте будет создана структура:
content/ └── blog/ └── 2026/ └── 2026-03-03--hugo-multilingual-support/ <-- папка бандла ├── index.ru.md ├── index.en.md (для английской версии) └── (ресурсы)export_hugo_section: blog/2026: задаёт путь к разделу (секции) внутриcontent/. Это может быть вложенная структура, напримерblog/2026создаст папкиblog/2026.export_hugo_bundle: 2026-03-03--hugo-multilingual-support: определяет имя папки самого бандла. Она будет создана внутри указанной секции.export_file_name: index.ru.md: имя файла внутри бандла. Для листового бандла обязательно должно бытьindex. Если не добавить расширение.md, ox-hugo обрежет доindex.export_language: ru: явно указывает язык.Для английской версии этого же поста создаётся отдельная секция с такими же
export_hugo_sectionиexport_hugo_bundle, но сexport_file_name: index.en.mdиexport_language: en.
3.2 Скрипт для миграции
3.2.1 Что делает
- Находит все блоки
:PROPERTIES:в.orgфайлах. - Извлекает значения
EXPORT_HUGO_SECTIONиEXPORT_FILE_NAME. - Определяет язык по префиксу
ru/илиen/в секции. - Формирует новые свойства:
export_hugo_section(без языкового префикса)export_hugo_bundle(старое значениеEXPORT_FILE_NAME)export_file_name: index.ru.md(илиindex.en.md)export_language: ru(илиen)
- Все ключи приводит к нижнему регистру (как вы просили).
- Сохраняет остальные свойства без изменений.
- Перезаписывает блок свойств в файле, создавая резервную копию (
.bak).
3.2.2 Использование
- Сохраните скрипт в файл, например
migrate_org_props.py. - Отредактируйте переменную
root_dirв начале скрипта, указав путь к корневой папке с вашими org-файлами. - Убедитесь, что
dry_run = True(режим просмотра без изменений). - Запустите скрипт:
python3 migrate_org_props.py. - Просмотрите вывод. Для каждого файла, где будут найдены подходящие свойства, появится сообщение
[DRY RUN] Будет изменён: .... Также для каждого изменяемого блока будет показан фрагмент старого блока. - Если всё выглядит правильно, установите
dry_run = Falseи запустите скрипт снова. - После завершения проверьте несколько файлов вручную и запустите
hugo serverдля проверки сайта.
3.2.3 Скрипт
#!/usr/bin/env python3
"""
Преобразование org-свойств для многоязычных page bundle.
Ищет блоки :PROPERTIES:, содержащие EXPORT_HUGO_SECTION и EXPORT_FILE_NAME,
и преобразует их в формат page bundle с языковыми суффиксами.
Все ключи свойств приводятся к нижнему регистру.
"""
import os
import re
import sys
from pathlib import Path
# ====== НАСТРОЙКИ ======
root_dir = "/путь/к/вашим/org-файлам" # <--- ИЗМЕНИТЕ ЭТО
dry_run = True # При True только просмотр, без записи
# =======================
# Регулярка для поиска блоков свойств
prop_block_re = re.compile(
r'^(\s*):PROPERTIES:\s*\n(.*?)^\s*:END:\s*$',
re.MULTILINE | re.DOTALL | re.IGNORECASE
)
# Регулярка для строк свойств
prop_line_re = re.compile(r'^\s*:([^:]+?):\s*(.*?)\s*$', re.MULTILINE)
def transform_properties(prop_text):
"""
Преобразует текст внутри блока свойств.
Возвращает новый текст или исходный, если преобразование не требуется.
"""
# Собираем все текущие свойства, ключи приводим к нижнему регистру
props = {}
for match in prop_line_re.finditer(prop_text):
key = match.group(1).strip()
value = match.group(2).strip()
props[key.lower()] = value
# Проверяем наличие необходимых свойств
if 'export_hugo_section' not in props or 'export_file_name' not in props:
return prop_text
section = props['export_hugo_section']
old_filename = props['export_file_name']
# Определяем язык по префиксу ru/ или en/
lang = None
if section.lower().startswith('ru/'):
lang = 'ru'
new_section = section[3:] # убираем 'ru/'
elif section.lower().startswith('en/'):
lang = 'en'
new_section = section[3:] # убираем 'en/'
else:
# Нет языкового префикса – пропускаем
return prop_text
# Обновляем свойства
props['export_hugo_section'] = new_section
props['export_hugo_bundle'] = old_filename
props['export_file_name'] = f"index.{lang}.md"
props['export_language'] = lang
# Сортируем ключи для стабильности порядка
sorted_keys = sorted(props.keys())
# Формируем текст свойств
lines = [f":{key}: {props[key]}" for key in sorted_keys]
return "\n".join(lines)
def process_file(filepath):
"""Обрабатывает один .org файл."""
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
new_content = content
modified = False
# Ищем все блоки свойств и обрабатываем каждый
for match in prop_block_re.finditer(content):
old_block = match.group(0)
indent = match.group(1) # отступ перед :PROPERTIES:
prop_text = match.group(2)
new_prop_text = transform_properties(prop_text)
if new_prop_text != prop_text:
# Восстанавливаем блок с тем же отступом
new_block = f"{indent}:PROPERTIES:\n{new_prop_text}\n{indent}:END:"
new_content = new_content.replace(old_block, new_block)
modified = True
# Показываем фрагмент старого блока для информации
print(f" Будет изменён блок: {old_block[:80]}...")
if modified:
if dry_run:
print(f"[DRY RUN] Будет изменён: {filepath}")
else:
# Создаём резервную копию с расширением .bak
backup = filepath.with_suffix(filepath.suffix + '.bak')
if not backup.exists():
os.rename(filepath, backup)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"Изменён: {filepath}")
else:
if dry_run:
print(f"[DRY RUN] Без изменений: {filepath}")
def main():
if dry_run:
print("*** РЕЖИМ ПРОСМОТРА – файлы не будут изменены ***\n")
else:
print("*** РЕЖИМ ЗАПИСИ – файлы будут изменены ***\n")
base = Path(root_dir).expanduser().resolve()
if not base.is_dir():
print(f"Ошибка: директория {base} не существует.")
sys.exit(1)
org_files = list(base.rglob("*.org"))
print(f"Найдено .org файлов: {len(org_files)}")
for org_file in org_files:
process_file(org_file)
if dry_run:
print("\n*** ПРОСМОТР ЗАВЕРШЁН. Для применения изменений установите dry_run=False. ***")
if __name__ == "__main__":
main()
