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>
ЭкзоОтель — лендинг для отелей
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. Получить долгосрочный токен
- amoCRM → Настройки → Интеграции → Создать интеграцию → Внешняя интеграция.
- Название любое (например «Сайт hotels»), redirect URI — любой рабочий адрес сайта: для долгосрочного токена он не используется.
- Права доступа: достаточно Сделки, Контакты (чтение и запись).
- Сохранить, открыть интеграцию → вкладка «Ключи и скопы» → «Генерировать токен» (долгосрочный, действует ~1 год).
- Скопировать токен в
.envвAMO_LONG_LIVED_TOKEN, поддомен — вAMO_SUBDOMAIN(дляhttps://exotherapy.amocrm.ruэтоexotherapy).
Токен читается только на сервере и никогда не попадает в браузерный бандл. Пометьте в календаре дату истечения — токен нужно перевыпустить через год.
Токен можно взять тот же, что у фитнес-лендинга: сделки различаются по тегу
(AMO_LEAD_TAGS=hotel-landing) и по названию сделки ЭкзоОтель — ….
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 браузера, а посетитель
видит телефон для связи.
Переменные окружения
| Переменная | Обязательна | Описание |
|---|---|---|
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
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 исходный однофайловый лендинг (визуальный эталон)
public/ favicon.png и og-image.jpg
Дизайн-система
Цвета, радиусы, тени, брейкпоинты и вертикальный ритм заданы токенами в
@theme внутри src/index.css. Палитра Tailwind по умолчанию отключена
(--color-*: initial), поэтому в разметке доступны только цвета EXO.
Брейкпоинты соответствуют исходной вёрстке: sm 640, md 900, плюс
max-width-варианты tiny (430), phone (480), compact (620),
narrow (780).
Ряды карточек (выгоды, гости, оборудование, сценарии, программы) до 900px
скроллятся по горизонтали со snap, выше — становятся сеткой. Это единственный
крупный кусок вёрстки, оставшийся классом (.scroll-cards в index.css):
grid-auto-columns не выражается утилитами, а сам ряд используется пять раз.
Соответствие исходной вёрстке
Текст страницы совпадает с legacy/index.html строка в строку (проверено
диффом innerText), высоты секций — в пределах 0,6% на 375 и 1440 px.
Расхождения дают метрики настоящего Inter: исходная страница просила Inter, но
нигде его не подключала и рисовалась системным фолбэком.
Три отличия сделаны намеренно:
.eyebrowи.leadв блоке заявки исходно оставались тёмными (#008F84и#60778B) на тёмно-синем фоне — контраст 3,4–3,8:1. Здесь они светлые, как в остальных тёмных секциях.- На экранах ≤430px значение в карточке hero-факта («4 аппарата», «1 оператор») не помещалось в колонку 80px и наезжало на подпись. Колонка расширена до 124px.
- В подписи к рыночному блоку исправлена опечатка «2025–2026г..» → «2025–2026 гг.».
Шрифты
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-версии этих названий тоже нет.