gendesign/tradein-mvp/frontend/Dockerfile
bot-backend 671fef758e
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m19s
CI / openapi-codegen-check (pull_request) Successful in 2m6s
CI / backend-tests (pull_request) Successful in 17m40s
feat(mera): Метрика и GA4 на публичном контуре + открытие сайта для индексации
ЗАЧЕМ. Статьи МЕРЫ публикуются с UTM-метками, но посмотреть, приходил ли по
ним кто-нибудь, было физически нечем: веб-аналитики на публичном контуре не
было вовсе. Заодно вскрылось, что «толкнуть в выдаче» тоже нельзя — всё
дерево mera-public отдавало `robots: noindex, nofollow`.

СЧЁТЧИКИ. Яндекс.Метрика и GA4 подключаются ТОЛЬКО в `mera-public/layout.tsx`
и никогда в корневом `app/layout.tsx` — иначе счётчик уехал бы в закрытый
контур (/v2, /admin, /scrapers, /history), где анонимных посетителей нет, а
приватные маршруты сотрудников есть. Идентификаторы приходят build-time
(`NEXT_PUBLIC_YM_ID` / `NEXT_PUBLIC_GA_ID`) — канон Dockerfile'а этого
проекта: Next инлайнит NEXT_PUBLIC_* на сборке, runtime env их не подхватит.
Пустое значение = тег не рендерится вовсе, никаких `ym(undefined)`. Оба
build-arg'а прописаны в ОБОИХ блоках CI, включая retry-сборку без кеша.

Вебвизор выключен намеренно. Он пишет ввод в поля, а на `/estimate` человек
вводит адрес своей квартиры; раздел 9 политики этого не раскрывает. Включать
следует одним заходом с правкой политики и маскировкой полей — в коде рядом
записано, что именно понадобится.

ЦЕЛИ ВОРОНКИ. Десять целей: клик по CTA, начало ввода адреса, адрес выбран,
результат с разбивкой по вердикту (ok/thin/none), ошибка расчёта, ошибка
валидации, отказ подсказок, показ платного тизера. Кнопок «Проверить
квартиру» восемь штук в разных компонентах, все — обычные `<a>` через
PublicLink, поэтому вместо восьми копий onClick один делегированный
слушатель на document: девятая кнопка подключится сама. Цель «оплата
успешна» НЕ заведена — вызова checkout во фронте нет вовсе, PAYMENTS_ENABLED
выключен, страницы возврата не существует; вешать её пока не на что.

ИНДЕКСАЦИЯ. Снят noindex со всех публичных страниц, добавлены `app/robots.ts`
и `app/mera-public/sitemap.ts`, metadataBase, canonical на КОРОТКИЕ адреса,
openGraph и JSON-LD Article на главной статье. robots.txt и sitemap.xml
разведены по двум разным handle в Caddy не от хорошей жизни: у Next
robots.txt — конвенция корня app/, а sitemap живёт в сегменте маршрута, и
формы путей не совпадают.

152-ФЗ. Раздел 9 «Файлы cookie и веб-аналитика» в политике (обработчики
названы поимённо — этого требует ч. 3 ст. 6) + уведомляющий, не блокирующий
баннер. Гейт «названий площадок в публичной копии быть не должно» получил
узкое исключение ровно на аналитические словосочетания в политике; голое
«Яндекс» как площадка остаётся запрещённым и там.

ПОПУТНЫЙ БАГ (замер на живом проде 10.09.2026). `meraocenka.ru/articles/`
с UTM-метками отдавал 301 на адрес БЕЗ query — матчер @meraShortSlash
собирал цель из regex-захвата пути и терял параметры. Код ответа при этом
оставался 301, поэтому смоук проблему не видел. Мессенджеры и
автолинкификаторы дописывают слэш сами, то есть атрибуция терялась именно на
трафике по опубликованной ссылке. Починено тем же приёмом, что у соседних
матчеров; в смоук добавлена проверка буквального Location.

ГЕЙТЫ. `isPublicPath` и периметр-тест узнали про новые машинные адреса;
noindex-гейт развёрнут (падает, если флаг вернулся) и расширен на строковую
форму `robots: "noindex"`; заведена проверка, что корневых `handle` в
site-блоке не появляется без объявления — раньше эту дверь гейт не видел.

Проверено: tsc и eslint чисто, `npm run build` проходит, robots.txt и
sitemap.xml отдаются по нужным адресам, при пустых ID в HTML нет ни одного
обращения к mc.yandex.ru и googletagmanager, `caddy validate` валиден,
изоляция mera-public от B2B не нарушена. Два теста LoginPage падают и на
нетронутом дереве — не наши.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiUFZ3rmTNpp3DRajUo8KQ
2026-09-10 16:52:31 +03:00

