«LILY BEYOND» — двуязычный сайт турлидера с CMS-админкой и приватной аналитикой

Двуязычный (RU/EN) сайт турлидера: витрина авторских туров, галерея, отзывы с модерацией и приём заявок. Весь контент — тексты, туры, фото, видео, резюме — редактируется из собственной админки и хранится в базе данных, а сайт ведёт свою приватную аналитику по трекинг-ссылкам, без cookie-баннера.

«LILY BEYOND» — двуязычный сайт турлидера с CMS-админкой и приватной аналитикой — фото 1
нажмите для просмотра в полном размере

Слайд 1 из 11

ОПИСАНИЕ

Full-stack веб-приложение на Node.js (Express, ESM) поверх PostgreSQL. Сознательно выбран максимально лёгкий стек: без фронтенд-фреймворков, без сборщиков и без ORM — серверная часть это тонкий слой SQL-запросов с параметрами через пул соединений pg, а фронтенд — чистый HTML/CSS и ванильный JavaScript. Идея та же, что отличает проект от статического лендинга: сайт — это управляемая из БД система, где каждый текст, тур, фотография, отзыв и раздел меняются через админ-панель, а публичные страницы берут данные из единого источника правды — PostgreSQL.

Архитектура «БД — источник правды, RAM — горячий кеш»

Ключевое инженерное решение проекта. База данных удалённая (облачная), и её задержки не должны влиять на скорость сайта, поэтому весь публичный контент — тексты, туры, галерея, одобренные отзывы, заявки, контакты, резюме и даже бинарные медиа — держится в памяти процесса и отдаётся оттуда мгновенно. БД остаётся источником правды: кеши прогреваются при старте и обновляются точечно после каждого сохранения из админки. Прогрев сделан отказоустойчивым — первый коннект к удалённой БД нередко таймаутит, поэтому каждый кеш поднимается с несколькими повторами через паузу; фоновый таймер раз в 5 минут перечитывает все кеши (самолечение при кратковременных сбоях БД). Если база недоступна вовсе, сайт продолжает работать на последних данных или на зашитых дефолтах, а не падает.

Медиа в базе данных

Видео и изображения хранятся прямо в БД как bytea вместе с метаданными (mime, имя, размер) и раздаются через собственный роут /api/media/:key из RAM-кеша — чтение 14-мегабайтного bytea из удалённой БД занимает секунды, поэтому уже загруженные файлы держатся в памяти. Роут полностью поддерживает HTTP Range-запросы (ответы 206 Partial Content с корректной обрезкой диапазона), благодаря чему браузер начинает проигрывать hero-видео сразу, не дожидаясь полной загрузки; отдача идёт с иммутабельным годовым кешем.

CMS-админка

Закрытый раздел /admin — полноценная панель управления с отдельными экранами: тексты каждой страницы, туры (с загрузкой изображений, порядком и публикацией), галерея, отзывы (модерация), заявки (папки, статусы, кампании), контакты и соцсети, резюме, юридические тексты, аналитика, заметки-блокнот и настройки дизайна. Весь контент правится без единой строчки кода — это и отличает проект от обычного статического сайта турлидера.

Аутентификация и защита бэкенда

