Я сделал себе сайт на др...
Решил я запилить себе сайтик на день рождения. В этом посте я немного расскажу о его создании, почему он сделан именно на Astro.
Итак, сайт (включая API) сделан на Astro, в качестве CMS используется directus, т.к. достаточно прост в использовании и не требует много ресурсов. Для пуш-уведомлений используется Firebase Cloud Messaging.
Всё это поднято в Docker Compose на виртуальной машине. Ну и естественно для защиты и кэширования используется Cloudflare.
Мой опыт с Remix, или почему именно Astro?
Изначально планировалось написать этот сайт на Remix. Зачем? SSR. Хотелось поэкспериментировать с SEO, к тому же чем меньше JS кода на клиенте, тем лучше.
Так вот Remix, как и NextJS - отличные решения, но они всё ещё слишком привязаны к реакту. И ещё одна большая проблема - в итоге весь Javascript бандл всё равно отправляется на клиент. А теперь добавим сюда мой способ перевода (см. [[#i18n]]). Да, в Remix можно отключить клиентский код, но тогда особого смысла от всей тесной интеграции с реактом нет. Ещё концепция loader и action, которая приводит к дублированию функционала в файлах API-эндпоинтов и страниц.
Ещё одна вещь, которая мне не нравится в Remix на данный момент - их переход на flat routes. С моей точки зрения, директории лучше всего подходят для иерархически организованных URL, а они почти всегда так и организованы. Да, есть плагин, который возвращает старую систему с дополнительными фичами, но я запутался в его документации.
Astro - фремйворконезависимый, с учётом слабой интерактивности моего сайта, реакт (или что-то другое) не нужен. Достаточно ванильного JS с несколькими библиотеками (напр. Photoswipe).
Плюсы Astro:
- простота, он как раз отлично подходит, когда не нужно делать очередной интернет магазин
- поддержка markdown в качестве файлов страниц. Полезно для сайтов с документации или статичных блогов. Я всё же решил использовать CMS, дабы ознакомиться с ними.
- Как и у других подобных фреймфорков, frontend и backend можно делать в одном проекте
- Можно писать на ванильном JS. Не тащите целый реакт, когда вам нужна одна интерактивная кнопочка
- Автоматический бандлинг и фигнерпринтинг скриптов (если не установлен атрибут
is:inline
). Т.е. можно делать так:
<script>
import Panzoom from '@panzoom/panzoom';
const elem = document.getElementById('image')!;
const panzoom = Panzoom(elem, {
});
elem.parentElement!.addEventListener('wheel', panzoom.zoomWithWheel);
</script>
У ремикса, правда, был полезный функционал для работы с meta тэгами и т.п.
Как обезопасить администраторские ресурсы
Можно, конечно, просто поставить сложные пароли и настроить fail2ban, этого должно быть достаточно. Но с другой стороны, не хотелось бы, чтобы ненужный трафик вообще шёл на сервер.
Одним из самых простых и безопасных вариантов - VPN или SSH туннель. Однако, есть ещё один вариант - клиентские сертификаты TLS. Cloudlfare на бесплатном плане дает настроить аутентификацию по таким сертификатам. С учетом того, что также можно настроить Authenticated Origin Pull (т.е. mTLS между конечным сервером и Cloudlflare), это позволяет добиться того, что левый трафик просто не пройдет дальше Cloudflare. И даже если кто-то подключиться к бэкенду напрямую по IP, его встретит ошибка из-за включенного mTLS. Ну и правила WAF можно и нужно добавить.
У такого подхода есть и ещё один приятный бонус - можно использовать заголовки для авторизации. Например, хранилище на files.captaindno.xyz
. Когда клиентский сертификат предоставлен, оно открывается с доступом на запись, а когда нет - только для чтения и только в директории public (ну и share-ы тоже). Разумеется, важна осторожность при настройке кэширования, когда речь идёт о ресурсах с ограниченным доступом.
i18n
Так как поддерживается только два языка (русский и английский), заморачиваться с различными i18n библиотеками не хотелось. К тому же, как правило, интернационализация подразумевает вынос всего текста в отдельные файлы - в документ мы подставляем строки под ключу. Это не очень удобно. Поэтому, для перевода просто используются компоненты вот такого вида:
---
const lang = Astro.params.lang!;
---
{lang === 'en' && <slot/>}
Затем они используются вот так:
<EN>Go to settings</EN>
<RU>Перейти к настройкам</RU>
Да, такой подход умножает количество текста в файле в два раза, но это не так критично для такого небольшого сайта с одним человеком, который поддерживает код.
Удобно добавлять картинки в статьи
Вообще, изображения для блога не обязательно должны храниться в CMS, но так иногда удобнее. Но, как уже говорилось выше, админские ресурсы защищены mTLS, и CMS в том числе. К тому же она под другим субдоменом.
Дело в том, что некоторые изображения не показываются в галерее, но доступны. Некоторые вообще недоступны с основного сайта. Но когда пишешь статью, хочется видеть картиночку. Простым добавлением авторизации для запросов на картинки это не решить - тогда невозможно будет кэшировать изображения на CDN, что крайне печально. Кроме того, для photoswipe необходимы атрибуты width и height, id для получения подписи и т.п.. Вручную их прописывать неудобно. И про альтернативный текст не забываем.
Поэтому в directus был добавлен flow, который для каждой картинки автоматически генерирует поле такого вида:
<!--[img]image_uid-->
<img src="https://directus.captaindno.xyz/assets/image_uid"/>
Его можно просто скопировать куда угодно, если есть доступ к directus, оно будет грузиться при редактировании.
Затем, при рендеринге странички мы автоматически редактируем тэги изображений. Всё происходит прямо на сервере, как и рендеринг md в html.
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i];
const m = line.match(/<!--\[img\](.*)-->/);
if (m) {
const imageId = m[1]!;
const imgTag = parse(lines[i + 1]!)!.querySelector('img')!;
imgTag.setAttribute('src', `/content/images/${imageId}?variant=full`);
imgTag.setAttribute('data-id', imageId);
const imgDetails = await getImageDetailsWithLang(imageId, lang);
if (!imgDetails) continue;
imgTag.setAttribute('data-width', imgDetails.width);
imgTag.setAttribute('data-height', imgDetails.height);
// Add alt text
if (!imgTag.hasAttribute('alt')) imgTag.setAttribute('alt', imgDetails.description);
// Modify actual line
lines[i + 1] = imgTag.outerHTML;
}
}
Заключение
В целом, этот сайт спокойно можно бы было сделать и без CMS, используя коллекции контента Astro. Но опыт работы с CMS всегда полезен, так что всё сделано не зря.
Кажется (но это не точно), вам нравится мой контент. Перейдите в раздел настроек, чтобы подписаться на пуш-уведомления или разрешить сбор аналитических данных, которые позволяют улучшать этот сайт и его содержимое.
Перейти к настройкам