SEO-добивка. Canonical и hreflang теперь в ‹head› каждой страницы — раньше альтернативы локалей декларировались только в sitemap; у блога и проектов появились meta-описания, /uikit впервые получил metadata. JSON-LD расширен: CreativeWork на страницах проектов со ссылками на репозиторий и live-сайт в sameAs, BreadcrumbList на статьях и проектах, Person на «Контактах». Попутно исправлен баг: BlogPosting и Course строили URL с префиксом локали (/ru/blog/…), которых при localePrefix: never не существует.
Новости
Журнал обновлений сайта: что было добавлено, починено и улучшено по версиям.
Единая лента активности. Главная вместо отдельных блоков статей и проектов показывает хронологический таймлайн: статьи, проекты и релизы в одном потоке — вертикальный рельс с точками, типизированные карточки, у релизов акцентный бейдж версии и первое предложение changelog-текста заголовком. Новая страница /timeline — полная лента без квот; соседние релизы одного дня схлопываются в запись с диапазоном версий (v1.4.0 – v1.5.2) и счётчиком «и ещё N релизов». Разделы «Лента» и UI Kit добавлены в шапку и палитру ⌘K; навигация стала трёхступенчатой: бургер до 880px, полное меню без поиска до 1160px, дальше — с окошком «Поиск ⌘K», и пункты больше не переносятся на вторую строку.
Песочница компонентов в UI Kit. Вместо статичной витрины — интерактивный playground: табы по шести компонентам (Button, Badge, Input, Textarea, ProgressBar, DotIcon), контролы пропсов по типам и живой JSX-сниппет, который собирается из текущих значений и копируется одной кнопкой; настройки компонентов переживают переключение табов. В дизайн-системе появился собственный Select — кастомный выпадающий список с акцентной точкой-маркером выбранного пункта, закрытием по Escape и клику мимо. Высоты карточки зафиксированы под самый объёмный набор контролов, grid-колонки получили min-width: 0 — песочница не дёргается при переключении и не распирается на мобиле.
Точечная иконографика. Все иконки сайта переведены на единую систему DotIcon — круги радиуса 5 на сетке с шагом 10, инлайновый SVG через currentColor: поиск в шапке, все пункты командной палитры, каналы на «Контактах», баннер 404 и переключатели темы/акцента. Логотип «sandwor» пересобран из 92 точек, извлечённых парсером path-данных из исходного SVG, и анимирован: клик рассыпает и собирает буквы заново со случайными задержками точек (рандом генерируется только после гидрации, prefers-reduced-motion уважается). Пять svg-файлов из public/img удалены — иконки больше не грузятся отдельными запросами, а иконка темы перестала мигать после гидрации: обе версии рендерятся на сервере, видимую выбирает CSS по data-theme.
Страница «Контакты» полностью переработана. Hero с акцентным CTA «Написать на почту», иконки-каналы (Telegram, GitHub, Instagram, VK) на инлайновых SVG через currentColor и сплит-раскладка: слева интро, справа форма на подложке-карточке фиксированной высоты с экраном успеха (галочка + «отправить ещё»). Форма ожила — сообщения уходят в Telegram-бота через серверный роут /api/contact (Bot API sendMessage, экранирование HTML, валидация полей). Антиспам без CAPTCHA: honeypot-поле, тайм-трап на слишком быструю отправку и лимит ссылок, все срабатывания тихо отбрасываются. Попутно починен matcher middleware: next-intl перехватывал /api/* и ронял все API-роуты в 404.
Раздел News вместо Notes. Журнал релизов с semver-версионированием. Сущность Note расширена опциональным полем version, NoteItem рендерит бейдж справа от даты. 301-редирект /notes → /news сохраняет SEO-вес старой страницы. Раздел добавлен в основную навигацию через NAV_LINKS — раньше /notes был только по прямой ссылке.
Bundle analyzer и бюджет JS в CI. @next/bundle-analyzer даёт интерактивный treemap через `npm run analyze`. Свой 50-строчный скрипт scripts/check-bundle-size.mjs суммирует gzipped-размер всех чанков в .next/static/chunks и фейлит CI при превышении 350 KB. Текущее использование — 273 KB (78% бюджета).
Engineering hygiene. Pre-commit hook через husky + lint-staged: ESLint --fix и Prettier --write на staged-файлах, плюс полный tsc --noEmit. GitHub Actions CI: lint, typecheck, jest, build на каждый push в main и каждый PR. concurrency cancel-in-progress, кеш npm, HUSKY=0 для CI. Удалены мёртвые MOCK_POSTS из BlogPage/HomePage.
View Transitions API и анимированный бургер. Плавный crossfade со slide-up между страницами через next-view-transitions — нативный браузерный API, ~2 KB gzip, ноль JS-overhead. Все next/link заменены на патченый Link библиотеки. Бургер на мобиле: 3 полосы морфят в крест × за 240ms с cubic-bezier easing, сама кнопка обособлена фоном/бордером. prefers-reduced-motion respect.
Палитра ⌘K расширена. Поиск по статьям через новый эндпоинт /api/posts/list — slim-проекция (slug, title, tag) с кешем s-maxage=3600. Lazy fetch на первом открытии — нулевые запросы до клика, потом кешируется в state. Видимая «Поиск ⌘K» кнопка в шапке для discoverability. Декаплинг через CustomEvent('palette:open') — любой компонент может открыть палитру одной строкой.
Мобильная вёрстка курса. CourseTopbar стыкуется под Header'ом (top: 56px), не торчит из-под него. На мобиле — две flex-строки: название курса + Reset сверху, progress bar снизу. Сайдбар уроков переведён в колонку — раньше 2-колоночная сетка ломала «Документы и Excel» переносом на 2 строки. Убран дубль «Урок N из M» — остался только в LessonContent.
Sticky reading progress bar. Полоса сверху статьи появляется после 240px скролла: «N мин чтения · дата» + 2px полоска прогресса в --accent цвете, scaleX-анимация по скроллу документа. Scroll-handler throttled через requestAnimationFrame — батчится react'ом, ~60fps без long tasks. Высота 56px ровно под Header'ом, prefers-reduced-motion отключает анимацию.
Командная палитра ⌘K. Своя реализация без cmdk (~140 строк): substring-фильтр по label + keywords, ↑↓ Enter навигация, фокус-менеджмент, click-outside, Esc. Команды: 7 навигационных пунктов, тема (light/dark/system), смена локали, цикл accent. По пути вынесены общие хелперы useLocaleSwitch и cycleAccent — теперь и тогглы шапки, и палитра используют одну логику.
Курс: reset прогресса и deep-link на урок. Кнопка «Начать заново» в топбаре с подтверждением через window.confirm, чистит localStorage и query-параметр. URL `?lesson=N` (1-индексация) открывает указанный урок при заходе — игнорирует lock, чтобы можно было расшарить любую страницу. «Поделиться уроком» — кнопка копирования URL в буфер через navigator.clipboard, ✓-подсветка на 2 сек.
Похожие посты под статьёй. Алгоритм getRelatedPosts(slug, limit): сначала посты с тем же тегом (новейшие первыми), потом добиваем самыми свежими из остальных, пока не наберём limit. Реюзит существующий PostList виджет с разделителем-заголовком. Скрывается, если на блоге всего один пост.
SEO: реальный sitemap + hreflang. До правки sitemap.xml отдавал MOCK_POSTS — Google ходил на несуществующие URL, индексация ломалась. Теперь — реальные посты через getTelegramPosts, проекты из projects.json, курсы из COURSES_REGISTRY. Hreflang-alternates для ru/en на каждой записи декларируют мультиязычность. Правильные priority и changeFrequency.
RSS/Atom фид. Эндпоинт /feed.xml отдаёт валидный Atom 1.0 — переиспользует getTelegramPosts, XML-эскейп вручную (без либ). В ‹head› каждой страницы — тег ‹link rel="alternate" type="application/atom+xml"› для автодискавери (Feedly, Inoreader, Chrome-расширения подсвечиваются). Ссылка «RSS» в футере. Edge-кеш s-maxage=3600, stale-while-revalidate=86400.
Прогресс курса в баннере на главной. Возвращающийся пользователь видит «Продолжить (3 из 6) →» вместо «Начать курс», точки уроков подсвечены ✓ по выполненным. localStorage-ключ и loadCourseState вынесены в общий lib, хук useCourseProgress читает их в client-overlay поверх server-баннера. SSR рендерит дефолт, чтобы не было hydration mismatch.
SEO-фундамент: OG-картинки и JSON-LD. Edge-роут /api/og рендерит 1200×630 PNG через next/og (он же @vercel/og) — динамическая обложка с заголовком, тегом и брендом. Подключено через generateMetadata в openGraph.images + twitter card summary_large_image. JSON-LD структурированные данные: Person + WebSite на главной, BlogPosting на каждой статье, Course на курсе — даёт rich-сниппеты в Google и контекст для ИИ-краулеров.
AI-курс «ИИ — твой новый помощник». 6 интерактивных уроков с теорией, чат-примером и практическим заданием. State-машина из трёх экранов (landing → course → completion) + персистенция в localStorage. Регистрация по имени для персонализации завершения. PDF-шпаргалка по результатам — через window.print() с print-styles. CourseSidebar с unlocking-логикой по выполненным урокам.
Раздел «Проекты». Карточки с локализованными названиями и описаниями (ru/en), тегами, опциональными ссылками на репозиторий и live-сайт. Источник — entities/project с JSON-данными. Отдельная страница каждого проекта (/projects/[slug]) с подробным описанием и обложкой.
Блог через Telegram-канал. Посты автоматически парсятся с публичной страницы t.me/s/‹channel› через node-html-parser. Извлечение: жирный заголовок из тегов ‹b›...‹/b›, обложка из background-image-стиля .tgme_widget_message_photo_wrap, теги из хештегов, время чтения из количества слов. React.cache() дедуплицирует fetch в рамках одного рендера. Next Data Cache (revalidate=3600) + webhook /api/telegram/webhook с revalidateTag при новом посте.
Динамический акцентный цвет. Кнопка в шапке циклит CSS-переменную --accent по 10-цветной палитре. Логика выбора исключает повтор подряд через module-level lastColor memo. После реализации палитры ⌘K — вынесена как cycleAccent() в features/accent-toggle/lib, переиспользуется и в тоггле, и в команде палитры.
UI Kit и дизайн-система. Витрина /uikit с токенами (цвета, типографика, отступы, радиусы) и базовыми компонентами: Button с тремя вариантами (primary/secondary/ghost), Input, Textarea, Badge, Card, ProgressBar. Кастомный SVG-набор иконок с CSS-mask для перекрашивания через currentColor. Mobile-first медиа-запросы, dark/light темы через data-theme атрибут.
Запуск sandwor.ru. Стек: Next.js 14 + App Router, TypeScript strict, Feature-Sliced Design (app → views → widgets → features → entities → shared). i18n на next-intl с переключением локали через куку (localePrefix: 'never'). Тёмная/светлая тема через next-themes. Шрифт Inter Variable. Деплой на Vercel.