Доступ к /admin и всем /api/admin/* закрыт HTTP Basic Auth на серверном уровне; логин и пароль сверяются в постоянном времени через crypto.timingSafeEqual (защита от timing-атак). Реализована политика «fail-closed»: если пароль администратора не задан в окружении, админка недоступна полностью — она физически не может случайно оказаться открытой из-за недонастройки. Отдельный слой middleware гарантированно блокирует выдачу файлов окружения (.env и прочих dotfiles), даже если бы их случайно попросили напрямую.

Двуязычность (RU/EN) на уровне данных, без i18n-библиотек

Редактируемые тексты хранятся под базовым (русским) ключом и его английским вариантом с суффиксом «__en»; API отдаёт нужный язык по ?lang=en с автоматическим откатом на русский, если перевод пуст. Перевод туров хранится внутри самого тура (data.i18n.en) и накладывается на базовую версию собственным рекурсивным «умным» слиянием: значения перевода перекрывают базу только там, где они непустые, массивы сливаются поэлементно, а общие поля (slug, картинка, акцентный цвет, флаг) в перевод не дублируются. Зашитые подписи интерфейса переводит небольшой скрипт по атрибутам data-i18n / data-i18n-ph. Выбор языка хранится в localStorage и общий для сайта и админки, контент переключается на лету без перезагрузки.

Модель туров

Весь объект тура хранится в JSONB (гибкая схема без миграций под каждое поле) плюс служебные колонки: slug, порядок сортировки и флаг публикации. Туры доступны по человекопонятным адресам /tours/:slug; старые ссылки с расширением .html аккуратно редиректятся (301) на чистые URL, включая перенос tour.html?slug=X на /tours/X — это сохраняет SEO при смене схемы адресов.

Приём заявок и отзывов

Заявки приходят с формы контактов и со страниц туров (с привязкой к конкретному туру и тарифу), проходят серверную валидацию и обрезку длины, складываются в БД со статусами (новая / прочитана / принята / архив) и раскладываются по папкам, которые администратор заводит сам (например, под конкретные туры). Отзывы публикуются только после модерации (pending → approved / rejected). Значки «новое» в меню админки считаются по кешу без обращения к БД.

Приватная аналитика без cookie-баннеров

Собственная система трекинга в той же БД: администратор создаёт короткие трекинг-ссылки (кампании с кодом ?ref=…), чтобы разделять источники трафика. Типы событий ограничены белым списком (просмотр страницы, время на странице, заявка, отзыв, клик по контакту). Время на странице считается честно — только активное (учитывается видимость вкладки), отправляется порциями и досылается на уходе через navigator.sendBeacon, чтобы не терять данные при закрытии вкладки. Для самого владельца предусмотрен режим notrack (запоминается в браузере) — собственные заходы не искажают статистику. Сервер агрегирует события по каждой кампании и отдельно по «прямым заходам», строит сводку «за сегодня» (посетители, суммарное время, топ туров) и полную разбивку с кликами по контактам и популярностью тарифов.

Мгновенный hero-экран

Первый экран появляется сразу: до загрузки видео фоном показывается статичный постер, а само видео рисуется поверх в canvas с прозрачным фоном (пока нет ни одного кадра, сквозь холст виден постер). Кадры масштабируются по принципу cover с учётом плотности пикселей экрана (DPR), а отрисовка видео в canvas — самая тяжёлая повторяющаяся операция — идёт только пока hero виден и вкладка активна: IntersectionObserver и visibilitychange останавливают цикл requestAnimationFrame и ставят видео на паузу за экраном.

Анимации и «живые» ленты

Заголовок первого экрана проявляется побуквенно — причём анимация запускается только после гидрации текста из БД, чтобы делить на буквы уже итоговую строку (с корректным переносом по словам на узких экранах). Лента туров — «бегущая строка» из стеклянных карточек, которую можно прокручивать перетаскиванием (Pointer Events) и которая замирает за пределами экрана. Отзывы идут бесшовной лентой (три копии набора для непрерывной петли), подгружаясь из реальной базы одобренных отзывов.

Страница тура: маршрут с летящим самолётиком

Маршрут по дням отрисован SVG-кривой: через центры кружков-дней строится плавная кубическая кривая, волнообразно изгибающаяся вправо-влево. По мере прокрутки линия «дорисовывается» (анимация stroke-dashoffset от полной длины к нулю), а по её концу летит иконка самолёта — позиция берётся через getPointAtLength, угол поворота считается по касательной к кривой (по двум близким точкам), учитывается направление прокрутки (при листании вверх нос разворачивается), а у конца маршрута самолёт плавно исчезает. Всё уважает prefers-reduced-motion.

Резюме и PDF

Резюме существует как страница сайта и скачивается в PDF «пиксель-в-пиксель» — сама страница захватывается через html2canvas и собирается в один PDF-лист формата A4 в html2pdf, без масштабирования под лист, поэтому размер текста в файле совпадает с тем, что видно на экране. PDF генерируется на языке текущей версии сайта; контактные иконки (Telegram / Instagram / VK) нарисованы так, чтобы чётко попадать в итоговый документ. Сам PDF-файл резюме также можно загрузить в админке и отдать по /resume.pdf.

Сквозные решения

Все секреты и внешние доступы (БД, пароль админки, SSL) вынесены в переменные окружения; трафик сжимается gzip (compression) для html/css/js/json; пул PostgreSQL ограничен по числу соединений и корректно переживает обрывы фоновых idle-соединений с удалённой БД (обработчик ошибок пула, чтобы необработанное событие не роняло процесс). Зависимости намеренно минимальны (express, pg, compression, dotenv). Прод развёрнут на VPS под управлением systemd.

ИСПОЛЬЗУЕМЫЕ ИНСТРУМЕНТЫ

  • JavaScript (ES Modules)
  • Node.js
  • Express
  • PostgreSQL
  • pg (node-postgres
  • пул соединений)
  • SQL
  • JSONB
  • bytea
  • HTTP Range (206 Partial Content)
  • HTTP Basic Auth
  • Web Crypto / crypto.timingSafeEqual
  • compression (gzip)
  • dotenv
  • HTML5
  • CSS3
  • Canvas 2D
  • SVG (getPointAtLength / stroke-dashoffset)
  • IntersectionObserver
  • Pointer Events
  • requestAnimationFrame
  • navigator.sendBeacon
  • localStorage
  • Fetch API
  • html2pdf.js / html2canvas
  • systemd (VPS-деплой)
  • Git

РЕЗУЛЬТАТ

  • Спроектирован и реализован двуязычный (RU/EN) сайт турлидера на лёгком стеке Node.js + Express + PostgreSQL — без фронтенд-фреймворков, сборщиков и ORM.
  • Выстроена архитектура «БД — источник правды, RAM — горячий кеш»: весь публичный контент отдаётся из памяти мгновенно и не зависит от задержек удалённой БД.
  • Реализована отказоустойчивость: прогрев кешей с повторами при старте, фоновое обновление раз в 5 минут (самолечение) и работа на дефолтах при недоступной БД.
  • Медиа (видео/фото) хранятся в БД как bytea и раздаются из RAM-кеша с поддержкой HTTP Range (206) и годовым immutable-кешированием — hero-видео стартует сразу.
  • Построена собственная CMS-админка: тексты, туры, галерея, отзывы, заявки, контакты, резюме, аналитика и дизайн правятся из БД без изменений кода.
  • Защита админки на серверном уровне: Basic Auth со сверкой пароля в постоянном времени (timingSafeEqual), политика fail-closed и блокировка выдачи .env/dotfiles.
  • Сделана двуязычность на уровне данных без i18n-библиотек: суффикс __en для KV-текстов и рекурсивное «умное» слияние перевода туров (data.i18n.en).
  • Туры смоделированы гибко в JSONB со slug-адресами /tours/:slug и 301-редиректами со старых .html-ссылок — SEO сохранено при смене схемы URL.
  • Реализован приём заявок с привязкой к туру и тарифу, статусами и папками, а также отзывы с модерацией (pending → approved/rejected) и счётчиками «новое» в меню.
  • Построена приватная аналитика без cookie-баннера: трекинг-ссылки (кампании), белый список событий, честный учёт активного времени с досылкой через sendBeacon, режим notrack для владельца и агрегация по кампаниям и «прямым заходам».
  • Сделан мгновенный hero: постер-фон + отрисовка видео в canvas с паузой рендера за экраном (IntersectionObserver + visibilitychange) и cover-масштабированием.
  • Реализованы «живые» интерфейсы: побуквенное появление заголовка после гидрации, drag-прокручиваемая лента туров и бесшовная лента реальных отзывов.
  • На странице тура построен SVG-маршрут по дням с самолётиком, летящим по касательной к кривой по скроллу (getPointAtLength + stroke-dashoffset), с учётом направления прокрутки и prefers-reduced-motion.
  • Реализовано скачивание резюме в PDF «пиксель-в-пиксель» (html2pdf/html2canvas): одна A4-страница с размером текста как на экране и корректными иконками контактов.
  • Все секреты вынесены в окружение, трафик сжимается gzip, пул PostgreSQL устойчив к обрывам фоновых соединений; прод развёрнут на VPS под systemd.

AI АССИСТЕНТ

Задать вопрос по этой работе