Назад к блогу

Как я сделал этот сайт и почему выбрал именно такой стек

10 мин чтения технологии личное

Я не фронтенд-разработчик. Я — Head of Data, и мой обычный стек — это Python, SQL, Airflow, dbt и BigQuery. У меня есть немного опыта в SEO, но он скорее теоретический — я понимаю принципы, много читал про техническое SEO, но никогда не занимался этим профессионально. Тем не менее, когда я решил сделать персональный сайт, я не хотел брать WordPress или Wix, нажать три кнопки и получить шаблонную страницу, которая выглядит как миллион других. Я хотел полный контроль — над кодом, над производительностью, над тем, как моё имя появляется в Google. И сделал всё с нуля.

Эта статья — не туториал и не гайд. Это рассказ о моём процессе: какие решения я принимал, почему именно такие, и что из этого вышло.

Почему не WordPress?

Давайте начнём с самого очевидного вопроса. WordPress — это 43% веба. Он бесплатный, есть миллион тем, плагинов, хостингов на любой бюджет. Почему не он?

Потому что WordPress — это динамический сайт. Каждый запрос — это PHP-скрипт, который соединяется с MySQL, собирает страницу на лету и отдаёт её. Для блога на три статьи это как стрелять из пушки по воробьям. Плюс — уязвимости: WordPress требует постоянных обновлений, патчей, защиты от ботов. Для сайта-визитки это лишняя головная боль.

Мне нужен был статический сайт — набор HTML-файлов, которые nginx отдаёт мгновенно, без базы данных, без серверной логики, без PHP. Просто файлы. Максимально быстро, максимально просто, максимально безопасно.

Выбор генератора: почему Eleventy

Когда ты выбираешь Static Site Generator (SSG), выбор огромный: Hugo, Jekyll, Gatsby, Next.js, Astro, Eleventy... Я перебрал несколько вариантов.

Hugo — написан на Go, молниеносно быстрый, но шаблоны на Go templates — это отдельный язык, который нужно учить с нуля. Документация есть, но для нетривиальных вещей начинаешь гуглить часами. Да, AI-ассистенты сейчас решают эту проблему — можно попросить написать любой шаблон. Но мне хочется хотя бы базово понимать, что происходит в коде. Потому что AI из глупого делает ещё глупее, а из умного — продуктивнее на 50–60%.

Next.js / Gatsby — React-based. Для персонального сайта с тремя страницами тащить за собой React, Webpack (или Vite), SSR/SSG конфигурацию и 200 МБ node_modules — это overkill. Я не строю SaaS-продукт, мне нужен блог.

Astro — интересный вариант, но для моего случая — слишком много «магии» под капотом. Острова, компонентная модель, собственный формат .astro — это круто для сложных проектов, но для персонального сайта с блогом мне хотелось чего-то проще и прозрачнее.

Eleventy (11ty) победил по нескольким причинам:

  1. Простота. Eleventy не навязывает фреймворк. Никакого React, Vue, Svelte — только шаблоны и контент. Ты пишешь Markdown, выбираешь шаблонизатор (Nunjucks, Liquid, Handlebars), и получаешь HTML. Всё
  2. Гибкость. Ты контролируешь каждый байт вывода. Нет «магических» страниц или роутинга — есть файлы и папки, которые становятся URL-ами. Интуитивно понятно
  3. Zero client JS по умолчанию. Eleventy не вставляет никакого JavaScript в готовые страницы. Весь JS — только то, что ты сам добавишь. Для скорости это критично
  4. ESM поддержка. Eleventy 3.x полностью поддерживает ES Modules — никаких require/module.exports, чистый современный JavaScript
  5. Data cascade. Система каскадных данных — когда JSON-файл в папке автоматически применяется ко всем файлам в ней — идеально подошла для мультиязычности

Мультиязычность: три языка без плагинов

Одно из главных требований — сайт должен работать на нескольких языках: украинский (основной) и английский (для международной аудитории).

Я не использую ни одного плагина для i18n. Архитектура банально проста:

  • Украинские страницы лежат в корне src/ (URL: /)
  • Английские — в src/en/ (URL: /en/)

Каждая языковая папка имеет свой JSON-файл (например, en.json), который через data cascade добавляет переменную lang ко всем файлам в ней. Шаблоны используют `` для UI-переводов, а контент страниц просто пишется вручную на каждом языке.

Hreflang теги генерируются автоматически через eleventyComputed.js — каждая страница знает свои альтернативы. Sitemap включает все три языковые версии с правильными hreflang-аннотациями. Всё это — без единого внешнего плагина.

CSS: Tailwind 4.x и CSS-first подход

