Коли ми будували власний сайт, бриф був простий: показати, що ми маємо на увазі під «якісною веб-розробкою», просто відвантаживши таку. Не маркетинговий лендинг. Повноцінний сайт: двомовний контент, CMS, блог, контактні форми, реальні показники продуктивності. Ось технічні рішення і те, як вийшло.
Дослідження та вибір технологій
Перш ніж писати код, ми пройшлися по тому, що сайту насправді потрібно. Основні вимоги:
- Двомовність з коректним SEO для кожної мови
- Headless CMS для керування контентом без участі розробника
- Показники Lighthouse: 90+ по всіх категоріях
- Блог з підсвіткою синтаксису та Portable Text
- Контактна форма з валідацією, захистом від ботів та email-нотифікаціями
Ми зупинились на: Next.js 16 (App Router, React 19), Sanity v5 як headless CMS, Tailwind CSS v4 для стилів, Framer Motion для анімацій, Resend для транзакційного email.Next.js 16 (App Router, React 19), Sanity v5 як headless CMS, Tailwind CSS v4 для стилів, Framer Motion для анімацій та Resend для транзакційних листів.
Архітектура проєкту
Сайт працює на App Router Next.js 16 із чітким розмежуванням серверних і клієнтських компонентів. Сторінки, метадані, GROQ-запити — все на сервері. Інтерактивні частини (форми, анімації, модальні вікна) сидять за 'use client'.
Структура компонентів
Ми організували 24+ компонентів за доменом: blog/, contact/, layout/, services/, shared/ та ui/. UI-примітиви на кшталт Button і Sheet живуть окремо від бізнес-компонентів. Легше шукати, легше перевикористовувати.
Білінгвальність
Для i18n ми використовуємо динамічний сегмент [locale] у маршрутизації. Словники (130+ ключів) лінькуються через динамічний import. Функція t() підтримує інтерполяцію параметрів, а TypeScript на етапі компіляції перевіряє паритет ключів між локалями.
Контент у Sanity CMS зберігається з дуальними полями: titleUk/titleEn, bodyUk/bodyEn тощо. GROQ-запити використовують coalesce() для фоллбеку на дефолтну локаль, якщо англійський переклад відсутній.
Продуктивність та Lighthouse
Продуктивність була в роботі з першого дня. Ось конкретні оптимізації, які підняли показники:
Оптимізація зображень
Кастомний компонент SanityImage обгортає Next.js Image і автоматично витягує розміри з Sanity asset references. Отримуєш нативну оптимізацію (WebP/AVIF, responsive srcset) без ручного зазначення width/height. Above-the-fold-зображення отримують priority prop.
Анімації без жертв продуктивності
Framer Motion підключаємо через LazyMotion з domAnimation, тому завантажується тільки той код анімацій, який реально використовуємо, а не весь бандл. Секції анімуються при скролі через useInView() з once: true. prefers-reduced-motion поважаємо повністю: коли він увімкнений, анімації миттєво завершуються.
Кешування та ревалідація
Замість CDN-кешу Sanity ми використовуємо tag-based revalidation Next.js. Кожен GROQ-запит прив'язаний до тегу ('blog', 'services', 'settings'), а вебхук Sanity викликає revalidateTag() при зміні контенту. Миттєве оновлення, без ребілду.
Результати Lighthouse
Результат, стабільно на всіх сторінках:
- Performance: 95–100
- Accessibility: 95–100
- Best Practices: 95–100
- SEO: 100
SEO та структуровані дані
Кожна сторінка генерує метадані через утиліту createPageMetadata() з канонічними URL і мовними альтернативами (hreflang). JSON-LD охоплює чотири типи схем: Organization, Service (з діапазонами цін), Article (для блогу), Breadcrumb. Сайтмапа генерується динамічно і включає кожен блог-пост з його датою оновлення.
Доступність (a11y)
Доступність — це не галочка, яку ставлять у кінці. Це те, як ми будуємо. Ключові рішення:
- ARIA-атрибути на всіх інтерактивних елементах: aria-current для навігації, aria-live для анонсів помилок, aria-required та aria-invalid для полів форм
- Клавіатурна навігація: стрілки, Enter та Space працюють на картках вибору у формах
- Мінімальний розмір інтерактивних зон — 44px для коректної роботи на сенсорних пристроях
- Підтримка prefers-reduced-motion для користувачів, які чутливі до анімацій
- Семантична структура заголовків (h1 → h2 → h3 → h4) та figcaption для зображень у блозі
Контактна форма: два підходи
Ми реалізували два режими контактної форми для різних типів користувачів:
Guided (покрокова) — 4 кроки: тип проєкту, цілі, бюджет, контактні дані. Підходить для клієнтів, які хочуть отримати точнішу оцінку.
Quick (швидка) — одна форма з мінімумом полів. Для тих, хто хоче просто зв'язатися.
Обидва режими мають захист від ботів (honeypot-поле), серверну валідацію, rate limiting (5 запитів на годину з одного IP) та автоматичні листи: один адміну про нову заявку, інший клієнту з підтвердженням. Дані живуть у Sanity як contactSubmission.
Блог з підсвіткою синтаксису
Блог працює на Sanity Portable Text з кастомними рендерерами для блоків коду, зображень, списків і заголовків. Підсвітка синтаксису — з Shiki: код обробляється на сервері і приходить на клієнт уже як HTML з підсвіткою. Жодного Layout Shift при завантаженні.
Статті підтримують зміст (Table of Contents) з автоматичною генерацією з h2/h3 заголовків, пов'язані пости та категоризацію.
Sanity CMS: вбудована студія та live preview
Sanity Studio вбудована у Next.js-додаток за маршрутом /studio, тож редакторам не потрібен окремий деплой. Draft Mode проведено через API-маршрути з токенізованою авторизацією, live preview показує зміни в реальному часі.
Десктоп-структура Sanity Studio організована за типами контенту: послуги, блог-пости, кроки процесу, заявки з форм та налаштування сайту. Заявки сортуються за статусом відповіді та датою.
Висновки та результати
Те, що в нас вийшло — це не просто сайт. Це робоче демо того, як ми будуємо. Цифри:
- Lighthouse Performance 95–100 стабільно на всіх сторінках
- Повна двомовність з SEO-оптимізацією для кожної мови
- Автономний CMS — редактори керують контентом без розробника
- Доступність на рівні WCAG AA
- Структуровані дані (JSON-LD) для покращення видимості в пошуку
Якщо хочете подібного рівня якості для вашого проєкту, контактна форма прямо там.
