CV Hub
Автор и разработчик·2026

CV Hub

Резюме как код. Профессиональная идентичность как воспроизводимая версионируемая система.

Platforms
  • Web
  • GitHub Pages
Stack
  • Astro
  • TypeScript
  • YAML
  • Playwright
  • docx.js
  • Canvas
  • GitHub Actions
Главная страница CV Hub

Обзор

CV Hub — это не шаблон портфолио. Это система. Проект переосмысляет резюме как инженерный артефакт — версионируемый, воспроизводимый и развёртываемый. Вместо того чтобы поддерживать разрозненные представления в LinkedIn, PDF, Notion и на сайтах-портфолио, CV Hub вводит единый декларативный источник правды — YAML-файл, который управляет всем пайплайном вывода. Результат — полностью автоматизированная система, где одно изменение согласованно расходится по всем форматам и всем ролевым вариациям. Сама страница, которую вы читаете, — часть этой системы: этот кейс собран из блочного YAML-документа тем же движком, что строит резюме.


Проблема

С точки зрения систем традиционные резюме фундаментально сломаны. Они: - дублируются между платформами - синхронизируются вручную - зависят от контекста (DevOps vs Backend vs GameDev) - не находятся под версионным контролем - не воспроизводимы Это ведёт к рассинхрону, несогласованности и заметным накладным расходам на поддержку. С инженерной точки зрения это эквивалентно запуску нескольких несинхронизированных окружений без единого источника правды.


Решение

CV Hub применяет инфраструктурные принципы к персональным данным. Базовая идея проста: опиши один раз → сгенерируй всё → разверни автоматически. Один YAML-файл становится: - живым сайтом - артефактами PDF, DOCX и TXT - несколькими ролевыми версиями резюме - версионируемым профилем под контролем Git Система устраняет дублирование и обеспечивает согласованность by design.


Мультипрофильная система

Архитектура base + delta

Ролевые резюме генерируются по двухслойной модели: базовое CV плюс дельта-переопределения на каждый профиль. Это даёт точный контроль над позиционированием без дублирования данных.

Пайплайн слияния профилей

Как работают профили

Каждый профиль объявляется один раз в profiles.yml — id, URL-slug и ключ spec. На этапе сборки merge.mjs проходит по каждой комбинации профиль × язык и создаёт нормализованный артефакт на пару. Слияние намеренно явное, а не магическое: - Скалярные и списочные поля дельты целиком заменяют базовое значение. - Записи опыта сопоставляются по названию компании — дельта-запись, в которой указана только компания, наследует базовую и переопределяет лишь те поля, что задала. - Список опыта профиля полностью определяет свой упорядоченный набор, так что каждая роль курирует ровно ту историю, которая ей релевантна. Вывод детерминирован: один и тот же источник всегда даёт одни и те же артефакты, а гард на этапе сборки следит, чтобы routing-slug профиля и его file-spec ключ не расходились — страница профиля никогда не разъедется с его CV и файлами для скачивания.


Интернационализация

Языки — это ось первого класса, а не довесок. Система рендерит матрицу N профилей × N языков из единой базы контента. - Реестр languages.yml задаёт доступные языки и язык по умолчанию; переключатель языка в шапке генерируется из него. - Каждая строка интерфейса живёт в translations.yaml и резолвится через небольшой хелпер makeT() с цепочкой фоллбэка: запрошенный язык → английский → сам ключ, так что отсутствующий перевод деградирует мягко, а не ломает страницу. - Маршрутизация полностью статическая. [...slug].astro выпускает по странице на каждую комбинацию профиль × язык, а главная на языке по умолчанию остаётся на чистом корневом пути. Китайские переводы уже лежат в слое контента и готовы к включению добавлением одной строки в реестр языков.