Для стилей я выбрал Tailwind CSS. Не потому что это модно, а потому что utility-first подход идеально ложится на шаблоны — ты видишь все стили прямо в HTML, без прыжков между файлами.

Tailwind 4.x изменил правила игры — конфигурация теперь живёт прямо в CSS-файле через @theme {}, без отдельного tailwind.config.js. Плагины подключаются через @plugin, а пути к шаблонам — через @source. Всё в одном файле main.css.

Кастомные компоненты (карточки с glass-эффектом, кнопки, градиентный текст) определены через @utility — это позволяет писать .glass-card или .btn-primary как обычные классы, но со всеми преимуществами tree-shaking от Tailwind.

Шрифты — Inter для текста, JetBrains Mono для кода. Оба — self-hosted как variable fonts в WOFF2, загруженные с font-display: swap, чтобы текст показывался сразу, а шрифт подгружался в фоне. Никаких запросов к Google Fonts CDN — меньше соединений, быстрее загрузка.

Сборка CSS — через @tailwindcss/cli, без PostCSS. At build time Tailwind трясёт дерево и оставляет только те классы, которые реально используются в шаблонах. Итоговый CSS — менее 15 КБ после минификации.

JavaScript: меньше — лучше

Никакого React, Vue, jQuery или какого-либо фреймворка. Весь JavaScript — один файл main.js, подключённый с defer.

Что он делает:

  • Scroll animationsIntersectionObserver добавляет класс .reveal к элементам, когда они попадают во вьюпорт. Плавные появления без библиотек
  • Mobile menu — открытие/закрытие бургер-меню
  • Language switcher — переключение языка через простые <a> ссылки (каждая языковая версия — отдельный URL, это canonical для SEO)
  • Engagement widgets — просмотры, лайки, рейтинг, комментарии — fetch() к API

Весь JS минифицируется при сборке. Конечный размер — несколько килобайт.

Backend: Express + SQLite для engagement

Единственная динамическая часть сайта — это «engagement»: просмотры статей, лайки, 5-звёздочный рейтинг и комментарии. Для этого нужен API.

Я написал его на Express + better-sqlite3 — минималистичный сервер, который:

  • Хранит данные в одном SQLite-файле (без MySQL, без PostgreSQL, без облачных баз)
  • Использует WAL mode для быстрых чтений/записей
  • Имеет prepared statements для всех запросов — никакого SQL injection, минимальный overhead
  • Включает rate limiting (in-memory) против спама
  • Имеет систему модерации комментариев (pending → approved)

Весь API — один файл server.js на ~450 строк. Работает как systemd сервис на сервере, nginx проксирует /api/ к нему. Почему SQLite, а не PostgreSQL? Потому что для блога с десятком статей мне не нужен отдельный database server. SQLite — это один файл на диске. Бэкап — одна команда cp. Всё.

Хостинг: Oracle VM + nginx + Cloudflare

Я не использую Vercel, Netlify или GitHub Pages. Сайт крутится на Oracle Cloud VM (Always Free tier — ARM64, 1/8 ядра OCPU, 1 ГБ RAM, 480 Мбит/с сеть). Абсолютный минимум, но для статического сайта — более чем достаточно. Бесплатно и с полным контролем над сервером.

Nginx — как веб-сервер, настроенный на максимальную производительность:

  • sendfile on, tcp_nopush, tcp_nodelay — минимизация системных вызовов
  • open_file_cache — кеширование файловых дескрипторов, чтобы nginx не ходил на диск при каждом запросе
  • ssl_buffer_size 4k — меньший TLS-рекорд для более быстрого TTFB
  • brotli_static on + gzip_static on — nginx отдаёт заранее сжатые .br и .gz файлы без затрат CPU в рантайме
  • Отдельные правила кеширования: статические ассеты — immutable, max-age=31536000 (1 год), HTML — короткий TTL чтобы обновления доходили до пользователей быстро

Cloudflare стоит перед nginx как CDN:

  • HTTP/3 (QUIC) + 0-RTT — минимальная задержка
  • Edge caching на 30 дней для HTML (с purge на каждом деплое)
  • Early Hints (103) — браузер начинает подгружать CSS/JS ещё до получения HTML
  • DDoS-защита из коробки
  • SSL через Origin Certificate

Деплой: git push → production за 2 минуты

Весь деплой автоматизирован через GitHub Actions. Один git push в main запускает пайплайн:

  1. Build & Test — npm ci → build CSS → build Eleventy → compress assets → lint HTML
  2. Deploy — rsync готовой _site/ папки на сервер через SSH
  3. Reload nginxsudo nginx -t && sudo systemctl reload nginx
  4. Purge & Warm Cloudflare cache — purge_everything → wait → curl по всем URL из sitemap.xml

