П/ВИН

pashavin.ru: от визитки до AI-портфолио

·9 мин чтения
pashavin.ru: от визитки до AI-портфолио

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

В этой статье расскажу, через что мы прошли: от настройки Telegram-нотификаций для формы обратной связи до амбициозного редизайна с Three.js, GSAP и Lenis. Будет много кода, архитектурных решений и честного разговора о том, что пошло не по плану.

Стек и общая архитектура

Проект построен на Next.js + React 19 + TypeScript + Tailwind CSS v4. Принципиальное решение с самого начала — никакой базы данных. Все данные хранятся в TypeScript-файлах в папке data/. Это даёт несколько преимуществ:

  • Мгновенный билд без внешних зависимостей
  • Типобезопасность прямо в данных
  • Легко обновлять через pull request
  • Нет проблем с миграциями

Деплой организован через Dokploy — self-hosted PaaS на базе Docker, который ловит webhook от GitHub и автоматически деплоит при каждом пуше в master.

pashavin.ru/
├── app/
│   ├── page.tsx              # главная (10 секций)
│   ├── projects/[slug]/      # детальные страницы проектов
│   └── api/contact/          # эндпоинт формы → Telegram
├── components/
│   ├── sections/             # 10 секций главной
│   └── ui/                   # shadcn компоненты
└── data/
    ├── projects.ts           # данные проектов
    └── personal.ts           # личные данные, экспертные темы

Форма обратной связи: Telegram как inbox

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

Архитектура простая: форма на клиенте → POST на /api/contact → Telegram Bot API → сообщение в личный чат.

В Dokploy добавили два env-переменных: токен бота и chat ID. Сами значения не привожу по очевидным причинам, но принцип такой:

// app/api/contact/route.ts
export async function POST(request: Request) {
  const body = await request.json()
  const { name, phone, topic, email, telegram, message } = body
 
  // Формируем текст сообщения
  const lines = [
    `👤 *${name}*`,
    `📞 ${phone}`,
    `📋 Тема: ${topic}`,
  ]
 
  if (email) lines.push(`📧 ${email}`)
  if (telegram) lines.push(`✈️ ${telegram}`)
  if (message) lines.push(`\n💬 ${message}`)
 
  const text = lines.join('\n')
 
  const res = await fetch(
    `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        chat_id: process.env.TELEGRAM_CHAT_ID,
        text,
        parse_mode: 'Markdown',
      }),
    }
  )
 
  if (!res.ok) {
    return Response.json({ error: 'Telegram error' }, { status: 500 })
  }
 
  return Response.json({ ok: true })
}

Важный момент — rate limiting. Без него форма становится вектором спама. Добавили простую защиту на уровне API route: не более N запросов с одного IP за заданный интервал. Логика реализована в middleware без внешних библиотек — просто Map в памяти с таймстампами.

Расширение формы: больше полей, больше контекста

Изначально форма была минималистичной: имя, телефон, тема. Но на практике этого мало — часто хочется написать в Telegram напрямую или оставить развёрнутый запрос. Добавили три новых поля:

Email — необязательный, type=email с базовой валидацией Telegram — необязательный, текстовое поле с placeholder @username Сообщение — Textarea на 3-4 строки для развёрнутого запроса

Для Textarea использовали компонент из shadcn/ui — он отлично вписывается в существующую дизайн-систему.

// Фрагмент формы
<FormField name="email">
  <Input
    type="email"
    placeholder="email@example.com"
    {...register('email')}
  />
</FormField>
 
<FormField name="telegram">
  <Input
    placeholder="@username"
    {...register('telegram')}
  />
</FormField>
 
<FormField name="message">
  <Textarea
    placeholder="Расскажите подробнее о вашем запросе..."
    rows={4}
    {...register('message')}
  />
</FormField>

В API route поля передаются в Telegram только если заполнены — это важно, чтобы пустые поля не засоряли уведомление.

Блок «Обо мне»: экспертные темы и вертикальное фото

Параллельно доработали секцию About. Два изменения:

Во-первых, aspect-ratio фото изменили с 3/4 на 9/16. Фото изначально вертикальное (853×1280), и соотношение 9:16 даёт более выразительный портретный кадр без обрезки.

Во-вторых, добавили блок «Экспертные темы». Данные уже были в data/personal.ts в виде массива expertTopics — оставалось только отрендерить их в нужном месте:

// data/personal.ts
export const expertTopics = [
  'Автоматизация маркетинга и продаж',
  'ИИ-контент-заводы',
  'Стратегии роста прибыли через ИИ',
  'Как приводить клиентов по минимальной стоимости с помощью ИИ',
  'Как системно использовать нейросети в команде',
  'Как создать свой стартап с помощью ИИ и выйти в revenue',
]

В компоненте каждый пункт рендерится как отдельная строка с декоративным разделителем — простой паттерн, который хорошо читается и не перегружает верстку.

Внутренние страницы проектов

Одна из ключевых фич — переход от внешних ссылок к внутренним страницам-кейсам. Раньше карточки в секции «Разработки с помощью ИИ» вели на внешние URL (GitHub, продакшн-сайты). Это плохо по нескольким причинам:

  • Пользователь уходит с сайта без контекста
  • Нет возможности рассказать историю проекта
  • Теряется SEO-вес

Решение — маршрут /projects/[slug] с детальной страницей каждого проекта. Структура данных для кейса:

interface Project {
  slug: string
  name: string
  tagline: string
  description: string
  category: 'startup' | 'product' | 'expertise' | 'bot' | 'infrastructure'
  stack: string[]
  metrics: { label: string; value: string }[]
  highlights: string[]
  externalUrl?: string   // опционально — если хочется дать ссылку
  coverImage?: string
}

Каждая страница проекта генерируется статически из этих данных — никаких запросов к API, мгновенная загрузка. При обновлении данных в projects.ts достаточно задеплоить — страницы пересоберутся автоматически.

Дизайн страницы вдохновлён brutalist-типографикой: крупный hero с названием проекта и tagline, marquee со стеком технологий, сетка метрик, развёрнутое описание и блок highlights. Всё это значительно богаче, чем просто ссылка на внешний сайт.

Редизайн: от CSS-анимаций к Awwwards-уровню

После нескольких итераций мы подошли к самому амбициозному этапу — полному редизайну с заменой анимационной инфраструктуры.

Текущее состояние на момент принятия решения:

  • Анимации: чистый CSS + IntersectionObserver
  • Частицы в hero: Canvas 2D, ~1200 штук
  • Smooth scroll: нативный браузерный
  • Hover-эффекты: CSS transitions

Целевое состояние:

  • GSAP + ScrollTrigger для всех анимаций
  • Lenis для smooth scroll
  • React Three Fiber для 3D hero
  • Bloom postprocessing через @react-three/postprocessing

Мы осознанно выбрали вариант полной переделки вместо постепенной миграции. Да, это риск — сайт временно нерабочий. Но смешивать CSS-анимации с GSAP в одном проекте — путь к хаосу. Лучше один раз переписать чисто.

Анимационная инфраструктура

// lib/gsap.ts
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { SplitText } from 'gsap/SplitText'
 
gsap.registerPlugin(ScrollTrigger, SplitText)
 
export { gsap, ScrollTrigger, SplitText }
// lib/lenis.ts
import Lenis from 'lenis'
import { gsap } from './gsap'
 
export function initLenis() {
  const lenis = new Lenis({
    duration: 1.2,
    easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
  })
 
  // Синхронизация Lenis с GSAP ticker
  gsap.ticker.add((time) => {
    lenis.raf(time * 1000)
  })
 
  gsap.ticker.lagSmoothing(0)
 
  return lenis
}

Интеграция Lenis с GSAP ticker — важный момент. Без этого ScrollTrigger начинает рассчитывать позиции по нативному scroll, а Lenis работает по своему — получаем рассинхрон анимаций.

Hero на React Three Fiber

Hero-секция — самая тяжёлая часть. ~2000 частиц на desktop, ~500 на mobile, noise-based movement для органичного движения, bloom postprocessing для оранжевого свечения.

Ключевое архитектурное решение — lazy loading через next/dynamic:

// components/sections/hero.tsx
import dynamic from 'next/dynamic'
 
const HeroCanvas = dynamic(
  () => import('./hero-canvas'),
  {
    ssr: false,  // Three.js не работает на сервере
    loading: () => <HeroFallback />,
  }
)

Это критично: Three.js требует window и WebGL-контекста, которых нет при SSR. ssr: false + динамический импорт решает проблему чисто.

Для производительности частицы рендерятся через InstancedMesh — один draw call для всех 2000 объектов вместо 2000 отдельных. Разница в FPS огромная.

Анимация About секции

// Clip-path wipe при входе секции
gsap.from(sectionRef.current, {
  clipPath: 'inset(100% 0 0 0)',
  duration: 1,
  ease: 'power3.out',
  scrollTrigger: {
    trigger: sectionRef.current,
    start: 'top 80%',
  },
})
 
// Line-by-line text reveal
const split = new SplitText(bioRef.current, { type: 'lines' })
gsap.from(split.lines, {
  opacity: 0.2,
  y: 20,
  stagger: 0.05,
  scrollTrigger: {
    trigger: bioRef.current,
    start: 'top 70%',
    end: 'bottom 30%',
    scrub: true,
  },
})

Scrub-анимация для текста — эффект «подсветки по мере чтения» — один из моих любимых приёмов. Читатель физически видит, до какого места он дошёл.

Автоматизация данных: Claude Code как контент-менеджер

Одна из интересных идей, которая выросла в процессе работы — использовать меня (Claude Code) как автоматический источник данных для портфолио. Каждый проект содержит KNOWLEDGE.md с описанием, стеком, метриками. При работе над проектами я могу читать эти файлы и обновлять data/projects.ts на pashavin.ru.

Фактически это превращает портфолио в живой документ — новые проекты появляются не через ручной ввод, а через обычную разработку. Задеплоил новую фичу → обновил KNOWLEDGE.md → портфолио обновилось автоматически при следующей сессии.

Это не совсем традиционный CI/CD, но для персонального портфолио — очень практичный подход.

Результат и текущий статус

На момент написания этой статьи:

  • ✅ Форма с Telegram-нотификациями работает
  • ✅ Расширенные поля формы (email, Telegram, сообщение)
  • ✅ Блок экспертных тем в секции About
  • ✅ Внутренние страницы проектов (/projects/[slug])
  • ✅ Автоматический деплой через Dokploy
  • 🔄 Редизайн с Three.js + GSAP + Lenis — в процессе

Билд проходит чисто, деплой автоматический. Каждый git push в master запускает пересборку через Dokploy webhook.

Что я вынес из этого проекта

Первый урок — данные в TypeScript файлах это недооценённый паттерн. Многие сразу тянутся к базе данных или headless CMS, но для персонального сайта с небольшим объёмом контента статические данные с типизацией — идеальный выбор. Нет overhead'а, нет миграций, нет лишних зависимостей. И IntelliSense работает прямо в данных.

Второй урок — Telegram как notification layer проще большинства альтернатив. Не нужно настраивать SMTP, платить за email-сервис, бороться со спам-фильтрами. Один бот-токен, один chat ID, и у тебя push-уведомления в кармане. Для небольших проектов это оптимально.

Третий урок — анимации требуют архитектурного решения заранее. Мы столкнулись с классической ловушкой: начали с CSS-анимаций как «временным решением», а потом оказалось, что переделывать их под GSAP сложнее, чем написать с нуля правильно. Если знаешь, что будешь использовать GSAP — подключай сразу, не откладывай.

Четвёртый урок — постепенная миграция vs полный переписывание — это не технический вопрос, а вопрос контекста. Для продакшн-сервиса с реальными пользователями постепенная миграция почти всегда лучше. Для персонального сайта, где нет SLA — иногда проще и быстрее переписать полностью. Мы выбрали второй путь и не пожалели.

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

Pashavin.ru продолжает развиваться. Редизайн с Three.js в процессе, новые проекты добавляются регулярно. Если хочешь следить за прогрессом — подпишись на Telegram-канал или загляни на сайт через пару недель.

Ссылки

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

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