Что я построил

  • Static-first архитектура с нулём клиентского JavaScript по умолчанию
  • YAML как единый источник правды для всех данных резюме
  • Мультипрофильная система (DevOps, GameDev и т.д.) через детерминированный пайплайн слияния
  • Поддержка нескольких языков с мягкой цепочкой фоллбэка
  • Автоматическая генерация документов (PDF через Playwright, DOCX/TXT через docx.js)
  • Блочный движок кейсов, на котором работают эти Deep Dive страницы
  • Четыре взаимозаменяемых анимированных фоновых компонента (CSS и Canvas)
  • Переключение тем через URL с шарингом состояния
  • Полностью автоматизированный CI/CD на GitHub Actions с алертами в Telegram
  • Динамическая конфигурация по окружению (siteUrl из GITHUB_REPOSITORY)
  • Одноколоночный ATS-safe PDF рядом с полированным двухколоночным, из тех же данных
  • Онбординг в одну команду (npm run init) — чистит пример и стартует форк с чистого листа
  • Автогенерируемые превью-карточки для соцсетей на каждый язык из реальных данных CV — без дизайнера
  • schema.org/Person JSON-LD, hreflang и SEO-метаданные на каждую страницу вместо одной общей строки
  • Готовность к печати — Ctrl+P даёт чистый документ, а не скриншот тёмной темы
  • Поддержка своего домена (SITE_URL / BASE_PATH) поверх дефолтного GitHub Pages URL

Архитектура

Система построена как детерминированный пайплайн. Слой данных: - YAML-файлы (базовое CV + дельты профилей, проекты showcase, кейсы, переводы, changelog) Слой обработки: - merge.mjs → нормализованные артефакты на каждый профиль и язык - генераторы документов → создают PDF / DOCX / TXT из этих артефактов Слой представления: - статическая сборка Astro → предрендеренный HTML, контент валидируется типизированными схемами Развёртывание: - GitHub Actions → неизменяемая сборка → GitHub Pages Никакого runtime-сервера. Никакого скрытого состояния. Никакой базы данных. Каждый вывод воспроизводим из исходника, а форма контента проверяется Zod-схемами ещё до сборки страниц.


Динамические фоны

Атмосферу сайта создаёт небольшая библиотека взаимозаменяемых фоновых компонентов. Активен всегда один — они спроектированы как drop-in палитра, каждый самодостаточен и настраивается через ту же систему дизайн-токенов, что и остальная стилизация. Семейства два: CSS-слой, который почти ничего не стоит, и набор Canvas-рендереров, вдохновлённых интерфейсом PlayStation XMB. Каждый показан ниже.

AnimatedBackground — по умолчанию

Чистый CSS · ноль JavaScript

Четыре крупных орба из радиальных градиентов дрейфуют за контентом, поверх — тонкий слой шума. Это полностью CSS — без JavaScript, без canvas, без работы в основном потоке. Это и самый оптимизированный фон: keyframes анимируют только смещение (без scale), поэтому браузер кеширует каждый размытый орб как текстуру и просто двигает её, а не пересчитывает blur 60px каждый кадр. Весь слой отключается ниже 768px и уважает prefers-reduced-motion.

AnimatedBackground — дрейфующие размытые орбы

GalaxyBackground

Canvas · requestAnimationFrame

Звёздное поле на canvas с медленным параллакс-дрейфом, подкрашенное под акцент активной темы. Более тяжёлый, кинематографичный вариант — когда атмосфера важнее стоимости простоя.

GalaxyBackground — звёздное поле

PlayStationWaves

Canvas · заливные волны в духе XMB

Заливные синусоиды, наслоённые как меню PlayStation XMB, со сдвигом оттенка по времени суток и недетерминированной стартовой позицией (сид от таймстампа) — так что два запуска не выглядят одинаково. Около 18 пропсов открывают число волн, скорость, амплитуду, цвет и качество отрисовки.

PlayStationWaves — заливные синусоидальные волны

WaveLines

Canvas · светящиеся линии в духе XMB

Штриховой родственник PlayStationWaves: светящиеся линии, симметрично расположенные вокруг центральной оси. Свечение каждой линии композитится на собственном offscreen-canvas — чтобы обойти тонкий баг, где общая маска destination-in съедала альфу фона по краям.

WaveLines — светящиеся штриховые линии

Инженерия производительности фонов

Красивые фоны легко сделать тормозящими, поэтому они рассматриваются как отдельная поверхность производительности. - CSS-орбы избегают пересчёта blur каждый кадр, анимируя только трансформации. - Полноэкранный градиент живёт на fixed-псевдоэлементе вместо background-attachment: fixed, поэтому не перерисовывается при скролле. - Полноэкранный проход mix-blend-mode был убран, как только подтвердилось: при своей прозрачности он невидим, но дорог каждый кадр. - Каждый анимированный фон отключается на маленьких экранах и уважает reduced-motion. Итог: насыщенное движение на мощных десктопах и спокойный, дешёвый рендер на всём остальном.


Темы

