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>
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. Получить долгосрочный токен
- amoCRM → Настройки → Интеграции → Создать интеграцию → Внешняя интеграция.
- Название любое (например «Сайт fitness»), redirect URI — любой рабочий адрес сайта: для долгосрочного токена он не используется.
- Права доступа: достаточно Сделки, Контакты (чтение и запись).
- Сохранить, открыть интеграцию → вкладка «Ключи и скопы» → «Генерировать токен» (долгосрочный, действует ~1 год).
- Скопировать токен в
.envвAMO_LONG_LIVED_TOKEN, поддомен — вAMO_SUBDOMAIN(дляhttps://exotherapy.amocrm.ruэтоexotherapy).
Токен читается только на сервере и никогда не попадает в браузерный бандл. Пометьте в календаре дату истечения — токен нужно перевыпустить через год.
2. Проверить подключение
npm run amo:check
Скрипт выведет название аккаунта, первый этап воронки 10980758, в который
попадут сделки, и то, в какие поля вашего аккаунта легли данные формы.
3. Что происходит при отправке формы
- Поиск контакта по телефону (во всех написаниях:
+7…,8…, только цифры) и по email. Если контакт найден — используется он, недостающий телефон или email дописывается в карточку. - Если контакта нет — создаётся новый с именем, телефоном и email.
- Создаётся сделка в воронке
AMO_PIPELINE_ID, в её первом этапе, с тегамиAMO_LEAD_TAGS+ тег формы (заявка с сайта/обратный звонок). - Данные, для которых в аккаунте есть подходящее поле (город, формат клуба,
компания,
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перед сборкой.