Как я веду свой сайт в Emacs
1. Что пробовал и к чему пришёл
Я веду все свои заметки в формате Org. В начале я пробовал создавать один из своих сайтов в WordPress. Это было удобно и быстро по настройке, в том числе я использовал какой-то плагин для двуязычного сайта. Но процедура создания поста меня стала утомлять: нужно было вставить текст из Org файла (ну а где мне ещё писать? :), потом заменить форматирование, потом настроить языковые версии. Я знаю, что есть плагины, для создания постов из Markdown, но они не решили бы всех проблем.
От сайта мне хотелось чего-то минимального и быстрого. Поэтому я стал смотреть на генераторы статических сайтов. Я попробовал Hugo, Pelican и StatiCL. Последний мне понравился больше всего. Но почти везде для нормальной работы нужно было использовать Markdown и были проблемы с двумя языковыми версиями.
Параллельно я изучал и настраивал org-publish. Это мне больше понравилось, но настройка превращалась в огромную кучу кода. Поэтому я решил с помощью LLM написать своё решение. Я очень плохо на данный момент знаю elisp, а с LLM вопрос решился довольно быстро.
2. MOSG
Я назвал своё решение Multilanguage Org Static site Generator — mosg :). Пока он не доступен публично, потому что в процессе разработки и содержит кучу не очень хорошего кода от LLM.
Как теперь стала выглядеть моя работа с сайтом.
2.1. Структура файлов
❯ tree -L2 -d content
content
├── en
│ ├── guides
│ ├── misc
│ ├── sf
│ └── tech
├── images
└── ru
├── guides
├── misc
├── sf
└── tech
Все Org файлы расположены в папке content, в ней папка для всех изображений и папки для языковых версий сайта. В последних находятся папки для разделов и сами посты в них.
В корне языковых папок находятся файлы index.org, которые отвечают за главную страницу. В корне папок с разделами автоматически генерируется index.org, который содержит список постов.
В папке static находятся файлы, которые копируются в корень сайта как есть. В папке html находятся шаблоны для HTML секции head, а также языковые версии для шапки и подвала сайта.
Конкретный раздел выглядит вот так:
❯ tree content/en/misc
content/en/misc
├── index.org
├── multipotentiality-in-the-modern-world.org
├── what-i-don-t-like-about-telegram.org
└── what-if-you-paid-people-to-do-what-they-already-want-to-do.org
Его итоговая версия на сайте:
❯ tree public/misc
public/misc
├── index.html
├── multipotentiality-in-the-modern-world
│ └── index.html
├── rss.xml
├── what-i-don-t-like-about-telegram
│ └── index.html
└── what-if-you-paid-people-to-do-what-they-already-want-to-do
└── index.html
Т.е. имя Org файла используется в качестве пути в адресе сайта. Также на можно выбрать будут ли в путях конкретные файлы или pretty URLs. У меня используется последнее, поэтому полный адрес до страницы выглядит вот так:
/misc/what-i-don-t-like-about-telegram/
Вместо:
/misc/what-i-don-t-like-about-telegram.html
2.2. Настройка
Вот моя настройка для языков и секций сайта:
(setq mosg-languages '("en" "ru")
mosg-default-language "en")
(setq mosg-language-config
'(("en" . ((:locale . "en_US")
(:date-format-full . "%1$s %2$d, %3$d") ; "January 18, 2026"
(:date-format-day-month . "%2$d %1$s") ; "18 January"
(:months . ["January" "February" "March" "April" "May" "June"
"July" "August" "September" "October" "November" "December"])
(:months-short . ["Jan" "Feb" "Mar" "Apr" "May" "Jun"
"Jul" "Aug" "Sep" "Oct" "Nov" "Dec"])
(:ui-published . "Published:")
(:ui-updated . "Updated:")))
("ru" . ((:locale . "ru_RU")
(:date-format-full . "%2$d %1$s %3$d") ; "18 января 2026"
(:date-format-day-month . "%2$d %1$s") ; "18 января"
(:months . ["января" "февраля" "марта" "апреля" "мая" "июня"
"июля" "августа" "сентября" "октября" "ноября" "декабря"])
(:months-short . ["янв" "фев" "мар" "апр" "мая" "июн"
"июл" "авг" "сен" "окт" "ноя" "дек"])
(:ui-published . "Опубликовано:")
(:ui-updated . "Обновлено:")))))
(setq mosg-sections
'((tech . ((:titles . (("en" . "Tech Blog")
("ru" . "Технические посты")))
(:rss-titles . (("en" . "Flin: Tech Blog")
("ru" . "Флин: Технические посты")))
(:descriptions . (("en" . "Technical articles and programming notes")
("ru" . "Технические статьи и заметки о программировании")))))
(sf . ((:titles . (("en" . "Science Fragments")
("ru" . "Научные фрагменты")))
(:rss-titles . (("en" . "Flin: Science Fragments")
("ru" . "Флин: Научные фрагменты")))
(:descriptions . (("en" . "Information from various sciences from an amateur")
("ru" . "Информация из разных наук от дилетанта")))))
(guides . ((:titles . (("en" . "Various guides")
("ru" . "Различные руководства")))
(:rss-titles . (("en" . "Flin: Guides")
("ru" . "Флин: Руководства")))
(:descriptions . (("en" . "Various technical manuals")
("ru" . "Различные технические руководства")))))
(misc . ((:titles . (("en" . "Various Posts")
("ru" . "Разные посты")))
(:rss-titles . (("en" . "Flin: Various Posts")
("ru" . "Флин: Разные посты")))
(:descriptions . (("en" . "What does not fit into other sections")
("ru" . "Что не подходит в другие разделы блога")))))))
В ней задаются доступные языки сайта, его разделы, локализация и формат даты, а также заголовки и описания разделов.
Текущая версия со всеми настройками находится вот тут.
2.3. Создание поста
Через комбинацию клавиш я вызываю mosg-create-post и она делает следующее:
- Даёт выбрать раздел сайта.
- Запрашивает название поста на русском.
- Переводит его в человекопонятный URL: "Как я провёл лето" ->
kak-ia-provel-leto - Делает перевод заголовка на английский и позволяет оставить его, или отредактировать.
- Делает пункт 3 для английского заголовка.
- Создаёт два Org файла с русской и английской версией, переключается в буфер русской версии.
2.4. Свойства файла
Понимаются следующие дополнительные свойства для всего Org файла:
date,updated: даты написания и обновления поста.translation: имя файла без расширения версии поста на другом языке. Если отсутствует, то при генерации сайта выведется предупреждение. Это мне нужно для понимания, что нигде нет ошибки в связывании переводов. Можно установить в значениеnil, это значит, что для поста нет перевода на другой язык.toc: включать или нет для поста оглавление. Перезаписывает глобальную настройку.meta_desc: содержимое тегаmeta description, может отсутствовать.og_image: изображение для OpenGraph. Если отсутствует, то применяется глобальная настройка.og_title: заголовок OpenGraph. Если отсутствует, то берётся заголовок страницы.og_desc: описание OpenGraph. При отсутствии берётсяmeta_desc.
Например этот пост:
title: Как я веду свой сайт в Emacs
date: 2026-01-27
translation: how-i-manage-my-website-in-emacs
meta_desc: Описание того как я выбирал между различными генераторами сайта и остановился на своём решении с использованием Emacs
2.5. Изображения
Для вставки изображения в пост можно вызвать mosg-insert-image. Эта команда спросит откуда я хочу взять изображение: из буфера обмена или файла. Если из файла, то дальше я его выбираю; планирую добавить отображение миниатюр. Если из буфера, то создаётся временный файл с его содержимым.
Дальше изображение обрезается до нужных максимальных размеров, сжимается, конвертируется в нужный формат (у меня это webp) и копируется в папку с изображениями сайта. Потом ссылка на него вставляется на место курсора.
2.6. Ссылки на другие посты
Я могу выполнить команду mosg-insert-port-link, она в удобном виде даст выбрать пост на который хочу поставить ссылку, отображая актуальные заголовки и раздел сайта.
2.7. SEO
Есть команда, которая через LLM генерирует meta description для текущего поста в буфере. Есть также команда, которая делает тоже самое для всех постов сайта, где отсутствует свойство meta_desc.
Я это сделал в том числе для того, чтобы одним махом сделать описания для накопившихся постов, которые я переносил из Телеграма.
Корректно вставляются теги canonical, og и twitter. Корректно генерируется Sitemap со связанными языковыми версиями постов и разделов сайта.
2.8. RSS
Отдельные файлы RSS генерируются для языковых версий и разделов сайта, чтобы можно было гибко выбрать на что подписаться. Они содержат полный текст постов.
2.9. Генерация и деплой
Одной командой mosg-build можно сгенерировать сайт. Она хранит контрольные суммы постов в отдельном файле и генерирует HTML только для постов с изменениями.
Команда mosg-deploy асинхронно вызывает настроенную shell команду (у меня это rsync) и показывает результат в отдельном окне.
3. Итог
На данный момент я очень доволен результатом. Я могу писать посты с любым форматированием в любимом редакторе, всё нужное делается через комбинации клавиш в нём же, и по итогу получаю сайт, который выглядит и работает как я хочу.