Темизация — это набор CSS-переменных поверх токен-базы. Тема выбирается параметром ?theme= в URL и применяется крошечным инлайн-скриптом, что делает состояние шарящимся: отправь ссылку — и получатель увидит ровно ту палитру, что выбрал ты.

Варианты тем
Frosted, Light, Nordic, Peachy — управляются через URL-параметр

Пайплайн экспорта документов

Тот же смерженный YAML, что рендерит сайт, генерирует и файлы для скачивания — так веб- и офлайн-версии не могут разойтись. - PDF получается рендерингом print-вёрстки в headless Chromium через Playwright — пиксель-в-пиксель с сайтом. - DOCX собирается программно через docx.js — это настоящий редактируемый документ Word, а не скриншот. - TXT отдаётся как чистый текстовый фоллбэк для ATS-систем и копипаста. Каждая пара профиль × язык получает свой набор файлов, генерируемых в CI и подключённых прямо на соответствующей странице. Предпочитаешь Ctrl+P? Живая страница тоже печатается чисто — отдельный print-стиль убирает обвязку (шапку, футер, анимированный фон) и переопределяет все цветовые токены под бумагу, без отдельного шаблона на поддержке.


Свой файл — своей аудитории

PDF и DOCX — то, что узнаёт посетитель: полированное резюме и редактируемая версия, под которую рекрутинговые агентства переверстывают документ. ATS-safe одноколоночный PDF и текстовый экспорт — совсем другая аудитория: пригождаются в основном самому владельцу CV — вставить в форму джоб-портала или в textarea «paste your resume» — а не то, что кликнет рекрутер, листающий сайт: «ATS» для него звучит как жаргон, а не выгода. Какие форматы показывать и в каком порядке — настройка site.yml, а не зашитый ряд кнопок: личный сайт оставляет только два узнаваемых посетителю формата, демо-деплой может включить все четыре, чтобы показать ATS-вариант. PDF при этом всегда остаётся единственной визуально «сплошной» кнопкой.


Превью-карточки для соцсетей

Ссылка при шаринге должна что-то показывать. Каждая сборка скриншотит реальную, живо отрендеренную страницу через headless-браузер — без дизайнерского инструмента, без статичного шаблона — и компонует её в рамку с обоями 1200×630, используя дизайн-токены активной темы, так что карточка всегда синхронна с тем, как сейчас темизирован сайт. Одна карточка на язык, из реального CV дефолтного профиля (не из мок-персоны) — любая страница, включая профильные и кейсы, подбирает карточку своего языка автоматически через тот же фоллбэк og:image.


Находимость

То, что не видно, пока не заглянешь в dev tools: своё meta-описание и title на каждой странице вместо одной общей строки на весь сайт, schema.org/Person JSON-LD на CV-страницах — чтобы поиску по имени было что показать, hreflang-альтернативы, прокинутые прямо из тех же данных, что двигают переключатель языка, canonical-URL на каждом маршруте без случайных двойных слэшей, и sitemap, который не расходится с тем, что реально задеплоено.


CI/CD и автоматизация

Релиз — это один git push. - GitHub Actions гоняет весь пайплайн на каждый push в main: merge → генерация документов → рендер PDF → статическая сборка → деплой на Pages. - Бинарь Playwright Chromium кешируется по хешу lockfile, экономя около двух минут на холодном деплое. - Telegram-бот сообщает результат — успех, провал сборки или провал деплоя — с веткой и коротким SHA коммита, молча деградируя, если секреты не настроены. - siteUrl и base-путь выводятся из GITHUB_REPOSITORY, так что форк собирается и деплоится корректно без всякой конфигурации. - Sitemap, покрывающий каждый маршрут профиль × язык, генерируется на сборке, с соответствующим robots.txt.


Инженерия производительности

Производительность измеряется, а не предполагается. - Статический предрендеренный HTML без стоимости гидратации по умолчанию. - Первая обложка Showcase с реальной картинкой грузится eager с высоким приоритетом, остальные остаются lazy — намеренная оптимизация LCP. - Композитинг при скролле был профилирован, дорогие слои (перерисовка fixed-градиента, полноэкранный blend-проход) убраны. - Итог: Lighthouse 100 Performance / 100 Accessibility / 100 SEO, с движением, которое остаётся плавным на слабом железе.


Движок Showcase

