Делаю свой генератор статического сайта
Вступление
Решил я, что пора отвлечься от работы над игрой, и заняться своим сайтом.
У меня нет профессионального опыта в управлении или создании сайтов. Раньше, я создавал, кое-как, свои персональные странички:
- Вначале использовал Hugo.
- Делал react
ssg
черезvite-ssr
и генерировал странички по роутам, как будто у меня SPA сайт (single page application). - Потом решил попробовать переделать всё под 11ty/eleventy...
И вроде все решения рабочие, гибкие, удобные, расширяемые... опять же разобраться несложно. Но, эти решения нужно изучать, понимать как они работают внутри, а я люблю всё делать без зависимостей, с нуля, именно по причине того, что я сам хочу учиться создавать решения. Я отнюдь не frontend developer, больше люблю писать игры. Я не представляю, как должен выглядеть мой сайт, множество возможностей из готовых решений сильно отвлекают от главного - создания игр и публикации статей.
По этим причинам я принял волевое решение создать (пересоздать) свой сайт с нуля... опять... снова...
Начинаем
Чего я хочу получить от своего сайта в итоге:
- Набор простых html страничек, без всякого javascript кода, по возможности.
- Использовать Markdown для создания и форматирования текстовых материалов.
- Подсветка синтаксиса кода
- Dark / light тема
- Удобный Просмотр с ПК и мобильного
- Два языка : Английский и русский
- Быстродействие генерация
- Быстрая загрузка страничек
- Красивые emoji
- Не встраивать содержание с других сайтов, всё должно быть замкнуто, по возможности, в одном месте
- Векторные SVG иконки
- Список соцсетей
- sitemap.xml (говорят, что всё таки должен быть)
- Статические файлы (такие как, robots.txt, ads.txt, файлы для верификации сайта, вплоть до папки с мелкой HTML5 игрой-прототипом, вшитой прямо в сайт)
- Быстрый Поиск по сайту
- Atom/RSS фид (уже не знаю, а нужен ли он вообще)
Новый хостинг
Поменял хостинг с firebase hosting на cloudflare. Добавил простую переадресацию на новый сайт в правилах firebase, где-то я адрес сайта поменять просто не смогу долгое время чисто физически. В итоге старый сайт eliasku-games.web.app просто ведёт на главную страницу текущй версии сайта eliasku.win. Наконец-то я купил себе короткий домен для сайта и настройка его была очень простой, cloudflare
даёт возможность покупки прямо в админке, даже кастомный почтовый адрес для сайта сделал. Все https
сертификаты так же работают из коробки.
Чтобы я не выбрал, теперь я уверен, что весь контент и генератор сайта останется у меня в репозитории и бекапах.
ℹ️ Советую рассмотреть так же netlify. Чтобы не засорять сайт, можно публиковать свои HTML5 демки и черновики игр на
netlify
. А так же, каждый черновик или прототип остаются в отдельном репозитории и будут иметь читаемый публичный домен, так же очень удобная автоматизация сборки и публикации по изменениям в репозиториях (так же бесплатно для приватных репозиториев)
Архитектура генератора
Проект делиться на 2 большие части:
- генератор и команды
- исходные материалы, статические ресурсы и шаблоны
Код и Скрипты
Проект представляет собой приватный npm пакет. Для кода использую TypeScript, запускаю всё это на NodeJS, через TSX. Для скриптов этот подход работает идеально (для меня / на данный момент).
Для работы над сайтом мне достаточно запустить npm start
и работать над своими материалами, и даже исправлять код генератора!
Для полной генерации и деплоя останется сделать только выполнить несколько команд:
npm run build
npx wrangler pages deploy ./dist --project-name ....
Дальше напишу о крупных функциональных блоках проекта
dev-сервер
Очень удобно иметь какой-то свой локальный сервер для быстрого предпросмотра результата работы генератора, тоесть предпросмотр вашего итогового сайта. Он автоматически будет следить за папками материалов и кода генератора сайта, перезапускать генерацию сайта при любых изменениях. Чтобы в реальном времени видеть даже изменения в логике кода самого генератора, я каждый раз запускаю отдельный процесс для скрипта генерации, что логически отделяет утилиту генерации от самого сервера.
Из улучшений я, конечно, хотел бы добавить автоматическую перезагрузку страницы сайта при изменениях. Делается легко, через дописывание маленького скрипта на каждую страничку в генераторе, который будет слушать Event Stream, получать нужный сигнал, и дёргать перезагрузку страницы.
Генератор статического сайта
Генератор - это код, который генерирует и выводит сайт в папку dist/
, которую, в свою очередь, уже можно размещать на настроенном под ваш сайт CDN.
Подготовка ресурсов
Преобразование и сжатие картинок, или любых других ресурсов, которые требуют промежуточной обработки. Здесь генерируется карта путей к ресурсам (маппинг), чтобы связать адрес на картинку в содержании с адресом в итоговой сборке сайта. Так же, некоторые файлы и папки мы копируем "как есть" без создания маппинга.
Обработка изображений и хеш-сумма
Для картинок использую библиотеку sharp.
- Cжимаю в
webp
с потерей качества (ограничение в размере пока не сделал). - Вычисляю хеш от содержимого файла, формирую имя файла, сохраняю в папку
public/a/
. - Добавляю соответствие в карту путей, например,
/games/duckstazy/duckstazy-1.png
найдём по адресу/a/4uvxTHwp.webp
. - При генерации меняем ссылки, в соответствии, с полученной картой путей.
Другие ресурсы работают по той же схеме, но просто без доп. трансформации. В любом случае контроль полностью у меня в руках.
Содержание
У меня из содержания что.. Странички моих старых игр, и несколько старых черновиков и статей. Я просто перенёс всё это "как есть". Немного пришлось что-то где-то подправить и улучшить.
Шаблоны

