Сайт
Статический сайт из markdown-файлов репозитория. Генерируется, не пишется руками.
python tools/site/build.pyОткрывать site/index.html — работает файлом, без сервера.
Правь только
.md. Файлы вsite/не редактируются никогда: они
перезаписываются при каждой сборке, и правка html руками теряется молча.
CI проверяет, чтоsite/соответствует markdown, — забыл пересобрать,
сборка упадёт.
Что внутри
tools/site/
├── build.py генератор
├── markdown.py конвертер markdown → HTML
├── bundle.py сборка в один html-файл
├── check_site.py проверка собранного сайта
├── verify_build.py побайтовая воспроизводимость
└── assets/
├── style.css
├── app.js тема, меню, поиск
├── alchemy-map.js интерактивная карта
├── scaler.js пересчёт порций
└── diagrams/ схемы mermaid, готовые SVGВывод зеркалит структуру репозитория: docs/base/alchemy.md →
site/docs/base/alchemy.html, README.md → site/index.html.
Ключевые решения
Данные отдаются как .js, а не .json. На file:// браузер блокирует
fetch локальных файлов, а <script src> — нет. Ради этого сайт открывается
без сервера, в Electron и на телефоне.
Схемы mermaid пре-рендерятся в SVG при сборке. Вендорить mermaid в клиент
значило 5,2 МБ на каждую страницу. Если SVG нет, а рендерер доступен:
npx -y @mermaid-js/mermaid-cli -i файл.mmd -o tools/site/assets/diagrams/имя.svg -t neutral -b transparentРендерера нет — в сборку попадёт исходник схемы, страница останется читаемой.
Свой конвертер markdown вместо библиотеки. Зависимостей в проекте нет.
markdown.py покрывает используемое подмножество. Принцип: сначала
экранируем всё, потом размечаем — так текст не может случайно стать разметкой.
Эмодзи запрещены, иконки Lucide встроены в разметку. Подмены эмодзи на
иконки нет: правило репозитория запрещает эмодзи совсем, проверяет
tools/check_no_emoji.py. Иконки Lucide живут только в интерфейсе — лупы
масштаба, шеврон скрытия панели — и вставлены прямо в HTML генератора.
Возможности
| Что | Как работает |
|---|---|
| Поиск | Индекс при сборке, по названию, заголовкам и тексту. Все слова запроса должны найтись. Ctrl+K |
| Тёмная тема | CSS-переменные, в localStorage, изначально по системной |
| Меню в шапке | Четыре раздела вместо списка страниц, описаны в nav.sections в ../../data/site.json. Аккаунт — значок справа, а не раздел |
| Оглавление | Слева список ## и ### текущей страницы, раздел подсвечивается при прокрутке |
| Полоска прогресса | Два пикселя под шапкой |
| Карта превращений | Отдельная страница на весь экран, подробности в ../../map.md |
| Переключатель вариантов | Варианты одного блюда на одной странице, кнопки сверху |
| Пересчёт порций | JSON рецепта вшит в страницу, множитель произвольный; галочка в строке убирает отключаемый ингредиент по полю give |
| Работа офлайн | Ни одного сетевого запроса |
Меню отвечает на вопрос «куда идти» и должно помещаться в одну строку,
поэтому это категории. Оглавление отвечает на «где я в этой статье» и
принадлежит конкретной странице. Оно собирается из уже отрендеренного
HTML, а не из markdown: тогда не может разойтись со страницей, даже если
заголовок добавила сборка.
Раздел набирает страницы по префиксу (docs/base/), а exclude отсеивает
и точную страницу, и целый каталог (docs/chocolate/recipes). Рецепты в
меню не перечисляются: на них ведёт страница «Рецепты». На телефоне меню —
короткий список под кнопкой, категории раскрывает человек, а не скрипт.
Карта превращений
site/map.html — не статья, а приложение на весь экран, поэтому своя
оболочка рисуется в build.py функцией render_map_page.
| Что | Где |
|---|---|
| Данные: узлы, рёбра, роли, варианты | ../../data/alchemy.json |
| Поведение: раскладка, подсветка, масштаб, управление | ../../map.md |
| Код | assets/alchemy-map.js |
Правила данных, которые проверяет код: у ребра с флагом alt должен быть
узел с вариантами, а pick у варианта — указывать на узел с пунктирной
стрелкой. Иначе на схеме будет утверждение, которому нет соответствия в
статье.
Проверка
python tools/site/build.py # собрать
python tools/site/check_site.py # проверитьcheck_site.py ловит битые внутренние ссылки и несуществующие ассеты,
остатки markdown (**, #, |), ссылки на .md, которые забыли переписать
в .html, несуществующие docUrl и recipeUrl в данных карты, страницы без
поиска, темы, карты или пересчёта там, где они обязательны, и подозрительно
короткие страницы.
Как пользоваться
Просто открыть: python tools/site/build.py --clean, дальше
site/index.html. Без сервера и без сети.
Один файл — для телефона и для сервера:
python tools/site/build.py --single-filedist/alchemistry.html: все страницы, стили, скрипты и схемы внутри одного
файла около 820 КБ. Внутри роутер по хешу: #/docs/base/alchemy.html. Файл
можно перебросить в мессенджер, открыть в браузере на телефоне или положить
на свой сервер как есть.
В Electron:
// main.js
const { app, BrowserWindow } = require('electron');
const path = require('path');
app.whenReady().then(() => {
const win = new BrowserWindow({ width: 1100, height: 800 });
win.loadFile(path.join(__dirname, 'dist', 'alchemistry.html'));
});
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });npm init -y
npm i -D electron electron-builder
npx electron .На телефон: dist/alchemistry.html открывается в браузере без установки
чего-либо. Нужно приложение, а не страница — подойдёт Capacitor, он тоже
умеет отдавать локальные файлы и собирается под Android и iOS.
Публикация: сборка от неё не зависит — на входе markdown, на выходе
папка файлов или один html. Нужен сайт с адресами вида
/docs/base/alchemy.html → папка site/; нужен один файл и нет возни с
путями → dist/alchemistry.html. GitHub Pages берёт папку site/ и работает,
пока репозиторий публичный.
Свой сервер: site/ лежит в /var/www/html/alchemistry, рядом служба
server/server.mjs, а nginx отдаёт статику и проксирует /api/ на неё.
Подробности — в server/README.md, раздел «Как это стоит на alchemistry.su».