Как сделан этот сайт: Astro, один файл токенов и агент, который идёт по брифу

Статический сайт на Astro с нативным CSS, сферой на three.js и курсорным полем. Что в стеке, как я работал с Claude Code и как довёл LCP с 2,6 до 1,66 секунды.

Знак hydroburner из кристаллов аметиста на чёрном фоне

Этот сайт это статический проект на Astro с нативным CSS и несколькими маленькими библиотеками. Без React, без Tailwind, без GSAP. Единственная тяжёлая зависимость это three.js, и нужна она только для сферы в боковой панели.

Я перенёс его с Framer на собственный код и заодно обновил. Арт-дирекшн и бриф мои. Основную часть кода писал Claude Code, сессию за сессией.

Портфолио у меня раньше жило на Framer. Меня оно устраивало, но гибкости не хватало, и многое из задуманного там сделать было нельзя. Когда я освоил Claude Code, я пересобрал сайт на собственном стеке. Подписки нет, я контролирую каждый слой, а заодно хотел нормально разобраться с three.js. Ещё я хотел живой, отзывчивый сайт, который вовлекает, отдельный проект, а не галерею картинок и историй.

Сначала бриф, потом сессии

До первой строки кода я написал бриф: стек, арт-дирекшн, значения анимации, страница за страницей, цели по производительности. Потом разбил работу на 12 сессий. У каждой есть чёткая цель, и она добавляет строку в журнал прогресса. В журнале лежат и открытые вопросы ко мне.

Правила, которые агент не должен нарушать, лежат в файле CLAUDE.md в репозитории:

  • оранжевый только в сфере, курсорном поле, прелоадере и выделении текста;
  • каждая длительность, кривая и пружина берётся из токенов, никаких магических 0.35s;
  • у каждого интерактивного элемента есть hover, press и focus, реакция за 100 мс или быстрее;
  • цифры кейса живут только в его meta.yml;
  • без выдуманного контента, только реальные факты, которые не публикуются без живой модерации.

Один источник токенов

Цвета, шрифты, отступы, длительности и пружины лежат в JSON-файлах, которые выгружаются из моей дизайн-системы. Скрипт превращает их в CSS custom properties перед каждым запуском dev и сборкой, поэтому значение печатается один раз. Значения анимации лежат в одном файле, который читают и TypeScript-модуль анимации, и скрипт токенов.

:root {
  --dur-fast: 200ms;
  --ease-hb-out: cubic-bezier(0.12, 0.23, 0.5, 1);
}

Типографика на clamp(), пружины экспортируются как CSS linear(). CSS разложен по слоям от reset до motion, а слой motion работает только при prefers-reduced-motion: no-preference.

Монохром и один акцент

В базе сайт монохромный, чтобы на первом плане был контент. Оранжевый работает как акцент и добавляет «жизни»: он появляется в поле глифов под курсором, в частицах сферы, в прелоадере и в выделении текста.

Стек и то, что не взял

  • Astro, статическая сборка, коллекции контента со схемами. Каждый кейс, проект и заметка это папка с meta.yml и одним текстовым файлом на язык. Сборка падает, если у кейса или проекта нет пары языков.
  • Нативный CSS: @layer, custom properties, вложенность, container queries.
  • Motion, vanilla-сборка движка, на котором стоит Framer Motion, поэтому пружины и кривые совпадают со старым сайтом.
  • Lenis для плавного скролла длительностью 0,8 с. На тач-экранах и при reduced motion он выключен.
  • three.js для одной сцены.
  • Playwright для тестов.
Четыре шага слева направо: docs/ds/tokens.json, scripts/build-tokens.mjs, src/styles/tokens.css, компоненты
Один файл токенов, печатается один раз

Три лаборатории, чтобы настраивать глазами

Самое сложное в работе с ИИ это финальная полировка, особенно для дизайнера, которому важно, как вещь выглядит и ощущается. Поэтому вместе с Claude я собрал три лаборатории: для курсора, для прелоадера и для 3D-сферы. Каждое значение я настроил на глаз, отдал результат агенту, и он внедрил ровно так. Обычный вайб-кодинг заканчивается там, где закончил агент. У меня есть fine tune, который я полностью контролирую, поэтому готовый продукт выглядит так, как нужно мне.

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

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

В лаборатории сферы 41 фигура, она задаёт фигуру для каждой страницы, кейса и проекта, а ещё наклон, вращение, число и размер частиц, физику курсора, цвета по темам и скорость морфинга. В лаборатории курсора 29 типов элементов от кнопок до слайдов, и каждому типу достаётся одна из семи реакций: none, halo, orbit, spotlight, underline, corners или ripple. Лаборатория прелоадера задаёт ритм 9 ударов, изинги и финал.

Каждая лаборатория экспортирует JSON-файл, а скрипт его проверяет и пишет конфиг, который читает сайт. Так лаборатория остаётся источником правды, и конфиг руками никто не правит. В лаборатории курсора есть ещё закреплённый режим: курсор сам встаёт на выбранный элемент, поэтому ползунки можно двигать, не держа там мышь.

