Этот сайт
Сайт-резюме на Next.js и Payload CMS: весь контент редактируется в админке, стаж и метрики считаются из данных и CI.

Context and goal
Сайт-резюме должен за полминуты отвечать рекрутеру на вопросы «кто, какой стек, как связаться», а техлиду — показывать, как я принимаю решения. Заодно он сам служит кейсом: всё, что на нём сказано про производительность, доступность и тесты, проверяется на нём же.
Контент полностью редактируется в админке. Пустые поля не превращаются в заглушки: блок без данных просто не рендерится. Цифры берутся только из данных или из CI.
My role
Дизайн, фронтенд, бэкенд и инфраструктура — от модели данных до деплоя.
- Next.js 16 (App Router, React Server Components) и Payload CMS 3 в одном приложении, PostgreSQL, миграции.
- Архитектура по Feature-Sliced Design, границы слоёв проверяет Steiger в CI.
- Дизайн-система на CSS custom properties и tailwind-variants, витрина в Storybook с проверкой доступности.
- Анимации на GSAP и Motion с общим провайдером
prefers-reduced-motion. - PDF-резюме из тех же данных через @react-pdf/renderer.
- Docker, Caddy с автоматическим HTTPS, GitHub Actions с Lighthouse CI.
Decisions
Стаж считается из записей опыта. Периоды переводятся в месяцы, пересекающиеся склеиваются, поэтому параллельная работа не удваивает стаж. Одна и та же функция работает на сайте и в PDF, и цифры не расходятся.
export const mergeRanges = (ranges: MonthRange[]): MonthRange[] => {
const sorted = [...ranges].sort((a, b) => a.from - b.from)
const merged: MonthRange[] = []
for (const range of sorted) {
const last = merged.at(-1)
if (last && range.from <= last.to + 1) {
last.to = Math.max(last.to, range.to)
} else {
merged.push({ ...range })
}
}
return merged
}Объединение периодов: смежные и пересекающиеся интервалы склеиваются до подсчёта.
Русская версия живёт в корне, английская — под /en. Внутри это один динамический сегмент [locale]: proxy переписывает адрес и на каждый запрос выдаёт nonce для Content Security Policy.
const response = hasLocale
? NextResponse.next({ request: { headers: requestHeaders } })
: NextResponse.rewrite(
new URL(`/${defaultLocale}${pathname === '/' ? '' : pathname}${search}`, request.url),
{ request: { headers: requestHeaders } },
)
response.headers.set('Content-Security-Policy', csp)Из-за nonce страницы рендерятся динамически, но данные кэшируются по тегам. Хуки Payload после сохранения сбрасывают только затронутые теги, поэтому правка в админке видна на сайте без пересборки.
Навык показывается, только если на него ссылается проект или место работы. Неподтверждённые навыки и всё, что скрыто из-за пустых полей, собраны на дашборде админки.
Results
- Производительность, главная
- 72из 100
- Локальный прогон Lighthouse CI 22.10.2026, мобильный профиль (Moto G Power, медленный 4G), медиана трёх замеров.
- Производительность, /projects
- 55из 100
- Локальный прогон Lighthouse CI 22.10.2026, мобильный профиль (Moto G Power, медленный 4G), медиана трёх замеров. На этой странице больше всего разброс между замерами.
- Производительность, этот кейс
- 74из 100
- Локальный прогон Lighthouse CI 22.10.2026, мобильный профиль (Moto G Power, медленный 4G), медиана трёх замеров.
- Доступность, лучшие практики, SEO
- 100из 100
- На главной, /projects и этом кейсе. Локальный прогон Lighthouse CI 22.10.2026, мобильный профиль (Moto G Power, медленный 4G), медиана трёх замеров.
- LCP главной
- 3,7с
- Локальный прогон Lighthouse CI 22.10.2026, мобильный профиль (Moto G Power, медленный 4G), медиана трёх замеров. CLS на всех трёх страницах — 0.
- Total Blocking Time главной
- 450мс
- Локальный прогон Lighthouse CI 22.10.2026, мобильный профиль (Moto G Power, медленный 4G), медиана трёх замеров.
- Начальный JS главной
- 172КБ gzip
- Все скрипты, загруженные до затишья сети, в том же локальном прогоне 22.10.2026. Форма связи, Motion, GSAP и WebGL грузятся отдельными чанками по необходимости.
- Автотесты
- 119 + 55
- Unit- и компонентные тесты Vitest плюс прогоны e2e Playwright на десктопном и мобильном Chromium (включая axe в обеих темах), 22.10.2026.