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>
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>
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>
Ports the single-file ЭкзоОтель page to the same architecture as the fitness
landing: Vite + React 19 + TypeScript + Tailwind v4 on the client, Express 5 for
the /api/leads/hotels endpoint, zod schema shared between the two.
The original page is kept in legacy/index.html as the visual reference. Its 15
inlined base64 images are extracted to files (the 1 MB HTML becomes ~318 KB of
JS plus assets loaded on demand), and its text is reproduced line for line —
verified with an innerText diff. Section heights stay within 0.6% at 375 and
1440 px; the drift comes from Inter actually loading, which the original asked
for but never served.
Three deliberate departures, documented in the README:
* eyebrow and lead in the CTA block were dark teal on navy (3.4:1); they now
match the other dark sections
* hero fact values overflowed their 80px column into the label below 430px
* "2025–2026г.." typo in the market source note
Leads reuse the fitness amoCRM integration: contact lookup by phone in every
spelling, deal in the first stage of pipeline 10980758, account fields matched
automatically with the rest written to a note. Rate limit, honeypot, and a
localStorage fallback so a lead survives the CRM being down. Vite runs on 5174
and the API on 3001 so both landings can run at once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>