Четыре шага слева направо: три лаборатории, настройка ползунками с живым просмотром, экспорт в JSON и один скрипт, сайт с теми же значениями
От ползунка в лаборатории до живого сайта
Окно лаборатории курсора: образцы элементов слева, справа список элементов с режимом у каждого типа, курсор закреплён на кнопке Book a call, вокруг неё оранжевые глифы
Лаборатория курсора: типы элементов слева, настройки справа

Сфера и курсорное поле

Сфера по умолчанию состоит из 1600 частиц на three.js Points с шейдерным спрайтом. Физика считается на CPU, а рендер спит, когда ничего не движется и когда сфера вне экрана. three.js весит около 131 КБ gzip и грузится динамическим import() только когда сфера видна. Запасной вариант это статичный кадр AVIF, снятый с той же сцены.

Пять фигур из частиц в лаборатории сферы на тёмном фоне: сфера, тор, двойная спираль, знак hydroburner, куб на ребре
Несколько из 41 фигуры сферы

Курсорное поле это порт компонента Framer на Canvas 2D, без библиотек. Его цикл засыпает, когда курсор стоит больше 0,7 секунды и все точки погасли. След лежит под стеклянными островами. Свечение, волны и орбиты наведённого элемента рисуются на холстах поверх контента, потому что под стеклом они выходили приглушёнными и размытыми.

Навигация, которая сохраняет энергию

Сначала я использовал Astro ClientRouter, а холст поля и сфера переживали переходы между страницами, чтобы не перезапускаться. Позже я заменил компонент его мета-тегами и собственным router-init.ts, чтобы код роутера грузился после первой отрисовки. Переход страницы занимает 0,2 с на уход и 0,2 с на появление, а обложка кейса перетекает в hero следующей страницы.

Производительность: LCP с 2,6 до 1,66 секунды

Первый замер Lighthouse mobile дал LCP 2,6 с на главной и 2,7–3,0 с на кейсе при цели 1,8 с. Причины: 20+ JavaScript-модулей, кириллический шрифт, который грузился заранее на английской версии, скрытый вариант картинки для светлой или тёмной темы, который грузился eager, и отдельные запросы за заглушками картинок.

Что исправил:

  • весь клиентский код стартует после события load и кадра LCP (boot.ts);
  • файлы шрифтов сделаны сабсетами: латиница 33,8 → 23,1 КБ, кириллица 15,9 → 10,9 КБ;
  • картинка с парой для светлой и тёмной темы грузит только нужную тему;
  • стрелки сделаны SVG, поэтому отдельный сабсет шрифта для символов больше не нужен.

10 октября 2026 года у главной LCP был 1,66 с, у страницы кейса около 2,0 с (Lighthouse mobile с симулированным троттлингом, медиана из 3 запусков, production preview). Performance был 98–99, а accessibility, best practices и SEO по 100. CLS и TBT были 0.

Шапка отчёта Lighthouse mobile для главной: Performance, Accessibility, Best Practices и SEO по 100, FCP 1,1 с, LCP 1,7 с, TBT 0 мс, CLS 0
Lighthouse mobile на локальной production-сборке главной

Прелоадер

При первом визите за сессию собирается знак, затем 9 ударов переключают кадры знака в разных материалах, а финал зумится в центральный шар и садится в логотип в навигации. Любой ввод пропускает его.

В первой версии кадры лежали в DOM как картинки, и браузер считал их кандидатами в LCP: 3 с на троттлинг-профиле телефона против 0,17 с без прелоадера. Теперь модуль отдаёт кадры как canvas, и LCP снова 0,18–0,32 с.

Девять кадров прелоадера подряд: знак в глубокой воде, грозовых облаках, магме, мхе, янтаре, листе с росой, розовой соли, коралле и лазурите
Кадры прелоадера, по одному на удар

Что пошло не так

Кнопки шапки ломались после переходов между страницами: обе иконки темы показывались слева. Два компонента объявляли @layer components в собственных стилях. После перехода Astro вставлял эти стили заново, строка с порядком слоёв для всего сайта оказывалась после них, и components попадал ниже reset. Простое правило svg { display: block } из reset начинало перебивать правило, которое прячет луну или солнце. Исправление: отдельный inline-стиль с порядком слоёв сразу после скрипта темы, который роутер оставляет на месте. Теперь на это есть тест.

Сфера перестала работать на моей машине по другой причине. npm run build писал в тот же node_modules/.vite, что и запущенный dev-сервер, и ломал его оптимизированные зависимости, поэтому three.js отдавал 504 и оставалась только статичная картинка. Теперь у сборки свой каталог кеша.

На что обратить внимание

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

Следующая заметка

Все заметки
18 октября 2026 г. · ИИ и агенты7 мест, где я использую ИИ-агентов в дизайне, и где я останавливаюсь