110 lines
6.4 KiB
Docker
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Trade-In MVP frontend — Next.js 15 + React 19 + TanStack Query.
# Без Sentry/Tailwind/Leaflet/echarts — лёгкая версия только для trade-in.
# ---- deps ----
FROM node:24-alpine AS deps
WORKDIR /app
# Лок ОБЯЗАТЕЛЕН (без `*`): `npm ci` без него падает, и это правильно —
# отсутствие лока раньше молча давало `npm install`, т.е. состав зависимостей
# определялся датой сборки (#2770).
COPY package.json package-lock.json ./
# --legacy-peer-deps: React 19 peer-dep mismatches (как в соседнем frontend/).
RUN --mount=type=cache,target=/root/.npm \
npm ci --legacy-peer-deps --no-audit --no-fund
# ---- builder ----
FROM node:24-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
# basePath + API-prefix baked-in во время build (Next.js — build-time only).
# В CI workflow для prod:
# --build-arg NEXT_PUBLIC_BASE_PATH=/trade-in
# --build-arg NEXT_PUBLIC_API_BASE_URL=/trade-in
# В локальном dev: оба пусто = no-op (same-origin).
ARG NEXT_PUBLIC_BASE_PATH=""
ENV NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_ENABLE_PREVIEW=""
ENV NEXT_PUBLIC_ENABLE_PREVIEW=$NEXT_PUBLIC_ENABLE_PREVIEW
# Аналитика и SEO-верификация (Яндекс.Метрика, GA4, Яндекс.Вебмастер,
# Google Search Console) — тот же контракт, что у переменных выше: Next.js
# инлайнит NEXT_PUBLIC_* в статические бандлы на `npm run build`, runtime
# env их НЕ подхватит, поэтому это build-arg, а не рантайм-конфиг.
#
# ПУСТОЙ ДЕФОЛТ — ЧАСТЬ КОНТРАКТА ФРОНТА, не временная заглушка: пустое
# значение означает «счётчик/мета-тег не рендерится вообще» (весь
# соответствующий script/meta отсутствует в разметке) — так решает
# frontend-код, потребляющий эти env (Dockerfile сам их не читает).
#
# СЧЁТЧИКОВ ПОКА НЕТ — владелец заведёт их позже. Когда появятся номера:
# 1) вписать литералом в build-args джоба build-frontend
# (.forgejo/workflows/deploy-tradein.yml) — В ОБОИХ блоках, основном
# И retry-без-кеша, иначе ретрай молча уедет без счётчика;
# 2) пересобрать образ — смена значения ТРЕБУЕТ build (build-time bake),
# одной правки runtime-конфига на проде недостаточно.
ARG NEXT_PUBLIC_YM_ID=""
ENV NEXT_PUBLIC_YM_ID=$NEXT_PUBLIC_YM_ID
ARG NEXT_PUBLIC_GA_ID=""
ENV NEXT_PUBLIC_GA_ID=$NEXT_PUBLIC_GA_ID
ARG NEXT_PUBLIC_YANDEX_VERIFICATION=""
ENV NEXT_PUBLIC_YANDEX_VERIFICATION=$NEXT_PUBLIC_YANDEX_VERIFICATION
ARG NEXT_PUBLIC_GOOGLE_VERIFICATION=""
ENV NEXT_PUBLIC_GOOGLE_VERIFICATION=$NEXT_PUBLIC_GOOGLE_VERIFICATION
# Версия продукта («Мера») + короткий git SHA + дата сборки — ДОЛЖНЫ быть
# build-time ARG (не runtime env): Next.js инлайнит NEXT_PUBLIC_* в статические
# бандлы на `npm run build`, а этот build context (./tradein-mvp/frontend) не
# видит tradein-mvp/VERSION (он на уровень выше, вне build context) — источник
# правды читает CI ДО вызова `docker build` (.forgejo/workflows/deploy-tradein.yml,
# job build-frontend) и передаёт сюда готовыми значениями. Пустые дефолты — для
# локальной сборки без CI; фолбэк на "VERSION-файл/dev/дата сборки" делает уже
# frontend-код, потребляющий эти env (Dockerfile сам файл не читает).
ARG NEXT_PUBLIC_APP_VERSION=""
ENV NEXT_PUBLIC_APP_VERSION=$NEXT_PUBLIC_APP_VERSION
ARG NEXT_PUBLIC_BUILD_SHA=""
ENV NEXT_PUBLIC_BUILD_SHA=$NEXT_PUBLIC_BUILD_SHA
ARG NEXT_PUBLIC_BUILD_DATE=""
ENV NEXT_PUBLIC_BUILD_DATE=$NEXT_PUBLIC_BUILD_DATE
# CHANGELOG.md — источник для /versions (src/lib/changelog.ts). Живёт на
# уровень выше этого build context (tradein-mvp/CHANGELOG.md), поэтому CI
# копирует его СЮДА (tradein-mvp/frontend/CHANGELOG.md) непосредственно
# перед `docker build` (см. .forgejo/workflows/deploy-tradein.yml, job
# build-frontend) — `COPY . .` ниже подхватывает её автоматически вместе с
# остальным контекстом. Локальная сборка без этого шага CI просто не находит
# файл — readChangelog() уже умеет деградировать (пустая история), сам
# Docker-билд при этом не падает (см. glob-COPY в runner stage ниже).
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# ---- runner ----
FROM node:24-alpine AS runner
WORKDIR /app
# HOSTNAME=0.0.0.0 — Next.js standalone server.js биндится по этому env.
# Без него Docker ставит HOSTNAME=<container-id> и сервер недоступен по
# имени сервиса из других контейнеров (Caddy → 502).
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \
HOSTNAME=0.0.0.0 \
PORT=3000
COPY --from=builder --chown=node:node /app/public ./public
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
# /versions — Server Component, statically prerendered at `npm run build`
# (see src/app/versions/page.tsx) — CHANGELOG.md's content is already baked
# into .next/standalone above. This is a defensive fallback ONLY, in case that
# page ever stops being static: glob (trailing `*`) makes it a no-op when the
# builder stage doesn't have the file either (local build without the CI
# pre-copy step, see builder stage comment above) — never fails the build.
COPY --from=builder --chown=node:node /app/CHANGELOG.md* ./
USER node
EXPOSE 3000
CMD ["node", "server.js"]