К содержанию
Написать
portfolio/

Этот сайт

Портфолио, собранное как продукт: статическая сборка, свой вид у каждой страницы, открытый журнал решений и проверки перед каждым коммитом.

Что это
Сайт на двух языках. Главная, Флоу и Проекты готовы, «Пора на пару» пока заготовка. Это файлы без сервера приложений, их отдаёт Caddy.
Моя роль
Дизайн, вёрстка, движение, инфраструктура и тексты.
Что открыто
Код, журнал решений и план лежат на GitHub. У каждого раздела есть ссылки на его файлы.
Период
с 30.10.2026
Адрес
finchaf.com
Стек
Next.js, TypeScript, Tailwind, GSAP, Caddy
  • 135коммитов
  • 14решений
  • 14проверок
  • 557тестов
  • 4темы
  • 2языка

На 03.11.2026, ветка main и прогон check.py

Форма

У каждой страницы своя форма

Портфолио обычно рассказывает о работе словами. Здесь страница устроена как сама работа: главная это сцена с прокруткой вниз и вбок, Флоу выглядит как Obsidian, потому что тот кейс и был базой заметок, а Проекты похожи на редактор, потому что я дизайнер.

Цена такой формы: у каждой страницы свой движок. Чтобы они не срослись, страницы разделены границами. Фича не импортирует другую фичу, а общее переезжает ниже, в ui/ и lib/. Это не договорённость, а правило ESLint: импорт через границу роняет проверку.

ФормаФорма

Темы

Один компонент, четыре темы

Каждая страница объявляет свою тему, и тема меняет цвета, гарнитуры и вид контролов. Режимов два, поэтому сочетаний восемь. Компонент о темах не знает: он читает токены, а значения лежат в файле темы. Новая тема это один файл.

Цвет вне контракта просто не соберётся: палитра Tailwind отключена. Контраст считает тест, и ради него у темы два акцента. accent это точный цвет бренда для заливок, accent-strong это вариант для текста. Белый на синем продукта даёт 3,7:1, для текста этого мало.

Тема объявлена в разметке и читается через :has(), поэтому она есть в первом HTML и страница не мигает. Кнопки \Написать на главной и [[Написать]] на Флоу это один компонент, а капсула меняет обшивку на лету, когда на главной приезжает глава другой темы. Значения ниже считаны с примера при каждом переключении.

Заголовок страницы

Тот же абзац, та же разметка, те же компоненты.

НаписатьКод сайта

Линза

Дизайн и код на одном экране

Первый экран это два слоя: дизайн и его чертёж с размерами и настоящими строками кода. Тест находит эти строки в исходниках, поэтому чертёж не расходится с кодом.

Слои совпадают по координатам: каждый блок чертежа стоит на месте настоящего, потому что внутри него лежит сам этот блок, скрытый, и задаёт размер. Линза только открывает второй слой через clip-path. Она достаёт и до капсулы, хотя та лежит вне первого экрана: скрипт пишет ей те же переменные в её координатах.

Первый экран не ждёт анимацию. Имя есть в HTML и отрисовано с первого кадра, а сборка лишь стирает с него контур. Настоящие контролы лежат под слоем кода и принимают клики, а фокус с клавиатуры поднимает их поверх.

Проведите линзой по кадру

Движение

Смена страницы по косой логотипа

У каждого инструмента своя зона: наведение и фокус делает CSS, смену страницы ViewTransition, сцены на прокрутке GSAP, графы и линии canvas. Длительности и кривые живут в токенах, в коде эффекта чисел нет.

Первая смена страницы выглядела плохо. ViewTransition вокруг страницы даёт один слой на всю высоту, у главной это 34 678 px на 03.11.2026, и браузер сжимает его до экрана. Теперь снимается только экран, а новая страница приходит по линии 20,2°, наклону косой из логотипа. Шаг назад по капсуле идёт в обратную сторону.

В Firefox форма стояла в конце всего перехода: он не интерполирует зарегистрированную переменную @property на этих псевдоэлементах. Поэтому формы выписаны прямо в ключевых кадрах polygon. Снимок WebDriver этого не показывает, такой кадр берётся системным снимком окна настоящего браузера.

