Commit Graph
10 Commits
Author SHA1 Message Date
Yuriy PanovandClaude Opus 5 fc6a6dde8b Refresh package-lock.json in all four landings
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 00:13:53 +06:00
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
Yuriy PanovandClaude Opus 5 d13feb7df8 fitnes: add the Fitness v2 mockup as the design reference
legacy/Fitnes v2/ is the static mockup the landing was just ported from
(README points to it as the current visual reference). launch.json gains a
fitnes-v2-legacy entry that serves it on :8090 for side-by-side review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 18:38:10 +06:00
Yuriy PanovandClaude Opus 5 432284eefb fitnes: apply the Fitness v2 design
Ports legacy/Fitnes v2 into the React landing.

- Copy: nav is now Экономика / Оборудование / Программы / Интерьер, every
  CTA reads «Получить консультацию», the hero has one button, a new label
  and new facts.
- Calculator: the average check defaults to 2 500 ₽, and a new investment
  field drives payback and 12-month ROI. Adds the ЭкзоКлиник source note
  and a new disclaimer.
- Constructor: «Состав оборудования», indications on the device cards. The
  CTA stays live with fewer than three devices picked, and the copy points
  to a consultation instead.
- «Юридическая рамка» is replaced by «Форматы размещения» (12 / 24 / 36 м²).
- Lead form: city and club format are dropped; company and email are
  optional. The server schema already treats them as optional.
- Section headings share one compact scale (headingScale + a `slim`
  640px variant), as in the design's v7 rule.

Deliberately kept from the current site rather than the mockup: the
+7 927 789-60-71 number and the callback modal behind the header call
button, both newer than the design.

With Inter substituted into the mockup (it ships no font and falls back to
Arial), every section height matches at 900 and 1440 px. At 375 px the
hero is 6 px shorter because of the existing fluid h1 on phones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 18:37:03 +06:00
Yuriy PanovandClaude Opus 5 8af2e463b3 analytics: count a successful form as the FORM_SUCCESS Metrika goal
The counter was in place but nothing reached it: src/lib/lead.ts only pushed
a custom dataLayer event, which Metrika's ecommerce: 'dataLayer' ignores, so
form conversions were not counted at all.

All five forms — including the fitnes callback modal — funnel through the one
success branch in submitLead(), so a single ym(112352796, 'reachGoal',
'FORM_SUCCESS') per landing covers every one of them. The counter id is
hardcoded to match the inline snippet in index.html; two sources for it would
drift. The call is optional-chained and wrapped in try/catch, because losing
analytics must never cost a lead.

The honeypot is the one success that is not a conversion: the server answers
bots with 200 {ok:true, leadId:0} so they learn nothing, and leadId === 0 is
what keeps that answer out of the goal.

The goal itself still has to be created in the counter's settings as a
JavaScript event named FORM_SUCCESS — noted in deploy/README.md, replacing
the entry that said conversions were unwired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 01:30:47 +06:00
Yuriy PanovandClaude Opus 5 e13854e918 leads: new phone number and an amoCRM lead on every call-button click
The published number becomes +7 927 789-60-71 across all four landings —
`contacts` in each `src/data/content.ts`, plus the fallback messages in the
lead endpoints and `src/lib/lead.ts` that spell it out when amoCRM is down.
The `legacy/` reference pages keep the old number: they are the visual
originals, not something that ships.

Tapping the phone button now also creates a lead. It cannot reuse the form
path: a click carries no name and no number, so `callClickSchema` in
`shared/lead.ts` validates the tracking data alone, `LeadService.submitCallClick`
creates a contactless lead tagged `клик по телефону`, and `AmoClient.createLead`
takes an optional `contactId` for it. The mapper's field/note split moved into
`collect()` so both lead kinds share it.

The browser fires this with `sendBeacon` (falling back to `fetch keepalive`),
because the same click hands the page to `tel:` and a plain fetch would be
cut off mid-flight. Duplicates are held down from both ends: one lead per
browser session on the client, four per IP per 30 minutes on the server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 00:58:15 +06:00
Yuriy PanovandClaude Opus 5 e7385e8898 analytics: add the Yandex.Metrika counter to all four landings
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>
2026-09-09 00:24:33 +06:00
Yuriy PanovandClaude Opus 5 51f39c9b88 deploy: follow the landing rename and the exorecovery.ru domains
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>
2026-09-06 23:36:17 +06:00
yuriy.pandClaude Opus 5 b673367c6f Prepare the four landings for production deployment
Fixes that block or weaken a real deployment. Nothing here changes the
rendered pages.

Bind the lead API to loopback. Each server called app.listen() without a
host, so it bound 0.0.0.0. Combined with a blanket `trust proxy: true` —
which makes Express take the leftmost X-Forwarded-For entry as req.ip —
the in-memory lead rate limiter was spoofable by anyone who could reach
the port directly. HOST now defaults to 127.0.0.1 and trust is narrowed
to 'loopback', so a request arriving from anywhere but the local proxy
has its forged header ignored.

Give each landing its own port. All four .env files claimed PORT=3000,
and fitnes/.env.example collided with medcenter/.env.example, so three of
the four could never have started on one host. Now 3000/3001/3002/3003
consistently across the code defaults, the env templates and the vite
dev proxies, so all four also run side by side locally.

Template the JSON-LD url. canonical and og:url already resolved from
%VITE_SITE_URL%, but the JSON-LD block hardcoded an exodevices.ru
sub-path that would not follow the environment. All four now read from
the same variable.

Declare the Node version. Nothing stated it, yet transitive deps impose
a >=22.12 floor (@rolldown/binding, yargs, concurrently). Added engines
and .nvmrc so a too-old runtime fails clearly.

Typechecked and production-built on all four; verified the socket binds
127.0.0.1 only, health reports amo:true, the site still boots with the
CRM unconfigured, and the limiter returns 429 with Retry-After on the
ninth request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 15:57:24 +06:00
Yuriy PanovandClaude Opus 5 60de4ef27c Convert fitness landing to React + Tailwind v4 with amoCRM lead capture
Port the single-file landing (2.6 MB of inlined CSS, JS and base64 images,
kept as fitnes/legacy/index.html) to Vite + React 19 + TypeScript, with an
Express API that files every form submission into amoCRM pipeline 10980758.

- Extract the 14 embedded images to src/assets/images and public/
- Rebuild the design system as Tailwind v4 @theme tokens; the stock palette and
  breakpoints are cleared so only the EXO scale is reachable from utilities
- Split the page into 15 components; all copy moves to src/data
- Lead endpoint: find-or-create the contact (Russian phone spellings compared on
  the last 10 digits), create the lead in the pipeline's first stage, map the
  fields the account already has and put the rest in a note. If amoCRM is
  unreachable the payload is logged and kept in localStorage rather than lost.
- Add a callback modal as a second entry point, tagged separately in the pipeline
- Self-host Inter Variable so the layout's 760/850/900 weights render as real
  weights instead of snapping to bold

Fidelity was checked by comparing section offsets and heights against the
original at 375/480/640/900/1120/1440 px; every section and the total page
height matched exactly. Loading Inter deliberately changes text metrics, so the
byte-exact comparison holds against the pre-font build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 13:21:48 +06:00