Кейс: разработка сайта агентства Digital Craft Tbilisi
Мы подошли к разработке сайта digital-craft-tbilisi.site: как к продукту, в котором каждое техническое решение обосновано, каждый байт кода — оправдан, а итоговая архитектура — масштабируема без роста затрат.
В этом кейсе мы разбираем систему изнутри: от концепции и выбора стека до конкретных алгоритмов, которые работают в продакшне прямо сейчас.
Почему отказались от стандартных CMS
Первый вопрос, который мы задали себе на старте: зачем нам вообще что-то писать самостоятельно? WordPress существует двадцать лет, Tilda решает задачу за несколько часов, Webflow даёт визуальный контроль над вёрсткой. Аргументы против самописного решения очевидны.
Аргументы за оказались сильнее.
Проблема первая: скорость и избыточный код
WordPress при стандартной установке генерирует страницу динамически при каждом запросе: запрос к базе данных, выполнение PHP, сборка HTML, отправка клиенту. Даже с агрессивным кешированием Time To First Byte редко опускается ниже 200–400 мс. Tilda и аналогичные конструкторы генерируют статику, но вместе с ней — десятки килобайт неиспользуемого CSS и JS, встроенные счётчики, обращения к внешним сервисам.
Для агентства, которое продаёт техническую экспертизу, иметь сайт с посредственными Core Web Vitals — противоречие в определении.
Проблема вторая: мультиязычность без плагинов
Мы работаем на четырёх рынках: грузиноязычная аудитория в Грузии, русскоязычная диаспора, украинский рынок, англоязычные клиенты. Каждый сегмент требует не просто переведённого текста, но технически корректной SEO-разметки: правильных тегов hreflang, региональных вариантов (ru-GE, ka-GE), корректного x-default и связанных записей в sitemap.xml.
Плагины типа WPML решают эту задачу частично и за деньги. Нам нужна была система, где мультиязычность — не надстройка, а часть архитектуры с первого дня.
Проблема третья: ограниченное SEO и зависимость от платформы
Любая SaaS-платформа накладывает ограничения: структуру URL, способ генерации метатегов, формат sitemap. Эти ограничения некритичны для большинства проектов, но неприемлемы для агентства, чья компетенция — именно техническое SEO.
Цели, которые мы поставили
- TTFB как можно ближе к нулю — страница должна отдаваться как статический файл с CDN.
- Полный контроль над SEO-разметкой без ограничений платформы.
- Нативная мультиязычность на уровне архитектуры, а не плагинов.
- Независимость от платных CMS и подписок.
- Готовность к AI-поиску — новой реальности, в которой сайты индексируются не только поисковыми ботами, но и языковыми моделями.
Технический стек и концепция архитектуры
Архитектура сайта строится на трёх компонентах, каждый из которых закрывает строго свою зону ответственности.
Firebase как Headless CMS
Firebase Firestore выступает в роли базы данных и источника правды для всего контента. Редактор работает с данными через кастомную админ-панель, которая пишет напрямую в Firestore. При этом Firebase не участвует в раздаче контента конечному пользователю — он существует только на этапе сборки и управления. Это классическая Headless-архитектура: бэкенд отделён от фронтенда полностью.
Python как статический генератор
Python-скрипт generate_site.py — это компилятор сайта. Он выкачивает все данные из Firestore, прогоняет их через шаблонизатор Jinja2 и записывает на диск готовые HTML-файлы. После завершения сборки Python больше не нужен: сайт существует как набор статических файлов.
Vanilla JS как SPA-движок
На стороне клиента работает JavaScript-движок на чистом Vanilla JS — без React, Vue или Angular. Он обеспечивает бесшовную навигацию между страницами через History API, управляет анимациями через Intersection Observer и инициализирует интерактивные компоненты после каждого перехода.
Концепция: сборка на лету, раздача статикой
Ключевой принцип архитектуры: вся вычислительная работа происходит один раз — во время сборки. Когда контент-менеджер нажимает кнопку деплоя, Python-скрипт за несколько секунд генерирует полный сайт: все страницы на всех языках, sitemap, llms-full.txt, 404-страницу. Результат — папка со статическими файлами, которую можно раздавать через любой CDN с нулевой серверной нагрузкой.
Сравнение с WordPress: при 1000 одновременных посетителей WordPress генерирует 1000 запросов к PHP и базе данных. Наша архитектура при тех же 1000 посетителях генерирует 0 серверных вычислений — CDN раздаёт файл из кеша.
Ключевые решения: разбор по слоям
Гибридная архитектура: generate_site.py и Jinja2
Скрипт сборки запускается в несколько последовательных этапов.
Первый этап — инициализация. Скрипт подключается к Firebase через сервисный аккаунт, который передаётся как переменная окружения FIREBASE_SERVICE_ACCOUNT. Ключи не хранятся в репозитории — это обязательное условие безопасности при работе с CI/CD пайплайнами.
Второй этап — загрузка данных. Скрипт обходит все коллекции Firestore: home, services, portfolio, blog, contact, carouselItems. Данные загружаются единым блоком в начале работы, чтобы минимизировать количество запросов к API.
Третий этап — рендеринг. Для каждого документа в каждой коллекции скрипт определяет соответствующий Jinja2-шаблон и рендерит страницу с подстановкой данных. Результат записывается в директорию public/ по пути, соответствующему будущему URL страницы.
Четвёртый этап — генерация служебных файлов: sitemap.xml, llms-full.txt.
Интеллектуальный парсинг: маркер [TOC]
Одна из нетривиальных задач — автоматическая генерация оглавления для длинных статей. Редактор не верстает оглавление вручную: он просто добавляет строку [TOC] в начало текста.
Скрипт обнаруживает этот маркер и запускает парсинг через библиотеку lxml. Алгоритм следующий: HTML-содержимое статьи парсится в дерево элементов, скрипт обходит дерево и собирает все теги h2 и h3, каждому заголовку присваивается id-атрибут на основе его текстового содержимого (транслитерированного и очищенного), затем из собранных заголовков строится вложенный список ссылок, который подставляется вместо маркера.
Важная деталь: id-атрибуты генерируются через ту же функцию slugify(), что и URL страниц, — это гарантирует отсутствие конфликтов и корректную работу якорных ссылок для грузинских и русских заголовков.
Интеллектуальный парсинг: маркер [CAROUSEL:key]
Маркер [CAROUSEL:key] позволяет встраивать интерактивную карусель в произвольное место статьи. Ключ соответствует идентификатору коллекции слайдов в Firestore.
При обработке скрипт находит маркер в теле статьи, загружает соответствующую коллекцию карусели из предварительно полученных данных, рендерит HTML-разметку карусели через отдельный шаблон и подставляет её вместо маркера. Итоговый HTML содержит полностью готовый компонент — JavaScript-движок карусели инициализирует его при загрузке страницы.
Кастомная транслитерация: логика slugify()
Функция slugify() реализована параллельно в двух местах: в Python-скрипте для генерации URL при сборке и в Vanilla JS для клиентского роутинга. Оба экземпляра работают по идентичному алгоритму, что гарантирует консистентность URL на сервере и в браузере.
Алгоритм строится на двойной детекции алфавита:
- Скрипт проверяет, содержит ли строка символы в диапазоне Unicode
U+10D0–U+10FF(грузинский алфавит Мхедрули). Если да — применяется кастомная таблица транслитерации: каждый грузинский символ заменяется латинским эквивалентом согласно стандартам национальной романизации. Например:შ → sh,ჩ → ch,ხ → kh,ჟ → zh,ჯ → j. - Затем скрипт проверяет наличие кириллицы в диапазоне
U+0400–U+04FF. Кириллица транслитерируется через библиотекуtransliterate(Python) или через встроенную таблицу (JS), включая украинские символы:ї → yi,є → ye,ґ → g. - После транслитерации все символы кроме
a-z,0-9и дефисов удаляются. Множественные дефисы схлопываются в один, ведущие и хвостовые дефисы обрезаются.
Результат: URL вида /ka/blog/saiti-mcire-biznesisat/ и /ru/blog/tekhnicheskoe-seo-dlya-biznesa/ вместо процент-кодированных строк, которые ломают аналитику и создают проблемы при копировании ссылок.
SEO-инженерия и AI-ready подход
Логика translationGroupKey
Мультиязычность реализована не как переключатель интерфейса, а как связь на уровне базы данных. Каждая страница в Firestore имеет поле translationGroupKey — произвольный идентификатор, который объединяет переводы одного материала. Например, статья о техническом SEO на четырёх языках будет иметь одинаковое значение этого поля: technical-seo-guide.
При сборке скрипт строит карту переводов: для каждого уникального translationGroupKey собирается список всех страниц с таким ключом. Затем при генерации каждой страницы этот список используется для двух целей: вставки тегов hreflang в head и добавления записей об альтернативных URL в sitemap.xml.
Автоматическая генерация hreflang
Для каждой страницы, у которой есть переводы, скрипт генерирует полный набор тегов link rel="alternate". Региональный код (GE, RU, UA) задаётся в поле region в Firestore и автоматически добавляется в значение атрибута hreflang.
Тег x-default указывает Google, какую версию показывать пользователям без явного языкового предпочтения. Скрипт определяет x-default по флагу isXDefault в записи Firestore. Если флаг не выставлен ни у одной из версий, скрипт автоматически назначает x-default на английскую версию страницы.
Итоговый блок hreflang для статьи блога выглядит так:
<link rel="alternate" hreflang="ru-GE"
href="https://digital-craft-tbilisi.site/ru/services/razrabotka-saitov-tbilisi/" />
<link rel="alternate" hreflang="en-GE"
href="https://digital-craft-tbilisi.site/en/services/custom-web-development-tbilisi/" />
<link rel="alternate" hreflang="ka"
href="https://digital-craft-tbilisi.site/ka/services/veb-gverdebis-damzadeba-tbilisi/" />
<link rel="alternate" hreflang="uk"
href="https://digital-craft-tbilisi.site/uk/services/rozrobka-saitiv-tbilisi/" />
<link rel="alternate" hreflang="x-default"
href="https://digital-craft-tbilisi.site/en/services/custom-web-development-tbilisi/" />Весь этот блок генерируется автоматически. Редактор заполняет одно поле — translationGroupKey — у связанных страниц. Всё остальное система делает при следующей сборке.
Динамический Sitemap с lastModified
Файл sitemap.xml пересобирается при каждом деплое. Скрипт берёт поле lastModified из каждого документа Firestore и подставляет его в тег . Это означает, что поисковый бот всегда видит корректную дату последнего изменения — не дату последнего деплоя, а дату реального обновления конкретной страницы. Приоритет (priority) и частота изменений (changefreq) настраиваются индивидуально для каждой страницы через поля в Firestore.
AI-ready инфраструктура: llms.txt и llms-full.txt
Поисковый ландшафт меняется. Всё большая часть информационных запросов обрабатывается языковыми моделями — ChatGPT, Claude, Perplexity, — которые формируют ответ на основе собственных знаний и доступных им данных. Для попадания в эти ответы классического SEO недостаточно: нужно дать AI-агентам структурированное описание сайта в формате, который они умеют читать.
Для этого существует формирующийся стандарт llms.txt или llms-full.txt. Мы реализовали его поддержку.
llms-full.txt — строгий машиночитаемый формат со всей структурой сайта. Страницы сгруппированы по translationGroupKey: агент видит не разрозненные URL, а логически связанные группы материалов с указанием языков и регионов. Языковая модель, обращающаяся к этому файлу, получает достаточное представление о том, чем занимается агентство.
Файл генерируется скриптом при каждой сборке — актуальная структура сайта всегда доступна AI-агентам без ручного обновления.
Кастомная CMS: Firebase Admin Panel
Управление контентом реализовано через кастомную Single Page Application, построенную на Vanilla JS с Firebase в качестве бэкенда. Это полноценная CMS без сторонних платформ и ежемесячных подписок.
Панель покрывает весь жизненный цикл контента:
- Создание и редактирование страниц во всех четырёх языковых версиях.
- Управление мультиязычными связями через
translationGroupKey. - Настройка SEO-полей:
seoTitle,metaDescription, OG-теги,isXDefault, регион, приоритет в sitemap. - Загрузка медиафайлов в Firebase Storage с автоматическим получением публичных URL.
- Управление слайдами карусели: порядок, изображения, заголовки, описания.
- Вставка произвольного HTML/CSS/JS для анимированного фона каждой страницы.
Функция Autofill from Article
Создание слайда карусели — типичная задача для контент-менеджера после публикации новой статьи. Без автоматизации это выглядит так: открыть статью, скопировать заголовок, вернуться в карусель, вставить заголовок, снова открыть статью, скопировать описание, снова вернуться, вставить описание, скопировать URL изображения обложки, вернуться, вставить изображение.
Функция Autofill from Article заменяет этот процесс одним действием. Редактор выбирает нужную статью из выпадающего списка — панель обращается к Firestore, получает документ статьи и автоматически подставляет заголовок, описание и обложку в поля нового слайда. Результат проверяется и сохраняется одним кликом.
Это пример принципа, которому мы следуем при разработке инструментов для клиентов: автоматизировать нужно не только технические процессы, но и редакционные.
Frontend Engine: SPA без фреймворков
Сайт ведёт себя как Single Page Application — переходы между страницами мгновенны, состояние интерфейса сохраняется, анимации не прерываются. При этом в сборке нет ни React, ни Vue, ни Angular. Весь клиентский движок написан на чистом Vanilla JS.
Это осознанное архитектурное решение, а не экономия на разработке. Любой JS-фреймворк — это дополнительные килобайты бандла, runtime-оверхед при гидрации, абстракции поверх браузерных API и зависимость от экосистемы, которая обновляется быстрее, чем живут проекты. Для сайта, где каждая миллисекунда Time To Interactive имеет значение, нативный браузерный JavaScript — не компромисс, а правильный инструмент. Браузер уже умеет всё необходимое: History API, Intersection Observer, fetch, CustomEvent. Нужно просто использовать эти механизмы напрямую.
Client-Side Routing через History API
Переходы между страницами реализованы без перезагрузки браузера. Клиентский роутер перехватывает клики по внутренним ссылкам через делегирование событий на уровне document — единственный обработчик вместо отдельного листенера на каждую ссылку. Далее выполняется следующая последовательность:
- Вызов
history.pushState()немедленно обновляет URL в адресной строке без какой-либо перезагрузки страницы. - Целевой HTML-файл загружается через
fetch()— запрос уходит к CDN, который отдаёт готовый статический файл. - Из загруженного HTML через
DOMParserизвлекается содержимое тега
и подменяется в DOM текущей страницы.<main> - Заново инициализируются наблюдатели анимаций, обработчики событий и все интерактивные компоненты новой страницы.
- Обработчик
popstateперехватывает браузерные события навигации и воспроизводит ту же логику при нажатии кнопок «Назад» и «Вперёд».
Принципиально важная деталь, которую легко упустить: несмотря на SPA-поведение в браузере, каждая страница остаётся полноценным статическим HTML-документом на диске. Прямой переход по URL, ссылка из мессенджера, индексация поисковым ботом — всё работает корректно и без серверного роутинга, потому что файл физически существует по нужному пути. Это принципиальное отличие от классических SPA на React Router или Vue Router, которые требуют серверной конфигурации для обработки «несуществующих» маршрутов.
Intersection Observer: ленивая загрузка не только картинок
Стандартная практика — использовать Intersection Observer API для ленивой загрузки изображений. Мы применяем его значительно шире: через этот API контролируется инициализация всех тяжёлых компонентов страницы.
Тяжёлые фоновые блоки не начинают работу до тех пор, пока не попадают в область видимости пользователя. Это означает, что при загрузке страницы браузер не тратит ресурсы на вычисление анимаций, которые находятся в пяти экранах ниже. Эффект прямой: снижается Total Blocking Time, улучшается Largest Contentful Paint, на мобильных устройствах страница становится отзывчивой значительно быстрее.
В системе реализовано три типа наблюдателей с принципиально разной логикой поведения:
- animateOnce — наблюдатель срабатывает при первом попадании элемента в viewport, добавляет CSS-класс
is-visibleи немедленно отключается черезunobserve(). Класс остаётся на элементе навсегда. Это не только правильная UX-модель для большинства анимаций появления, но и осознанная оптимизация: отключённый наблюдатель не потребляет ресурсы при дальнейшей прокрутке. - animateAlways — класс добавляется при входе элемента в viewport и убирается при выходе. Наблюдатель не отключается. Используется для элементов, которые должны воспроизводить анимацию каждый раз, когда пользователь прокручивает к ним — например, для счётчиков или акцентных блоков.
- floatingObserver — расширенная логика, учитывающая не только факт видимости, но и позицию элемента относительно viewport. Элементам, которые уже прокрутились выше текущей позиции, присваивается класс
is-above. Это позволяет применять отдельные CSS-правила для «пройденных» секций — например, реализовывать эффект parallax-выхода или изменять стиль шапки в зависимости от того, какой блок остался позади.
Custom Backgrounds: изолированные анимации в iframe
Каждая страница сайта может иметь уникальный анимированный фон — p5.js-скетч с генеративной графикой, Three.js-сцену с 3D-объектами, GLSL-шейдер с процедурными текстурами или любой другой визуальный эффект. Код фона хранится в Firestore как произвольная HTML/CSS/JS-строка, вставляется в поле админ-панели и при генерации страницы помещается внутрь тега
<iframe>Использование iframe — не упрощение, а намеренное архитектурное решение, которое одновременно решает две несвязанные проблемы.
Первая — безопасность. Код фона выполняется в полностью изолированном контексте браузера: он не имеет доступа к DOM основной страницы, не может читать JavaScript-переменные, не видит пользовательские данные и не способен перехватить события интерфейса. Это важно, потому что поле для фона доступно через админ-панель, и изоляция исключает случайное или намеренное нарушение работы основного интерфейса.
Вторая — производительность. Тяжёлые вычисления — рендеринг WebGL, физические симуляции частиц, попиксельные операции шейдеров — выполняются в отдельном контексте рендеринга и не конкурируют с основным потоком за ресурсы процессора. Пользователь видит богатый визуальный фон, пока интерфейс остаётся отзывчивым и не теряет кадры при взаимодействии.
Glow Carousel: математика на уровне UI
Карусель на главной странице — это не стандартный CSS-слайдер с overflow: hidden и переключением классов. Это компонент с физически обоснованным эффектом светового блика, который реагирует на позицию курсора в реальном времени.
Ядро эффекта — функция арктангенса от двух аргументов: atan2(dy, dx). При движении курсора над карточкой компонент вычисляет вектор от центра карточки до текущей позиции курсора. Функция atan2 преобразует этот вектор в угол в диапазоне от -π до π. Угол переводится в градусы и используется как направление CSS-градиента, создающего блик. Расстояние от центра карточки до курсора определяет интенсивность эффекта через функцию clamp().
Результат: блик физически корректно «следует» за источником света — курсором пользователя. Это не анимация по таймеру и не предзаписанный keyframe, а вычисление в реальном времени при каждом событии mousemove.
Помимо эффекта блика, в карусели реализован ряд инженерных решений:
- Lazy Loading фоновых изображений через атрибут
data-bg-src. Изображение карточки не загружается до тех пор, пока карточка не становится активной. CSS-свойствоbackground-imageустанавливается программно только в момент показа слайда, после чего атрибутdata-bg-srcудаляется — повторной загрузки не происходит. - Защита от прерывания анимации через булевый флаг
isAnimating. Если пользователь кликает стрелку быстрее, чем завершается переход между слайдами, новый переход не запускается до окончания текущего. Это предотвращает визуальные артефакты и некорректное состояние компонента. - Динамическая генерация навигации. Точки-индикаторы и стрелки создаются программно на основе реального количества слайдов, полученных из Firestore. Шаблон не содержит захардкоженных элементов навигации.
- Поддержка тем через флаг
isLightTheme. Компонент адаптирует цветовую схему блика к светлой или тёмной теме страницы без дублирования логики.
Результаты: что получилось в итоге
Архитектура, описанная в этом кейсе, — это не экспериментальная разработка и не академическое упражнение. Это продакшн-система, которая обслуживает сайт агентства ежедневно и воспроизводима для клиентских проектов там, где задача требует максимального технического качества.
TTFB близок к нулю: сервер не думает — он отдаёт
Time To First Byte — время от отправки запроса до получения первого байта ответа — практически равен 117 мс. Сервер не выполняет ни одного вычисления по запросу: нет запросов к базе данных, нет PHP или Node.js, нет серверного рендеринга. CDN получает запрос и немедленно отдаёт готовый HTML-файл из кеша ближайшей точки присутствия.
Для сравнения: WordPress с платным плагином агрессивного кеширования при благоприятных условиях даёт TTFB 80–200 мс.
Нет серверного рендеринга: никакого боевого бэкенда
На продакшн-сервере нет ни Node.js, ни PHP, ни работающей базы данных. Инфраструктура сведена к минимуму: Firebase Firestore хранит данные и доступен только во время сборки, CDN раздаёт статические файлы. Это означает отсутствие целого класса проблем — уязвимостей серверного кода, утечек памяти, падений базы данных под нагрузкой, необходимости обновлять серверное окружение.
SEO из коробки: разметка как результат сборки, а не ручного труда
Каждая страница при генерации автоматически получает полный комплект технической SEO-разметки: корректные теги hreflang для всех языковых версий с региональными суффиксами, правильно назначенный x-default, канонический URL, полный набор Open Graph-тегов для социальных сетей, актуальную запись в sitemap.xml с реальной датой последнего изменения из базы данных.
Ни одно из этих свойств не требует ручного контроля после первоначальной настройки системы. Редактор добавляет новую статью — при следующей сборке она автоматически появляется в sitemap с корректными метатегами и hreflang-связями со всеми переводами.
Масштабирование без затрат: трафик растёт, счёт — нет
Статические файлы раздаются CDN. Стоимость хостинга не зависит от количества посетителей: тысяча запросов в день и миллион запросов в день стоят одинаково — близко к нулю. Это принципиальное отличие от любой архитектуры с серверным рендерингом, где рост трафика прямо транслируется в рост операционных расходов.
Горизонтальное масштабирование бесплатно по умолчанию: CDN-провайдер сам распределяет нагрузку между точками присутствия по всему миру.
AI-видимость: готовность к следующему поколению поиска
Файл llms-full.txt обновляется при каждом деплое и дает языковым моделям — ChatGPT, Claude, Perplexity и другим — полное и структурированное представление о сайте: чем занимается агентство, какие услуги предоставляет, какой контент опубликован и на каких языках. Это инвестиция в поисковую видимость будущего, где значительная часть трафика будет генерироваться AI-агентами, а не классическими поисковыми системами.
Лучший способ продемонстрировать техническую компетенцию — показать её в работе. Каждое решение этого сайта мотивировано конкретной задачей, которую мы сформулировали до начала разработки.
Если ваш проект требует той же степени инженерной точности — мы готовы обсудить его архитектуру.