Этот блог — папка с markdown-файлами внутри репозитория сайта. Ни базы данных, ни админки, ни серверного рантайма: команда сборки превращает каждый файл в готовый HTML, а сервер потом просто отдаёт файлы с диска. Ниже — почему я выбрал такую схему и что она означает для того, кто открывает страницу.
Почему markdown, а не CMS
Первая причина скучная: статья — это текст, а текст хорошо живёт в git. У каждой правки есть автор, дата и причина; откатить неудачную формулировку — одна команда. База данных так не умеет: там есть только текущее состояние и надежда на бэкап.
Вторая причина в том, что файл в репозитории лежит рядом с кодом, который его показывает. Меняя вёрстку статьи, я вижу настоящую статью, а не lorem ipsum из демо-данных. Это дисциплинирует: типографику приходится проверять на живом тексте со всеми его списками, таблицами и длинными ссылками.
Что даёт такой подход на практике:
- нечего ломать — нет админки, значит нет и панели, которую надо обновлять по безопасности;
- нечего терять — статьи и их история едут вместе с кодом в любой клон репозитория;
- нечего ждать — черновик открывается в редакторе за секунду, без логина и без сети;
- нечего чинить в проде — если сборка прошла, страница уже финальная.
Чем я за это плачу
Честно: публикация теперь равна коммиту. С телефона так не пишут, случайную опечатку не поправить за десять секунд из браузера, а предпросмотр требует локальной сборки. Для блога, который я веду сам и не каждый день, это приемлемая цена. Для редакции из пяти человек — уже нет, и я бы не стал советовать такую схему команде с потоком материалов.
Правило, которого держусь: если страницу можно отдать файлом, её надо отдавать файлом.
Формулы считаются на сборке, а не в браузере
В тексте про инженерию иногда нужна формула. Обычный путь — подключить библиотеку рендеринга и дать ей работать на стороне читателя. Я делаю это на сборке: markdown проходит через конвейер unified, где формулы превращаются в готовую разметку KaTeX ещё до того, как файл попадёт на сервер.
Порядок шагов важнее, чем кажется:
- разбор markdown и извлечение фронтматтера;
- GFM-расширения — таблицы, сноски, зачёркивание;
- пометка формул как формул;
- перевод в HTML-дерево;
- санация — чистим исходное дерево, пока в нём ещё нет чужой разметки;
- KaTeX, подсветка кода, якоря у заголовков;
- сборка строки HTML.
Санация стоит именно пятым шагом, до KaTeX: если пропустить её после, она выпотрошит математическую разметку, потому что не знает её тегов. Сам конвейер выглядит так:
const html = String(
await unified()
.use(remarkParse)
.use(remarkFrontmatter, ["yaml"])
.use(remarkGfm)
.use(remarkMath)
.use(remarkRehype)
.use(rehypeSanitize, schema) // до KaTeX, а не после
.use(rehypeKatex)
.use(rehypeStringify)
.process(source),
);
Статьи лежат в content/blog/*.md, имя файла становится адресом страницы. Никакого поля «URL» в форме, никакой рассинхронизации между ссылкой и заголовком.
Что из этого получает читатель
| Что происходит | CMS с рендером в браузере | Этот блог |
|---|---|---|
| Открытие страницы | запрос к базе, шаблон, ответ | готовый файл с диска |
| Превращение формулы в вёрстку | на устройстве читателя | один раз на сборке |
| Отключённый JavaScript | пустая страница или сырой текст | страница читается целиком |
Последняя строка — главная. Текст, списки, таблицы, подсветка кода и формулы существуют в HTML как есть. Скрипты на страницах статей не нужны ни для чего, кроме мелочей вроде аналитики, и их отсутствие ничего не ломает.
Время чтения считается, а не выдумывается
Подпись «7 мин чтения» под заголовком — не украшение, а обещание. Считаю её честно, по числу слов в исходном markdown при скорости слов в минуту1:
Округление вверх и минимум в одну минуту дают предсказуемый результат: короткая заметка никогда не покажет ноль, а длинная не притворится быстрой. Код и формулы из подсчёта не выбрасываю — их тоже читают, причём медленнее прозы, так что небольшой запас здесь уместен.
Такой же подход я держу и в остальном на сайте: меньше движущихся частей, больше вещей, которые вычислены заранее и не могут сломаться в рантайме. Более длинный рассказ о том, как я пришёл к этому способу работать, лежит в отдельном эссе.
Сноски
-
180 слов в минуту — рабочая константа, которую я выбрал для русского текста, а не измеренная величина. Если окажется, что оценка систематически врёт, поменяю одно число в коде. ↩