Персональный сайт-портфолио с CMS, AI-ассистентом и приватной аналитикой
Сайт-портфолио с собственной админкой, AI-ассистентом для посетителей и встроенной приватной аналитикой без cookie-баннера

Слайд 1 из 12
ОПИСАНИЕ
Full-stack веб-приложение на Next.js 16 (App Router) и React 19 на TypeScript. Рендеринг — серверные компоненты (RSC), все изменения данных идут через серверные экшены, сборка — Turbopack. Идея проекта: сайт — это не статическая вёрстка, а управляемая из БД система, где каждый текст, картинка, работа и раздел меняются через админ-панель, а публичные страницы и AI-ассистент берут данные из единого источника правды — PostgreSQL. Архитектура и слой данных. БД — PostgreSQL, доступ через postgres-js и ORM Drizzle; вся схема вынесена в отдельное пространство имён БД (future_face) и насчитывает порядка двадцати пяти таблиц. Контент разбит на сущности: навигация, hero-блоки страниц, цитаты (с массивом страниц, где они показываются), KV-тексты главной/«обо мне»/работ/контактов/резюме, общий словарь UI-текстов, слоты изображений сайта и заметки администратора. Работы смоделированы как корневая таблица work_items с зависимыми таблицами тегов, пунктов «что сделал» / «как сделано» / «результаты» и изображений — все с внешними ключами и каскадным удалением. Пул подключений к БД ограничен по числу соединений и закрывает простаивающие (max, idle_timeout, max_lifetime), чтобы не упираться в лимит подключений сервера. CMS-админка. Закрытый раздел /admin — это полноценная панель управления: отдельные экраны для текстов каждой страницы, работ (с загрузкой изображений), разделов «обо мне», блоков резюме, контактов, цитат, навигации, галереи, изображений-слотов, дизайна и настроек AI-ассистента. Контент правится без единой строчки кода — это и отличает проект от обычного статического портфолио. Аутентификация и защита бэкенда. Доступ к /admin защищён на серверном уровне: запросы перехватывает Next-proxy (бывший middleware) и проверяет подписанный токен сессии — HMAC-SHA256 поверх httpOnly-cookie, реализованный на Web Crypto API, поэтому модуль одинаково работает и в edge-рантайме proxy, и в Node. Пароль сверяется в постоянном времени (хешируются оба значения + timing-safe сравнение), у токена есть срок жизни. Дополнительно каждый изменяющий серверный экшен первой строкой вызывает requireAdmin() — это реальная защита мутаций, а не только редиректы неавторизованных. Двуязычность (RU/EN). Реализована на уровне данных, без i18n-библиотек: язык хранится в cookie, KV-значения переводятся суффиксом ключа __en, а табличные сущности — параллельными колонками *_en. Итоговый словарь собирается наложением переводов из таблицы UI-текстов поверх статичного словаря в коде, так что любой текст можно переопределить из БД. Язык ответа AI-ассистента определяется автоматически по тексту реплики пользователя (с откатом на язык сайта). Медиа (Cloudinary). Загрузка идёт серверным потоковым upload-ом (image/video/raw, чанками), раздача — через производные URL с трансформациями: авто-формат и авто-качество, ресайз по ширине/высоте, размытый LQIP-плейсхолдер (w_32 + e_blur) для мгновенного показа до загрузки оригинала, постер и чёткий превью-кадр для видео, конвертация тяжёлых исходников (.mov/.avi и т.п.) в .mp4. Удаление из облака — по сохранённому cloudinary_id. AI-ассистент, текст. Работает поверх OpenAI Chat Completions с циклом function-calling: модель может вызывать веб-поиск (Tavily) и чтение страницы по ссылке, число таких вызовов за запрос ограничено. Инструмент чтения страницы защищён от SSRF — перед запросом резолвится DNS и каждый адрес проверяется на приватные/служебные диапазоны (loopback, частные сети, link-local и облачные метаданные 169.254, CGNAT, multicast, аналоги в IPv6), редиректы обходятся вручную с повторной проверкой каждого хопа, стоит тайм-аут, тело страницы обрезается. В системный промпт подставляется «досье» о владельце, собираемое из БД (разделы «обо мне», работы, резюме, контакты) и кешируемое с TTL вместе с картой сайта; есть режимы (анализ вакансии / задачи / оценка по рынку) с редактируемыми из админки инструкциями. Стоимость каждого запроса считается по токенам и тарифам моделей и пишется в аналитику. AI-ассистент, голос. Голосовой режим — на OpenAI Realtime: сервер выпускает эфемерный client_secret (ключ наружу не отдаётся), модель и голос берутся из настроек. Ассистенту даётся инструмент navigate_to(path) — по согласию пользователя он открывает нужную страницу прямо во время звонка, не прерывая разговор; путь берётся строго из данных сайта. Голосовые сообщения распознаются через Whisper. Логика выбора языка прописана отдельно: разговор начинается на языке сайта и переключается на язык пользователя, как только тот заговорил. Доступ к OpenAI через прокси. Для серверов, у которых нет прямого доступа к OpenAI (geo-блокировка), сделана обёртка над fetch: при заданной переменной окружения все вызовы OpenAI (чат, голос, распознавание) идут через SOCKS5-прокси (fetch-socks поверх undici-fetch, в обход патченного глобального fetch); без переменной — обычный прямой запрос. Приватная аналитика. Своя система в той же БД, без cookie-баннера: идентификатор посетителя — суточный SHA-256-хэш от IP + User-Agent + соль, поэтому личные данные не хранятся, а хэш ежедневно сменяется. Типы событий ограничены белым списком (просмотры, время на странице, сообщения и звонки ассистенту, клики по контактам, скачивание резюме, смена языка), определяются устройство (по UA) и страна (по заголовкам прокси), админка никогда не трекается. Реализованы кампанийные ссылки: короткий адрес /<code> редиректит на целевую страницу и помечает события меткой, по каждой ссылке можно отключить трекинг и задать аудиторию (для разделения рекрутёров, друзей и собственных заходов). Выгрузка в Google Sheets. Аналитическая сводка выгружается в Google Sheets через сервисный аккаунт; клиент к Sheets API написан без сторонних зависимостей — сервис-аккаунтный JWT подписывается самостоятельно по RS256. Метрики считаются агрегирующими SQL-запросами (классификация страниц по типам, суммы по событиям). Резюме и PDF. Резюме существует и как страница сайта, и как отдельный документ (/resume-file) с двухколоночной А4-вёрсткой. На странице резюме показан «живой» предпросмотр этого документа во встроенном iframe, масштаб которого пересчитывается через ResizeObserver и зависит только от ширины контейнера — поэтому при изменении высоты окна вёрстка не «плывёт». Скачивание резюме в PDF делается рендерингом печатной версии страницы через headless Chrome (с учётом языка). Дополнительные блоки резюме (в боковую колонку или основную часть) добавляются из админки. Раздел «обо мне». Тексты раздела хранят собственную лёгкую разметку (:quote: — крупная цитата, :stat: — карточки с цифрами, идущие подряд группируются, :tags: — ряд чипов, заголовки, выноски, списки); отдельный парсер разбирает её в React-компоненты. Разделы имеют человекочитаемый url_slug для адресов /about/<slug>, есть галерея. Адаптивная типографика. Раскладка и размеры построены на CSS container queries и единицах cqi/cqh/cqw с clamp(): карточки работ подстраиваются под ширину самой карточки (а не под брейкпоинты вьюпорта) — при сужении сначала скрываются теги, потом сжимается галерея с трёх превью до одного, описание не обрезается; в блоках AI-ассистента шрифт ужимается по высоте контейнера, чтобы текст всегда влезал. Стили — Tailwind CSS v4 с CSS-first конфигурацией (@theme), без отдельного config. Сквозные решения. Все секреты и внешние доступы (OpenAI, Cloudinary, Tavily, сервис-аккаунт Google, прокси, БД, пароль админки) вынесены из бизнес-логики в переменные окружения; токены доступа и тяжёлые выборки кешируются; время событий аналитики нормализуется; рабочая директория Turbopack явно зафиксирована, чтобы сборщик не индексировал лишнее.
ИСПОЛЬЗУЕМЫЕ ИНСТРУМЕНТЫ
- TypeScript
- Next.js 16 (App Router
- React Server Components
- Server Actions
- proxy/middleware)
- React 19
- Tailwind CSS v4 (CSS-first @theme)
- tw-animate-css
- PostgreSQL
- postgres-js
- Drizzle ORM
- Drizzle Kit
- Cloudinary
- OpenAI API (Chat Completions
- Realtime
- Whisper)
- Tavily API
- Google Sheets API (сервисный аккаунт
- JWT RS256)
- fetch-socks
- undici
- Web Crypto API (HMAC-SHA256)
- CSS Container Queries (cqi/cqh
- clamp)
- headless Chrome (печать в PDF)
- Turbopack
- ESLint
- Git
РЕЗУЛЬТАТ
- Спроектирована и реализована схема PostgreSQL на ~25 таблиц в отдельном namespace, с внешними ключами и каскадным удалением; доступ через Drizzle ORM и пул postgres-js с контролем числа и времени жизни соединений.
- Построена собственная CMS-админка: весь контент сайта (тексты, изображения, работы, разделы, контакты, цитаты, навигация, настройки ассистента) правится из БД без изменений кода — единый источник правды.
- Реализована серверная аутентификация на подписанных HMAC-SHA256-токенах через Web Crypto (работает и в edge-proxy, и в Node), со сверкой пароля в постоянном времени и защитой всех мутаций через requireAdmin().
- Сделана двуязычность RU/EN на уровне данных (cookie + суффикс __en для KV и колонки *_en для сущностей) с наложением переводов из БД поверх статичного словаря, без i18n-библиотек; язык ответа AI определяется автоматически.
- Интегрирован Cloudinary: серверная потоковая загрузка image/video/raw и раздача через производные URL (авто-формат/качество, ресайз, размытые LQIP-плейсхолдеры, постеры и mp4-конвертация для видео).
- Разработан текстовый AI-ассистент на OpenAI с function-calling: веб-поиск (Tavily) и чтение страниц с защитой от SSRF (проверка DNS на приватные диапазоны и обход редиректов по хопам); контекст-«досье» собирается из БД и кешируется.
- Реализован голосовой режим на OpenAI Realtime с эфемерными сессиями и инструментом navigate_to для навигации по сайту во время звонка; распознавание речи через Whisper.
- Добавлена возможность пускать все вызовы OpenAI через SOCKS5-прокси (fetch-socks + undici) для серверов без прямого доступа, переключаемая переменной окружения.
- Реализован учёт стоимости запросов к AI по токенам и тарифам моделей с записью в аналитику.
- Построена приватная самописная аналитика без cookie-баннера: суточный хэш-идентификатор посетителя, белый список событий, время на странице, определение устройства и страны, исключение админки из трекинга.
- Реализованы кампанийные ссылки: короткий /<code> с редиректом на целевую страницу, метка событий, переключатель трекинга и аудитория по каждой ссылке.
- Написан клиент Google Sheets API без зависимостей (самостоятельная подпись JWT по RS256) и выгрузка агрегированной аналитики в таблицы.
- Сделан «живой» предпросмотр резюме через масштабируемый iframe (пересчёт масштаба по ResizeObserver, независимый от высоты окна) и экспорт резюме в PDF через headless Chrome.
- Написан парсер собственной разметки для раздела «обо мне» (:quote:/:stat:/:tags:/заголовки/выноски/списки) с рендером в компоненты.
- Внедрена fluid-типографика и адаптивная раскладка на CSS container queries и единицах cqi/cqh с clamp() — подгонка под контейнер с приоритетным скрытием второстепенных элементов вместо обрезки.
- Все секреты и внешние доступы вынесены в переменные окружения; тяжёлые выборки и токены кешируются.
AI АССИСТЕНТ
Задать вопрос по этой работе