Files
exodevices/fitnes
Yuriy PanovandClaude Opus 5 e7385e8898 analytics: add the Yandex.Metrika counter to all four landings
Counter 112352796 goes into the <head> of every landing's index.html, ahead
of the Vite entry, so the hit fires before React mounts. The pages are plain
SPAs served off disk by nginx (Express only answers /api/*), so the static
head is the only place it needs to be.

One counter serves all four domains, which means reports mix them and every
domain has to be listed in the counter's own settings or its hits get
filtered — noted in deploy/README.md along with the conversion tracking that
is still missing: src/lib/lead.ts pushes a plain custom dataLayer event,
which Metrika's ecommerce: 'dataLayer' does not read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 00:24:33 +06:00
..

EXO Recovery Zone — лендинг для фитнес-клубов

React-приложение лендинга «EXO Recovery Zone». Каждая форма создаёт сделку в amoCRM (воронка 10980758) и контакт, если его ещё нет.

Стек: Vite + React 19 + TypeScript + Tailwind CSS v4, API — Express 5.


Быстрый старт

npm install
cp .env.example .env   # заполнить AMO_SUBDOMAIN и AMO_LONG_LIVED_TOKEN
npm run dev

npm run dev поднимает Vite на http://localhost:5173 и API на :3000; запросы /api/* проксируются с фронтенда на API.

Продакшен

npm run build   # сборка клиента в dist/client
npm start       # Express отдаёт dist/client и обрабатывает /api/*

Один процесс Node на любом VPS. Порт — PORT (по умолчанию 3000).


amoCRM

1. Получить долгосрочный токен

  1. amoCRM → Настройки → Интеграции → Создать интеграцию → Внешняя интеграция.
  2. Название любое (например «Сайт fitness»), redirect URI — любой рабочий адрес сайта: для долгосрочного токена он не используется.
  3. Права доступа: достаточно Сделки, Контакты (чтение и запись).
  4. Сохранить, открыть интеграцию → вкладка «Ключи и скопы»«Генерировать токен» (долгосрочный, действует ~1 год).
  5. Скопировать токен в .env в AMO_LONG_LIVED_TOKEN, поддомен — в AMO_SUBDOMAIN (для https://exotherapy.amocrm.ru это exotherapy).

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

2. Проверить подключение

npm run amo:check

Скрипт выведет название аккаунта, первый этап воронки 10980758, в который попадут сделки, и то, в какие поля вашего аккаунта легли данные формы.

3. Что происходит при отправке формы

  1. Поиск контакта по телефону (во всех написаниях: +7…, 8…, только цифры) и по email. Если контакт найден — используется он, недостающий телефон или email дописывается в карточку.
  2. Если контакта нет — создаётся новый с именем, телефоном и email.
  3. Создаётся сделка в воронке AMO_PIPELINE_ID, в её первом этапе, с тегами AMO_LEAD_TAGS + тег формы (заявка с сайта / обратный звонок).
  4. Данные, для которых в аккаунте есть подходящее поле (город, формат клуба, компания, utm_* и т.д.), пишутся в поля; всё остальное — в примечание к сделке. Ничего настраивать в amoCRM заранее не нужно.

Если amoCRM недоступна, заявка не теряется: она пишется в лог сервера ([lead] payload was: …) и сохраняется в localStorage браузера, а посетитель видит телефон для связи.

Переменные окружения

Переменная Обязательна Описание
AMO_SUBDOMAIN да Поддомен аккаунта, например exotherapy
AMO_LONG_LIVED_TOKEN да Долгосрочный токен интеграции
AMO_PIPELINE_ID нет Воронка для сделок (по умолчанию 10980758)
AMO_RESPONSIBLE_USER_ID нет Ответственный за сделку пользователь
AMO_LEAD_TAGS нет Теги через запятую для всех сделок
PORT нет Порт API/продакшен-сервера (3000)
VITE_SITE_URL нет Канонический адрес, подставляется в canonical и og:
VITE_BASE_PATH нет Подпуть размещения, например /fitness/ (по умолчанию /)

Структура

index.html            точка входа Vite (meta, Open Graph, JSON-LD)
src/
  components/         секции лендинга и UI-примитивы
  data/               весь текстовый контент и данные конструктора
  hooks/              useConstructor, useReveal, useScrollState, …
  lib/                отправка заявки, форматирование чисел
  assets/images/      14 изображений, извлечённых из исходного HTML
  index.css           дизайн-токены Tailwind v4 (@theme) и базовые стили
shared/lead.ts        схема заявки (zod), общая для клиента и сервера
server/src/           Express API + клиент amoCRM
scripts/amo-check.ts  диагностика подключения к amoCRM
legacy/index.html     исходный однофайловый лендинг (визуальный эталон)

Дизайн-система

Цвета, радиусы, тени, брейкпоинты и вертикальный ритм заданы токенами в @theme внутри src/index.css. Палитра Tailwind по умолчанию отключена (--color-*: initial), поэтому в разметке доступны только цвета EXO. Брейкпоинты соответствуют исходной вёрстке: sm 640, md 900, lg 1120, плюс max-width-варианты narrow (760), phone (520), compact (680), tiny (480).

Вёрстка совпадает с legacy/index.html попиксельно: высоты всех секций и общая высота страницы идентичны на 375 / 480 / 640 / 900 / 1120 / 1440 px.

Шрифты

Inter подключён локально пакетом @fontsource-variable/inter — внешних запросов (Google Fonts и т.п.) страница не делает. Это вариативный шрифт с диапазоном 100–900, поэтому нестандартные веса из макета (760, 850, 900) отрисовываются по-настоящему, а не округляются до bold.

Подсеты разделены по unicode-range, браузер скачивает только нужные:

подсет размер зачем
latin 48 КБ латиница
cyrillic 19 КБ кириллица
latin-ext 85 КБ только ради знака

Итого ~152 КБ. Если 85 КБ ради одного символа кажутся лишними — уберите latin-ext, и будет отрисовываться системным шрифтом.

Символы → ↗ ↘ ≈ не входят ни в один подсет Inter у Google Fonts / Fontsource, поэтому для них всегда используется системный фолбэк. Это заметно только при очень крупном кегле.

Manrope и Segoe UI остались в стеке --font-sans как фолбэк, но отдельно не загружаются: Manrope недостижим, пока Inter грузится успешно, а Segoe UI — проприетарный шрифт Microsoft, который нельзя раздавать с сайта.

Калькулятор

оплаченные программы в день × средний чек × рабочие дни = выручка в месяц.


Известные особенности

  • og:image отдаётся по абсолютному адресу VITE_SITE_URL + /og-image.webp; при смене домена обновите VITE_SITE_URL перед сборкой.