П/ВИН

Как мы строили pashavin.ru: от формы до автодеплоя

·7 мин чтения
Как мы строили pashavin.ru: от формы до автодеплоя

Когда делаешь личный сайт с нуля, первая версия всегда «просто работает». Форма отправляет данные, проекты показываются, страницы грузятся. Но потом начинается настоящая разработка — когда хочется, чтобы всё это работало хорошо: уведомления приходили мгновенно, проекты подтягивались автоматически, а дашборд не лагал на мобильном. Именно через этот путь прошёл pashavin.ru — личный сайт с портфолио, блогом и разделом AI-разработок.

В этой статье — подробный разбор нескольких ключевых улучшений: интеграция Telegram-бота в форму обратной связи, переход от внешних ссылок к внутренним страницам проектов с AI-генерацией описаний, и оптимизация производительности дашборда all.pashavin.ru.

Telegram-бот в форме «Связаться»

Первая задача была простой по задумке, но важной по смыслу: сделать так, чтобы заявки с сайта приходили напрямую в Telegram. Никаких email-рассылок, никаких CRM на старте — только моментальное уведомление в личку.

Для этого подключили бота через переменные окружения в Dokploy — токен бота и chat ID. После деплоя форма начала отправлять сообщения в реальном времени.

Но на этом не остановились. Исходная форма содержала всего три поля: имя, телефон и тема обращения. Этого явно не хватало — человек не мог написать, что именно хочет обсудить. Добавили:

  • Email — необязательное поле для тех, кто предпочитает переписку
  • Telegram — чтобы сразу писать в мессенджер, минуя звонки
  • Сообщение — textarea для развёрнутого описания запроса
// Было: 3 поля
<Input name="name" required />
<Input name="phone" required />
<Select name="topic" />
 
// Стало: 6 полей
<Input name="name" required />
<Input name="phone" required />
<Input name="email" type="email" />
<Input name="telegram" placeholder="@username" />
<Select name="topic" />
<Textarea name="message" rows={4} placeholder="Расскажите подробнее..." />

API-роут обновили так, чтобы необязательные поля попадали в Telegram-сообщение только если заполнены — никаких пустых строк в уведомлении:

const lines = [
  `👤 ${name}`,
  `📞 ${phone}`,
  email ? `📧 ${email}` : null,
  telegram ? `✈️ ${telegram}` : null,
  `📌 ${topic}`,
  message ? `\n💬 ${message}` : null,
].filter(Boolean).join('\n')

Параллельно обновили блок «Обо мне»: добавили раздел «Экспертные темы» с шестью направлениями (автоматизация маркетинга, AI-контент, стратегии роста и другие), а фото сделали вертикальным с пропорцией 9/16 — так оно выглядит значительно лучше на мобильных устройствах.

Внутренние страницы проектов вместо внешних ссылок

Раздел «Разработки с помощью ИИ» изначально работал как таблица ссылок — каждый проект вёл на внешний GitHub или задеплоенный сервис. Это плохо по нескольким причинам: пользователь уходит с сайта, теряется контекст, нет места для маркетингового описания.

Решение — внутренние страницы-кейсы по паттерну /projects/[slug]. Каждый проект получает собственную страницу с полным описанием, стеком технологий, скриншотами и ключевыми фичами.

Но главная проблема была в другом: проекты были захардкожены в TypeScript-файлах. Новый репозиторий на GitHub — не значит новый проект на сайте. Нужно было автоматизировать.

Автогенерация из GitHub

Написали скрипт generate-projects.ts, который:

  1. Вызывает gh repo list и получает актуальный список репозиториев
  2. Для известных проектов (13 штук) берёт вручную написанные маркетинговые описания
  3. Для новых репозиториев — читает код и README, затем генерирует описание через Claude API
  4. Кэширует результат, чтобы не тратить токены при каждом деплое
// Упрощённая логика скрипта
const repos = await getGitHubRepos()
 
for (const repo of repos) {
  if (SKIP_REPOS.includes(repo.name)) continue
  
  if (KNOWN_PROJECTS[repo.name]) {
    // Используем вручную написанное описание
    projects.push(KNOWN_PROJECTS[repo.name])
  } else {
    // Генерируем через Claude API с few-shot примерами
    const description = await generateWithClaude(repo, KNOWN_PROJECTS)
    projects.push({ ...repo, ...description })
  }
}

Ключевая идея с few-shot: в промпт передаём 13 уже написанных проектов как примеры — и Claude генерирует новые описания точно в том же стиле и структуре. Никакого разнобоя в тоне.

Для утилитарных репозиториев (документация, задачи, инфра) добавили список SKIP_REPOS — они не попадают в портфолио.

Пагинация «загрузить ещё»

Показывать сразу все 20+ проектов — перебор. Сделали отображение по 6 штук с кнопкой «Загрузить ещё», которая добавляет ещё 6 при каждом нажатии:

const [visible, setVisible] = useState(6)
 
const loadMore = () => setVisible(prev => prev + 6)
 
return (
  <>
    {projects.slice(0, visible).map(p => <ProjectCard key={p.slug} {...p} />)}
    {visible < projects.length && (
      <Button onClick={loadMore}>Загрузить ещё</Button>
    )}
  </>
)