Портфолио-половина сайта — та же идея, применённая рекурсивно. Проекты объявляются YAML-списком, и каждый может нести полноценный Deep Dive кейс — как этот — собранный из небольшого набора контент-блоков: text, image, video, code, divider. Блоки валидируются типизированной схемой и рендерятся отдельными компонентами, а значит длинный редакторский кейс пишется целиком в данных, без какого-либо кастомного кода страницы. Ведёт себя как контролируемая статическая альтернатива странице Notion: портативно, версионируемо и быстро. video встраивает YouTube-ссылку напрямую как адаптивный плеер — без хостинга своего файла, что очень пригодилось при тестировании:

Видео с YouTube, встроенное в видео-блок кейса
Теперь тебя могут рикролльнуть прямо в шоукейсе.

Инженерные решения

  • Astro вместо SPA-фреймворков — чтобы убрать лишний рантайм и стоимость гидратации
  • Статическая генерация — ради гарантии производительности и предсказуемости
  • YAML вместо CMS — ради полного контроля и Git-нативного версионирования
  • Состояние через URL вместо localStorage — ради шаринга и прозрачности
  • Явные дельта-файлы вместо неявных переопределений — чтобы изменения были наблюдаемы
  • Типизированные схемы контента — чтобы кривые данные роняли сборку, а не браузер

DevOps-подход

Проект относится к резюме как к build-артефакту. Ключевые принципы: - версионирование на Git - неизменяемые сборки - автоматизация CI/CD - развёртывание, независимое от окружения - нулевые runtime-зависимости Каждый коммит порождает полностью согласованное, развёртываемое состояние системы.


Компромиссы

  • Нет CMS — намеренный компромисс ради контроля и прозрачности
  • Нет бэкенда — чисто статическая архитектура
  • Ручное создание дельт для профилей — явность вместо неявной сложности
  • Canvas-фоны дороже CSS-дефолта — поэтому один по умолчанию, остальные по выбору
  • Ограниченная гибкость в рантайме в пользу детерминизма

Результат

  • Одна правка перегенерирует все форматы по всем профилям и языкам
  • Полностью статический деплой с нулевой стоимостью инфраструктуры
  • Готовая к форку система, не требующая конфигурации
  • Lighthouse: 100 Performance / 100 Accessibility / 100 SEO
  • Предсказуемый, воспроизводимый вывод между окружениями

Выводы

CV Hub показывает, что даже нетрадиционные домены вроде резюме выигрывают от инженерной строгости. Применяя принципы системного дизайна — единый источник правды, детерминированные пайплайны и автоматизацию — проект превращает статический документ в масштабируемую, поддерживаемую систему. Этот подход обобщается за пределы резюме на любой домен, где важны согласованность данных, воспроизводимость и вывод в несколько форматов.


Хочешь такое же?

Всё на этой странице — этот кейс, резюме за ним, даже рикролл парой секций выше — живой инстанс CV Hub. Если у тебя есть пять минут и ты умеешь пользоваться git, можешь получить свой.

1. Форкни

Сделай fork репозитория на GitHub, затем клонируй свой форк.

git clone https://github.com/YOUR_ACCOUNT/cv_hub.git
cd cv_hub

2. Начни с чистого листа

npm install, затем npm run init — чистит пример CV, шоукейса и профилей, что идут вместе с репозиторием, и подставляет вместо них пустой плейсхолдер, так что правишь своё, а не чужое резюме. Сначала всё бэкапит; npm run init -- --dry-run показывает, что будет сделано, ничего не трогая.

npm install
npm run init

3. Заполни данные

Всё живёт в одном файле: src/content/cv/ru.yaml. Три способа заполнить его — выбери, какой удобнее:

  • Редактировать YAML напрямую — имя, summary, опыт, навыки. Обычный текст, не нужен билд, чтобы увидеть форму.
  • Импортировать JSON Resume — уже есть такой? npm run resume:import сконвертирует его.
  • Отдать нейросети — вставь своё текущее резюме (PDF или обычный текст) в Claude или ChatGPT вместе с промптом из docs/LLM-CONTEXT.md. На выходе — готовый к использованию YAML. Реально самый быстрый путь, если не хочешь трогать схему руками.

4. Посмотри

Открой http://localhost:4321 — каждое сохранение видно сразу.

npm run dev

5. Задеплой

Включи GitHub Pages один раз — Settings → Pages → Source: GitHub Actions — а дальше просто пушь. Каждый коммит в main пересобирает и передеплоивает сайт автоматически. Больше никогда не нужен отдельный шаг деплоя.

git add .
git commit -m "my resume"
git push