Bring deploy/ in line with the renamed app directories and with the domains that are actually configured in the per-app .env files: fitnes fitness.exorecovery.ru 3000 fitness-landing hotel hotel.exorecovery.ru 3001 hotel-landing medcenterphysio physio.exorecovery.ru 3002 physio-landing medcenterstart start.exorecovery.ru 3003 start-landing apps.conf, the README provisioning steps (directories, bare clones, systemd units, the sudoers whitelist) and the exo-deploy usage line all used the old medcenter / medcenterpersonal keys. The README table and app.build.env.example still carried the *_DOMAIN placeholders. Also refresh the tracked .env.example files: real VITE_SITE_URL values instead of the old exodevices.ru sub-paths, and the amo lead tags that match apps.conf. Two lockfile fixes, both of which broke `npm ci` in exo-deploy: - medcenterstart had no lockfile at all after package-lock.json and pnpm-lock.yaml were removed — restore package-lock.json and keep the pnpm one deleted, since the deploy path is npm; - medcenterphysio/package-lock.json had lost "resolved" and "integrity" on 147 of its 236 entries — restore the complete file. Verified: exo-render-nginx renders all four vhosts with no leftover placeholders, and every package-lock.json agrees with its package.json. 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 браузера, а посетитель
видит телефон для связи.
Переменные окружения
| Переменная | Обязательна | Описание |
|---|---|---|
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перед сборкой.