Просто, без лишней магии. Пользователь сам решает, нужно ли ему листать дальше.

Дашборд all.pashavin.ru: от лагов к плавности

Отдельная история — дашборд all.pashavin.ru, который агрегирует проекты и инструменты. После деплоя обновлённого дизайна (pixel-perfect по HTML-макету) появилась серьёзная проблема: сайт ощутимо лагал.

Диагностика

Ревью кода сразу показало виновника — SVG-шум через feTurbulence в CSS:

/* Было — убийца производительности */
.bg-noise::before {
  background-image: url("data:image/svg+xml,...feTurbulence numOctaves='3'...");
  mix-blend-mode: overlay;
  z-index: 9998;
}

feTurbulence с numOctaves=3 — это процедурный алгоритм генерации шума Перлина, который CPU пересчитывает на каждый кадр для fullscreen-элемента. В связке с mix-blend-mode и высоким z-index браузер не мог закэшировать этот слой — каждый скролл и анимация вызывали полный перерасчёт.

Дополнительные проблемы:

  • backdrop-blur-md на фиксированном навбаре — GPU-тяжёлый эффект без will-change
  • Четыре CSS-свойства в transition на карточках вместо двух ключевых
  • letter-spacing: 0.3em вместо 0.1em из дизайна (попутная правка)

Решение

Главный фикс — заменить процедурный шум на статическую PNG-текстуру:

/* Стало — GPU просто тайлит текстуру */
.bg-noise::before {
  background-image: url('/noise.png'); /* 16 КБ, 100x100px, repeat */
  mix-blend-mode: overlay;
  will-change: opacity;
  z-index: 9998;
}

Статическая текстура 16 КБ загружается один раз и тайлится GPU без каких-либо вычислений. Разница ощутима мгновенно.

Дополнительные оптимизации:

/* will-change подсказывает браузеру вынести элемент на отдельный слой */
.bg-grid {
  will-change: opacity;
  transform: translateZ(0); /* форсируем GPU-композитинг */
}
 
/* Сокращаем transition с 4 свойств до 2 */
.glass-card {
  transition: border-color 200ms ease, box-shadow 200ms ease;
  /* было: color, background-color, border-color, box-shadow */
}

После деплоя лаги исчезли. Прокрутка стала плавной, анимации — без рывков.

Пайплайн деплоя

Все изменения деплоятся через GitHub Actions → GHCR → SSH-деплой на VPS. VPS никогда не собирает образ сам — только подтягивает готовый из реестра. Это сократило время деплоя с 20-30 минут до 1-3 минут.

Типичный флоу:

git push origin master
  → GitHub Actions: build Docker image
  → Push to GHCR
  → SSH на VPS: docker pull + service update
  → Готово за ~90 секунд

Авто-синк скрипт коммитит и пушит изменения автоматически — в логах часто видно auto-sync как автора коммита. Это удобно при парной работе с Claude Code: изменения не теряются между сессиями.

Технологический стек

Проект построен на Next.js 14 с App Router и TypeScript. UI-компоненты из shadcn/ui — в частности, Textarea был добавлен именно в этом цикле разработки. Деплой через Dokploy — self-hosted PaaS на базе Docker Compose.

Для генерации маркетинговых описаний используется Claude API (Anthropic) с few-shot prompting — это позволяет поддерживать единый стиль описаний без ручного редактирования каждого нового проекта.

Выводы

Главный урок этого цикла разработки — итеративность важнее совершенства с первого раза. Форма начала с трёх полей и стала шестипольной не потому, что мы изначально плохо спроектировали, а потому что пользовательский сценарий прояснился в процессе. Telegram-интеграция работала, но не давала достаточно контекста о запросе. Добавили поля — стало лучше.

Автоматизация генерации проектов через GitHub API + Claude — хороший пример того, как AI-инструменты меняют рутинные задачи. Вместо того чтобы вручную писать маркетинговое описание для каждого нового репозитория, скрипт делает это автоматически, обучаясь на примерах уже написанных текстов. Few-shot prompting здесь работает значительно лучше, чем zero-shot — модель точно воспроизводит нужный тон и структуру.

Проблема с производительностью дашборда — классическая ловушка при pixel-perfect переносе HTML-макетов в продакшн. Дизайнер добавляет красивый шум-эффект через SVG-фильтр, в браузере это выглядит отлично. Но feTurbulence в статичном HTML рендерится один раз при загрузке страницы, а в React-приложении с анимациями — на каждый кадр. Замена на статическую текстуру убирает лаги полностью при нулевой потере визуального качества.

И наконец — will-change и transform: translateZ(0) это не магия, а инструмент коммуникации с браузером. Явно указывая, какие элементы будут анимированы, вы позволяете GPU заранее выделить отдельный compositing layer. Это особенно важно для фиксированных элементов (навбар с blur) и fullscreen-оверлеев (noise texture). Добавляйте эти свойства осознанно — на реально анимируемые элементы, иначе вы просто занимаете лишнюю GPU-память.

Паша Вин
Паша Вин

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.