Как я веду свой сайт в 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 и она делает следующее:

  1. Даёт выбрать раздел сайта.
  2. Запрашивает название поста на русском.
  3. Переводит его в человекопонятный URL: "Как я провёл лето" -> kak-ia-provel-leto
  4. Делает перевод заголовка на английский и позволяет оставить его, или отредактировать.
  5. Делает пункт 3 для английского заголовка.
  6. Создаёт два 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, она в удобном виде даст выбрать пост на который хочу поставить ссылку, отображая актуальные заголовки и раздел сайта.

mosg-select-link-to-post.webp

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. Итог

На данный момент я очень доволен результатом. Я могу писать посты с любым форматированием в любимом редакторе, всё нужное делается через комбинации клавиш в нём же, и по итогу получаю сайт, который выглядит и работает как я хочу.