Files
exodevices/fitnes
Yuriy PanovandClaude Opus 5 9af36cc374 Move the Yandex.Metrika noscript pixel from head to body
A <noscript> in <head> may only contain link, style and meta, so parse5
rejected the <div><img> pixel (disallowed-content-in-noscript-in-head).
Applied to hotel, fitnes, medcenterphysio and medcenterstart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 23:52:37 +06:00
..
2026-09-11 18:37:03 +06:00
2026-09-11 18:37:03 +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 браузера, а посетитель видит телефон для связи.

4. Что происходит при клике на телефон

Кнопка звонка (в футере и в модальном окне «Перезвоним вам») помимо набора номера отправляет POST /api/leads/fitness-centers/call — маячком navigator.sendBeacon, чтобы запрос пережил переход браузера на tel:.

Сервер создаёт сделку без контакта: посетитель не оставил ни имени, ни номера, известен только факт клика. Имя сделки — «Звонок с сайта — Recovery Zone», тег — клик по телефону (плюс AMO_LEAD_TAGS), страница и utm_* идут в поля или в примечание, как и у обычной заявки.

Защита от дублей двойная: на клиенте — один лид на сессию браузера (sessionStorage), на сервере — не больше 4 обращений с одного IP за 30 минут.

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

Переменная Обязательна Описание
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     исходный однофайловый лендинг (первая версия дизайна)
legacy/Fitnes v2/     макет Fitness v2 — текущий визуальный эталон

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

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

Визуальный эталон — макет legacy/Fitnes v2/. Сам макет Inter не подключает и рисуется системным шрифтом, поэтому сверять его с сайтом нужно, подставив в него Inter Variable. На одном шрифте высоты всех секций и общая высота страницы совпадают на 900 и 1440 px; на 375 px hero ниже на 6 px, потому что на телефонах h1 масштабируется (clamp(34px, 11.3vw, 44px)), а в макете стоит фиксированные 44 px.

Намеренные отличия от макета: телефон +7 927 789-60-71 (в макете старый номер), кнопка «Звонок» в шапке открывает модальное окно «Перезвоним вам», а не tel:.

Шрифты

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, который нельзя раздавать с сайта.

Калькулятор

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

Средний чек по умолчанию — 2 500 ₽ (данные «ЭкзоКлиник», Тольятти). Если указаны инвестиции в запуск, считаются ещё два показателя, оба без операционных расходов:

  • срок возврата = инвестиции ÷ выручка в месяц;
  • ROI за 12 месяцев = (выручка в месяц × 12 − инвестиции) ÷ инвестиции.

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

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