ПРИВЕТ !
Добро пожаловать на первый блог-пост нашего нового сайта! На момент написания мы ведём работу над кучей проектов, и ввиду общей занятости народа, работа идёт медленно, да и посты писать особо некому.
Так давайте расскажем хотя бы про этот сайт!
Вводные данные
Сайт написан на TypeScript с применением фреймворка SvelteKit. Некоторые отзываются о нём, мягко говоря, не лестно, но нас Svelte подкупил тем, что все страницы, и даже ручки API, можно предварительно отрендерить по файлам, получив полностью статичный сайт! А фреймворк Tailwind CSS даёт нам удобные классы и неплохие дефолтные стили с цветовой палитрой. Пока у нас нет полноценных веб-дизайнеров, довольствуемся их встроенной палитрой.
Это решение нам досталось в наследство от сайта Small Fish. Впрочем, как и половина их кодовой базы: для быстрого старта с разрешения команды мы воспользовались их исходниками. Но вы не подумайте, что сайт слизан подчистую! Как минимум в процессе обновления версий фреймворков пришлось провести небольшой рефакторинг, а потом ещё пошли наши прочие хотелки (об этом дальше), и можно смело сказать, что спустя сотню коммитов, из общего остались только фреймворки и сама структура сайта.
В общем-то, главная причина почему мы взяли их сайт за основу, а не написали вручную — это удобная система блогов.
Собственно, блоги
Конечно, в нашей небольшой команде абсолютно все компьютерно грамотные ( вы можете это исправить!), и никому не составит труда настрочить текст вот так, просто HTML-ом. Но как же хочется писать блог в удобном WYSIWYG-редакторе, или хотя бы в простом формате!
На помощь приходит библиотека mdsvex. Она по сути превращает .md файлы в Svelte-компоненты, которые можно потом
встроить куда угодно. Здорово!
Только надо решить парочку проблем. Во-первых, все элементы, вроде ссылок или картинок, переводятся в обычные HTML5-объекты. А у нас есть свои собственные, особые ссылки ( вот такие!). Благо, mdsvex позволяет просто использовать Svelte-компоненты, если правильно настроить авто-импортирование. Но это же не удобно! Это ведь надо знать, что есть такие ссылки, а есть не такие. Плюс, это будет выглядеть уродливо в тех же WYSIWYG-редакторах по типу Obsidian.
Решилось это с помощью системы layout-ов внутри mdsvex: можно задавать шаблоны, которые будут натянуты вокруг преобразованного Markdown-текста. Тут же
можно экспортировать особый объект, где к названию каждого стандартного HTML-элемента можно поставить ссылку на свой собственный класс. Таким образом,
каждый <a href="...">...</a> превращается в <SocialHyperlink href="...">...</SocialHyperlink>, без лишних телодвижений писателя блога.
Отлично, у нас есть отдельные блоги, теперь хочется собрать их все вместе, и желательно рассортировать по дате. И вот тут всплывает вторая проблема: система Git, которой мы пользуемся, просто не хранит дату создания и изменения файла! Надо это всё записать куда-то в метаданные. Самое простое решение: присобачить к каждому Markdown-блогу по JSON-у. Но есть решение ещё лучше, оно называется frontmatter YAML — де-факто стандарт встраивания любых метаданных в файл Markdown.
Здесь мы пишем дату в формате ISO, ну и на сдачу можно добавить кучу других данных. За примером далеко идти не надо: вот вам заголовок этой же заметки:
---
title: 'Наш первый блог'
thumbnail: 'wave.png'
thumbnailAlt: 'Смайлик с махающей рукой'
date: '2025-10-22'
description: 'Немного о том, как мы делали наш сайт'
type: 'article'
authors: 'rndtrash'
--- Тут вам и дата, и описание, и даже красивая миниатюра! И сюда можно вставить вообще всё, что поддерживает YAML, в том числе списки и булевы переменные. Зачем? Ну-у-у… Потом разберёмся🙂
Важно то, что теперь с помощью import.meta.glob('/src/blogs/*.md') можно просто пройтись по всем постам, выдернуть метаданные и отсортировать их по дате.
Эта система нам пригодилась в ещё одной вещи, которую, казалось бы, давно забыли, а зря!
RSS-лента
Да, мы сами в шоке, но даже после становления фактической олигополии интернета Google-Twitter-ЧёЕщёТам, RSS-ленты продолжали использоваться для подкастов. А с недавней волной популярности децентрализации ( Mastodon, BlueSky, NeoCities и прочие), в моду вернулись маленькие бложики. Да, мы знаем, что они никуда и не уходили, можете не писать гневные комментарии. Ах, да, их некуда писать. Ой.
RSS в этом плане нам очень удобен, во-первых, потому, что это просто XML-файл, и его можно просто куда-то положить, а во-вторых, на него могут подписаться не только люди, но и машины. В нашем случае это Discord-бот FeedCord. Ура, можно не городить свой воркфлоу, дёргающий веб-хуки!
Поскольку SvelteKit — это fullstack-фреймворк, на нём же напишем простую ручку rss.xml. Это будет простая функция GET(),
возвращающая текст, ручками слепленный в что-то приблизительно похожее на XML. Но погодите, наш сайт же статичный, как мы будем вызывать эту JS-функцию?
Внимание, фокус! Мы берём обычный файл src/routes/blog/rss.xml/+server.ts, и добавляем в него волшебную строчку:
export const prerender = true; При сборке SvelteKit видит, что страницу можно сгенерировать заранее, выполняет весь этот код (а там то же самое, что в странице со списком блог-постов, но очевидно, в формате XML),
и в папке build/ создаёт файл rss.xml. Та-да!
Конечно, это отвратительный хак, но мне честно не хотелось добавлять этап постобработки, у нас и так там стоит генератор sitemap.xml. Да, ещё одна забытая технология. Надеюсь, хоть так чёртовы ЖиПиТи-боты перестанут вслепую тыкаться по нашим сайтам, особенно по нашему инстансу Forgejo.
Деплой
Сайт Small Fish, упомянутый в самом начале, располагается на GitHub Pages: CI/CD хук собирает сайт, запаковывает его в zip-архив и публикует на своём домене .github.io,
который ещё файлом CNAME можно поменять на абсолютно любой.
Мы же хотим оставаться независимыми, в конце концов, зачем мы платим за серваки? Вот Nginx в контейнере, вот SFTP, закидывай и всё!
Так и было, когда наш сайт был проще, намного проще (умоляю вас, не смотрите в Web-архив!!!), но потом захотелось CD как у больших дядь. На это есть Forgejo Actions, местный аналог GitHub Actions. Пишешь скрипт, пушишь коммит, всё будет сделано за тебя. Так, стоп, а почему не работает? Стоп, надо ещё хостить свой раннер?
Да уж, совсем забыл упомянуть… На нашем основном сервере с доменом teasanctuary.ru всего один гигабайт ОЗУ. Один. Сюда еле влез Forgejo с несколькими сервисами для нашего внутреннего пользования, а тут ещё надо запускать NodeJs, который запросто выжрет остаток и не подавится.
Как быть?..
Помощь пришла откуда не ждали. Я, rndtrash, некоторое время назад переехал на другую квартиру, и пока я разбирал вещи, среди ящичков моему взору предстала ма-а-аленькая чёрная коробочка с логотипом некоторого пчелиного оператора. Как позже выяснилось, это Smart TV-приставка, которую провайдер просто отказался забирать назад, ну а хозяевам квартиры она просто не нужна.
Меня накрыло в пот, и спустя один запрос в поисковике, один тред на 4PDA, один звонок владельцам квартиры и тридцать минут беготни в магазин электроники, я понял, что передо мной лежал халявный сервер на Linux с ARMv8 процессором на 1.5 ГГц с одним гигабайтом оперативки.
ДЖЕКПОТ!!! 🎰🎰🎰
Дальше дело за малым: я просверлил несколько вентиляционных отверстий, чтобы коробка совсем не прокоптилась (вы бы видели местный охлад…), разрегистрировал её в нашем Forgejo, и проработал план действий. Ввиду простоты сайта, наш deploy-скрипт предельно прост:
- Ставим Ноду, клонируем репозиторий, устанавливаем Npm-зависимости
- Кешируем зависимости, потому что это всё непотребство работает от моего Интернета
- Собираем сайт командой
npm run buildи формируем артефакт — архив с сайтом - Достаём SSH-ключ к основному серверу из секретного хранилища и регистрируем в SSH-агенте. Этот ключ имеет доступ только к особому пользователю, который в свою очередь имеет доступ только к особой папке только для сайта
- С помощью
rsync, полностью заменяем содержимое удалённой папки на содержимое артефакта, сделанного на этапе 3. Это сделано на случай, если в репозитории случайно оказалось что-то непубличное👻
В этом плане есть только один прокол, причём очень важный.
SSL
Тот самый заветный зелёный замочек у нас был с самого начала, но в какой-то момент он пропал. Покопавшись в логах Certbot, мы с ужасом обнаружили, что с переездом всего ПО в Podman-контейнер, он вот уже месяц как не мог запустить свой собственный сервер для сертификации.
Вообще, процесс получения сертификата Let’s Encrypt происходит вот так: некий бот раз в два-три месяца запускает свой собственный сервер на порту 80,
и с его помощью раздаёт особый файл-ключ по адресу домен/.well-known/acme-challenge/случайные-буквы. Главный сервер Let’s Encrypt стучится по этому адресу, тем самым удостоверяется,
что к нему обратился действительно владелец домена, а не случайный прохожий, и наконец, регистрирует обновлённый файл сертификата.
У нас опять всё не как у людей, поэтому мы сделали вот как: Certbot имеет особый режим, где он просто кладёт нужный файл, а дальше системный администратор как-то сам решает, каким
образом поднять 80 порт. Мы не можем закинуть этот файл прямо в папку сайта, потому что наш deploy-скрипт полностью уничтожает постороннее содержимое, поэтому мы сделали отдельную
папку, а Nginx в свою очередь сконфигурировали так, что все обращения по пути /.well-known/acme-challenge идут мимо папки сайта.
Финал
Вот столько трудностей нам пришлось преодолеть, чтобы довести сайт хотя бы до такого состояния. Если что, на момент написания работает только главная страница, и собственно блоги. Остаётся надеяться, что написание остальных страниц не составит особых усилий.
Если вам понравился наш блог, то подписывайтесь на наш RSS-фид, и обязательно загляните в наше сообщество в Discord.
Ещё увидимся!
- Команда Tea Sanctuary, 2025