Files
exodevices/hotel
yuriy.pandClaude Opus 5 b32e7da506 Add the Top.Mail.Ru counter to all four landings with a lead goal
The counter (id 3795059) was only in fitnes and only counted pageViews.
Carry the snippet over to hotel, medcenterstart and medcenterphysio — the
same counter serves all four, as the Metrika one already does — and fire
goal 'form' from submitLead(), the single place both the request form and
the callback dialog pass through.

The goal sits inside the existing `leadId !== 0` guard: that response is
the honeypot decoy, so a bot submission must not register a conversion,
in Top.Mail.Ru no more than in Metrika.

The noscript pixel goes in the body next to the Metrika one, per 9af36cc;
the fitnes one, added in the head, moves down to join it.

Note that fitnes/index.html also carries a Prettier pass over the whole
file — the reflowed Metrika block is formatting only, no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 22:08:29 +06:00
..

ЭкзоОтель — лендинг для отелей

React-приложение лендинга «ЭкзоОтель». Каждая заявка создаёт сделку в amoCRM (воронка 10980758) и контакт, если его ещё нет.

Стек: Vite + React 19 + TypeScript + Tailwind CSS v4, API — Express 5. Архитектура та же, что у лендинга для фитнес-клубов (../fitnes).


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

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

npm run dev поднимает Vite на http://localhost:5174 и API на :3001; запросы /api/* проксируются с фронтенда на API. Порты отличаются от фитнес-лендинга (5173 / 3000), поэтому оба сайта можно держать запущенными одновременно.

Продакшен

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

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


amoCRM

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

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

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

Токен можно взять тот же, что у фитнес-лендинга: сделки различаются по тегу (AMO_LEAD_TAGS=hotel-landing) и по названию сделки ЭкзоОтель — ….

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

npm run amo:check

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

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

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

Форм две. Основная — блок «Следующий шаг» внизу страницы: обязательны только имя и телефон, отель, email и комментарий по желанию; сделка называется ЭкзоОтель — <отель или имя>. Вторая — модальное окно «Перезвоним вам», его открывает кнопка «Звонок» в шапке (то же окно, что на фитнес-лендинге): имя, телефон и согласие на обработку данных. Сделка называется Обратный звонок — <имя>, в поле или примечании «Форма» — «Быстрый обратный звонок».

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

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

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

Сервер создаёт сделку без контакта: посетитель не оставил ни имени, ни номера, известен только факт клика. Имя сделки — «Звонок с сайта — ЭкзоОтель», тег — клик по телефону (плюс 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/продакшен-сервера (3001)
VITE_SITE_URL нет Канонический адрес, подставляется в canonical и og:
VITE_BASE_PATH нет Подпуть размещения, например /hotels/ (по умолчанию /)

Структура

index.html            точка входа Vite (meta, Open Graph, JSON-LD)
src/
  components/         секции лендинга и UI-примитивы
  data/content.ts     весь текстовый контент страницы
  hooks/              useReveal, useScrolled, useAnchorScroll, useBodyLock, useEscapeKey
  lib/                отправка заявки, форматирование чисел
  assets/images/      11 изображений, извлечённых из исходного HTML
  index.css           дизайн-токены Tailwind v4 (@theme) и базовые стили
shared/lead.ts        схема заявки (zod), общая для клиента и сервера
server/src/           Express API + клиент amoCRM
scripts/amo-check.ts  диагностика подключения к amoCRM
legacy/index.html     исходный однофайловый лендинг (первая версия дизайна)
legacy/hotel v7/      макет v7 — текущий визуальный эталон
public/               favicon.png и og-image.jpg

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

Цвета, радиусы, тени, брейкпоинты и вертикальный ритм заданы токенами в @theme внутри src/index.css. Палитра Tailwind по умолчанию отключена (--color-*: initial), поэтому в разметке доступны только цвета EXO. Брейкпоинты соответствуют исходной вёрстке: sm 640, md 900, плюс max-width-варианты narrow (780), slim (640), compact (620), phone (480), tiny (430), micro (380). Tailwind выводит пользовательские варианты в порядке объявления, поэтому они объявлены от широкого к узкому: где действуют два, побеждает более узкий — как в каскаде макета.

Ряды карточек (выгоды, гости, оборудование, сценарии, программы) до 900px скроллятся по горизонтали со snap, выше — становятся сеткой. Это единственный крупный кусок вёрстки, оставшийся классом (.scroll-cards в index.css): grid-auto-columns не выражается утилитами, а сам ряд используется пять раз.

Соответствие макету

Визуальный эталон — макет legacy/hotel v7/. Сам макет Inter не подключает и рисуется системным шрифтом, поэтому сверять его с сайтом нужно, подставив в него Inter Variable. На одном шрифте текст страницы совпадает с макетом строка в строку (дифф innerText), а высоты всех секций и общая высота страницы — попиксельно на 360 / 375 / 430 / 480 / 620 / 640 / 780 / 900 / 1120 / 1440 px.

Намеренные отличия от макета:

  • .eyebrow в блоке заявки в макете остаётся тёмным (#008F84) на тёмно-синем фоне. Здесь он светлый, как в остальных тёмных секциях.
  • В подписи к рыночному блоку исправлена опечатка «2025–2026г..» → «20252026 гг.».
  • Модальное окно «Перезвоним вам» взято с фитнес-лендинга (светлые поля, согласие отдельным чекбоксом), а не вёрстка .exo-callback из макета; маски телефона, как и на фитнесе, нет. Тексты окна — из макета.

Шрифты

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

Подсеты разделены по unicode-range, браузер скачивает только нужные: latin (48 КБ), cyrillic (19 КБ) и latin-ext (85 КБ — только ради знака в калькуляторе). Итого ~152 КБ.

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

Калькулятор

программы в день × средний чек × рабочие дни = выручка в месяц (та же формула, что в исходном exo-economics-calculators).


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

  • og:image отдаётся по абсолютному адресу VITE_SITE_URL + /og-image.jpg; при смене домена обновите VITE_SITE_URL перед сборкой. Если сайт стоит на подпути (/hotels), файл всё равно лежит в корне — проверьте адрес после деплоя.
  • В legacy/index.html остался комментарий о том, что названия отелей-кейсов (Marriott, Cosmosstay, «Адонис» и др.) не выводятся до подтверждения права использования брендов. В React-версии этих названий тоже нет.