Зачем нужны шаблоны. Наша цель - выдать html код, мы бы его и просто слиянием строк могли бы получить. Но удобнее конечно писать какой-нибудь html классический и в каких-то местах вставлять что-нибудь эдакое (привет, PHP).
Для шаблонов я решил использовать шаблонную библиотеку eta: без зависимостей, поддержка typescript
, минималистичный функционал. Мне понравилось, выглядит привычно, концептуально похоже на то, что используется в генераторах статических сайтов (иерархия и включение частей шаблонов).
Мета-данные
Front-matter для мета-данных - это наверное стандарт. Не знал вначале, насколько будет удобно, но в любом случае, хотелось взять что-то готовое.
Markdown файлы
Для рендера markdown файлов (превращения .md
в html
код), решил попробовать marked (есть плагины, есть типы, вроде более популярный чем markdown-it
, проверил что есть плагины для якорей, синтаксиса, ссылок).
В итоге Table of contents написал регуляркой, якоря тоже регуляркой, ссылки на картинки и другие шорткаты инъецировал в кастомный Renderer. В итоге, конечно, много всего есть, но было ощущение, что всего мало. Но в результате расширять его оказалось достаточно просто, пока я смирился.
Подсветка синтаксиса
Хотелось использовать PrismJS, но как будто не сильно подходит для работы с marked
, или я просто не разобрался, как это вообще работает. Пришлось взять самый популярный highlight.js, поменять тему (которых, как оказалось, навалом, просто нужно искать css
в проектах самих тем). Для примера, вот тема catppuccin.
Локализация
Cтруктура папок
Нужно было определиться, все рекомендуют делать структуру так же как это выглядит на сайте.
Для локализации сайта я выбрал стратегию https://eliasku.win/blog/
- английская версия, https://eliasku.win/ru/blog/
- русская версия. В начало пути сайта добавляется код языка. Ничего не добавляется для основного английского языка.
В лучших практиках нашёл совет делать такую структуру материалов:
./ru/blog/post1/index.md
./ru/blog/post2/index.md
./en/blog/post1/index.md
./en/blog/post2/index.md
Это неудобно. Мало того что картинки вы может и не хотите дублировать между 2 языками, так еще и сверяться не удобно, нужно лезть от корня далего внутрь, чтобы глазами увидеть, что, например, русской версии статьи нету. Тем более, чисто логически, мне удобнее работать от концепции сущности - создал Статью/Игру, внутри этой папки все картинки, переведенные версии. Поэтому я решил что у меня будут сущности страницы, которые имеют набор вариантов для разных языков. Концептуально - добавляем Статью, в Статью добавляется 2 версии этой страницы: русская и английская. Дальше просто перебираем в удобной в коде структуре. Именно Страница имеет 2 варианта, и 2 страницы с разными адресами в итоге. Но логически это именна та статья! (к которой, допустим, нет перевода). Пришел в итоге к такому варианту:
./blog/post1/index.md
./blog/post1/index.ru.md
./blog/post2/index.md
./blog/post2/index.ru.md
Дата и Время
Для работы с датами я решил использовать библиотеку luxon, так же поддерживается локализация даты (например, имена месяцев). Из минусов - нет объявления типов для typescript
в самой библиотеке.
DateTime.fromJSDate(date, { zone: "utc" }).setLocale("ru").toFormat("dd LLLL yyyy");
Словарь
1 шаблон или страница так же могут быть не разделены на 2 варианта под каждый язык, а просто генерироваться из одного материала, но под разные языки, просто подставляя переводы для нужных текстов из общей таблицы переведённых строк. Словарь в данных:
export const loc = {
en: {
copyright: "Copyright",
// ...
},
ru: {
copyright: "Авторские права",
// ...
},
};
Использование локализованной строки в шаблоне
<%~ it.loc.copyright %>
Беда со статьями, созданными ранее
Писал постмортем для игры 13 на hashnode изначально, перенёс статью на свой сайт. В статье остались twitter посты (из-за видео), дело в том, что в twitter "нельзя просто взять" и скачать свои же видео на сайте платформы. Пришлось мне, через всеми известные методы, выкачивать видео, для того чтобы разместить их у себя на сайте и УДАЛИТЬ этот дурной twitter embed js скрипт для встраивания постов из твиттера!
dzen - тоже проблемы при переносе.
Вывод: популярные, закрытые и разрекламированные площадки не дают вам в нормальном виде экспортировать свои материалы - вам всё равно придется что-то править, форматирование переделывать.. Вообще, статья принадлежит именно вам. А все эти сервисы просто эксплуатируют ваш труд на своих "удобных" платформах. Я знаю, что многие действительно зарабатывают деньги, создавая материалы специально для этих платформ, но другие могут заблуждаться и тратить свой труд впустую.
Ссылки на социальные сети, чтобы не потеряться
тут мне было интересно использовать просто <svg>
код для иконок ссылок. Оказалось, что это проще, чем использовать какие-то там fontawesome.
Добавил новые ссылки на bsky
, mastodon
, telegram
. Поигрался с цветом для узнаваемости и добавил названия. Ибо ссылок много и они должны отличаться.
Другие интересные мелочи
Адреса и русский язык
Для создания якорей в Содержании статьи (Table of Contents) нужно сгенерировать ссылки на заголовки внутри страницы. Для генерации этих ссылок у нас может быть только заголовок на русском языке, поэтому нужно позаботиться о создании транскрипции латинскими символами. Для адресов страниц, при желании, можно сделать тоже самое. Но данная уже написанная функция будет крайне полезной, так же её можно будет расширять другими языками в будущем.
sitemap.xml
Самая простая часть, у вас есть список страниц и их конфигурации, вам осталось только сгенерировать sitemap.xml
правильно. В сети много информации как это нужно делать, я особо не заморачивался и использовал свой старый шаблон. Единственное, что стоит учесть, вы должны сделать настройку страницы, чтобы её можно было исключить из карты сайта. Например, страница 404.html
или черновик новой статьи, которым делимся с друзьями 🥴 для ранней обратной связи.
Cursor и ИИ-ассистент
В конце переписывания, я таки перешёл в Cursor и продолжал писать сайт в нём с поддержкой claude-3.5
. Важно отметить, что он не смог бы создать то, что мне было нужно чисто проптами и исправлениями, даже с ясными инструкциями и правилами для него. Весь код очень простой, тут важно придумать КАК ваш сайт будет генерироваться. Отдать писать всё ИИ было бы плохо, потому что код был бы сильно перегружен лишними функциями, которые естественно модель вспомнит из мирового достояния написания генераторов статических сайтов. С другой стороны, какие-то функции он напишет за вас. Код должен быть понятным и иметь более менее линейную структуру выполнения. Например, модель смогла без ошибок самостоятельно вспомнить и адаптировать код для генерации якорей в оглавлении страницы.
Lighthouse тест
Результат не 100%, потому что CloudFlare добавляет на сайт свои скрипты:
- обфускация email адреса для ботов (ttl час)
rum
- что-то вроде встроенной аналитики просмотра страницы, тоже сделано скриптом, ещё он пишет в консоль ошибки, что так же немного снижает итоговый результат теста.
Я не хочу их пока выключать, потому что пусть будут, а результат даже с ними достаточно хороший.
Конец
Дочитали до конца!? Вы получили от меня новое достижение!
Что вам еще хотелось бы узнать подробнее? Может быть возникли вопросы или что-нибудь стоит осветить более подробно?
А пока я еще не реализовал 💬 комментарии для сайта, не стесняйтесь писать мне в соц-сетях или на почту!
🔔 Подписывайтесь в соц-сетях, чтобы не пропустить новые статьи и игры, будем разбираться вместе!
Попробуйте создать свой SSG! И делитесь своим личным опытом.