Снимается только экран, новая страница приходит по косой.

Проверки

14 проверок, одна команда

python3 scripts/check.py идёт перед каждым коммитом, и CI запускает её же на PR. Ниже настоящий прогон от 03.11.2026: 300 модульных тестов и 257 браузерных.

Браузерные проверки идут за настоящим Caddy с образом того же digest, что на сервере: адреса, заголовки, CSP, доступность в обоих режимах, сцены при уменьшенном движении и без JavaScript. Проверка должна уметь падать: каркас проверялся намеренным импортом через границу и текстом в разметке.

python3 scripts/check.py

Docsok0.1 s

Publicok0.2 s

Typesok2.3 s

Lintok13.7 s

Formatok2.2 s

Contentok0.0 s

Unitok1.0 s

Buildok11.3 s

Bundleok0.3 s

Addressesok15.5 s

SEOok9.1 s

A11yok35.8 s

Motionok210.2 s

Themesok36.8 s

14 из 14, 5 мин 39 с

Бюджет JavaScript первой загрузки, gzip, 03.11.2026
Главная245,2 КБ
Флоу192,2 КБ
Остальные страницы178,2 КБ

предел: 280 КБ

Что нашлось по дороге

  • 664494f

    На карточке Флоу симуляция графа заканчивается на разных координатах на arm64 и x64. На ноутбуке было зелено, в CI красно. Теперь хэш считает, из чего граф состоит, а не где он оказался.

  • da37183

    Тестовый конфиг отличался от продакшена, и гейт оставался зелёным, хотя на сервере ответ 404 назвал бы сервер и потерял HSTS. Теперь блок заголовков совпадает построчно, а гейт смотрит на заголовки 404.

  • 3e2a2bf

    Gecko случайно теряет тонкий диагональный градиент, и штриховка рассыпалась в Firefox. Теперь это SVG-маска, а тест не пускает такие градиенты в стили.

  • bbdeab3

    Тени брали цвет текста, и на тёмном фоне это было свечение. Теперь в тёмном режиме тень прозрачна.

Решения

Сначала решение, потом код

Всё, что дорого откатывать, записано до кода: контекст, варианты, выбор и условие пересмотра. Принятая запись не переписывается, другое решение это новая запись.

Статика выбрана не ради экономии: ресурсов на сервере хватало. Она быстрее, надёжнее и безопаснее для соседа по серверу, а анимации и так живут в браузере. Репозиторий открыт с первого коммита, потому что закрытое, попав в историю, остаётся в старых коммитах. Код под MIT, тексты и изображения под защитой, а GSAP остаётся зависимостью и в репозиторий не копируется.

Выкатка

Релиз это одно переключение

Сайт это папка файлов на общем сервере рядом с другим продуктом. Выкатка не трогает ни Caddy, ни контейнеры: новая сборка ложится отдельной папкой, а затем одной операцией переключается ссылка current. Простоя нет.

Хранятся три последних релиза, и --rollback возвращает прошлый за секунду. Ключа от сервера в GitHub нет: CI только проверяет, а выкатка идёт с ноутбука. Это осознанная плата: разработчик один, а сайт собирается заново из любого коммита.

  1. Отказ, если ветка не main, дерево грязное или main расходится с GitHub
  2. Все проверки и сборка
  3. Копия в releases/ в папку коммита, одинаковые файлы жёсткими ссылками
  4. Атомарное переключение ссылки current
  5. Проверка настоящего домена: коды, CSP, редиректы
  6. Не прошло: ссылка возвращается на прошлый релиз

ls portfolio/releases

664494f84cee← current

252bf73f2ffc

1a4994f45e0d

Коммиты в main по дням
3830.106131.102601.111002.11

Стадия

Что ещё не готово

Сайт в работе, и это видно по плану в репозитории.

  • Страница «Пора на пару» пока заготовка.
  • Снимки истории выше бюджета: 2,12 МБ всего при пределе 1,5 МБ, замер 01.11.2026.
  • Смена страниц и глава Флоу ещё не проверены на живом телефоне и в настоящем Safari.

Файлы и решения