От пуша до обновлённого сайта — менее 2 минут. Никакого ручного вмешательства.

Скрипт компрессии (compress.mjs) проходит по всем файлам в _site/ и генерирует .br и .gz версии. Это означает, что nginx не тратит CPU на сжатие в рантайме — просто отдаёт готовые файлы. Результат: ~80% экономии на размере, нулевая нагрузка на сервер.

Сжатие и оптимизация

Я одержим скоростью (профессиональная деформация — когда работаешь с данными, миллисекунды имеют значение). Вот что я сделал:

Pre-compression. Скрипт compress.mjs генерирует Brotli (.br) и Gzip (.gz) файлы для всех HTML, CSS, JS, JSON, XML, SVG и TXT. Nginx отдаёт их через brotli_static и gzip_static — без каких-либо затрат CPU.

HTML minification. Eleventy минифицирует HTML при сборке через html-minifier-terser — убирает комментарии, лишние пробелы, минифицирует инлайн CSS и JS.

Image optimization. Eleventy Image shortcode генерирует AVIF + WebP + JPEG в нескольких размерах (640, 960, 1280px) с loading="lazy" и decoding="async". Браузер выбирает оптимальный формат и размер автоматически.

Service Worker. Кеширует критические ассеты при первом посещении. Повторные визиты — мгновенные. Офлайн-страница показывается, когда нет интернета.

Self-hosted шрифты. Inter и JetBrains Mono как WOFF2 variable fonts — один файл на шрифт вместо десятка отдельных. Без внешних HTTP-запросов.

Результат: TTFB ~30мс на Cloudflare edge, LCP < 1с, общий размер страницы — менее 50 КБ в gzipped. PageSpeed Insights показывает 99–100 для мобильных и десктопа.

Тестирование: 102 юнит-теста + E2E

Для персонального сайта это может показаться overkill, но тесты дают мне уверенность, что изменения ничего не ломают. А в эру AI не писать тесты — это просто хамство и неуважение даже к самому себе. AI генерирует тесты за секунды, тебе остаётся только запустить. Нет никакого оправдания.

Vitest (юнит-тесты):

  • Валидация HTML — каждая сгенерированная страница проверяется на корректность
  • i18n — все ключи переводов существуют для всех трёх языков
  • Schema.org — JSON-LD разметка валидна и содержит правильные данные
  • TTFB — реальные запросы к klimnyk.dev проверяют, что сервер отвечает < 200мс

Playwright (E2E):

  • Навигация работает правильно на всех языковых версиях
  • Мобильное меню открывается и закрывается
  • Языковой переключатель ведёт на правильные страницы
  • Мета-теги (title, description, og:*) присутствуют

Всего 102 юнит-теста и 62 E2E-теста. Запускаются в CI перед каждым деплоем.

SEO: не просто мета-теги

Как Head of Data я знаю цену органического трафика. SEO здесь — не afterthought, а одна из основных причин существования сайта.

  • Schema.org — 7 типов JSON-LD разметки: WebSite, Person, BreadcrumbList, WebPage, BlogPosting, Blog, ProfilePage
  • Hreflang — правильные языковые альтернативы + x-default
  • Sitemap.xml — автоматически генерируется со всеми URL и hreflang
  • robots.txt — правильно настроен, не блокирует CSS/JS
  • llms.txt — файл для LLM-краулеров по стандарту llmstxt.org, обновляется автоматически
  • GEO meta tags — citation_*, article:published_time, max-snippet
  • Canonical URLs — каждая языковая версия — отдельная страница с canonical на себя

Почему не проще?

С AI я потратил один вечер на базовую структуру сайта и пару часов в неделю на написание статей и правки. Я мог бы сделать сайт на Notion, Tilda или WordPress за один вечер. Но тогда бы у меня не было:

  • TTFB в 30мс
  • Полного контроля над разметкой и SEO
  • Автоматического деплоя из GitHub
  • Нулевого счёта за хостинг
  • Уверенности, что сайт выдержит любую нагрузку (это же статика!)
  • Удовольствия от того, что сделал всё сам

Это персональный проект, и в нём есть кайф от инженерии. Да, я Head of Data, а не фронтенд-разработчик. Но технологии — это технологии, и мне нравится разбираться, как вещи работают под капотом.

Что дальше

Сайт живёт, деплоится автоматически, проходит тесты. Контент появляется когда есть что сказать. Я не ставлю себе цель публиковать каждую неделю — это личный блог, а не медиа-издание. Но каждая статья делается с таким же вниманием к качеству, как и сам сайт.

Код живёт на GitHub в приватном репозитории. Никаких секретов, никакой магии — просто хорошо продуманный стек, который делает свою работу.

(0)

Комментарии (0)