diff --git a/caddy/sites/apps.caddy b/caddy/sites/apps.caddy
index 8010c04b..085146a4 100644
--- a/caddy/sites/apps.caddy
+++ b/caddy/sites/apps.caddy
@@ -347,7 +347,7 @@ meraocenka.ru {
# `trailingSlash: false` ответил бы на такой путь 308-редиректом на вариант
# без слэша — то есть на ДЛИННЫЙ адрес, который handle ниже отправит 301 на
# «/», и запрос закольцуется.
- @meraPages path /estimate /oferta /refund /privacy /articles /articles/kak-ocenit-kvartiru /docs
+ @meraPages path /estimate /oferta /refund /privacy /articles /articles/kak-ocenit-kvartiru /docs /business
handle @meraPages {
rewrite * /trade-in/mera-public{path}
reverse_proxy tradein-frontend:3000 {
@@ -361,7 +361,7 @@ meraocenka.ru {
# голый 404 (так было и до этого PR, с момента #2615). Заодно это
# замыкает цепочку для длинных адресов со слэшем: они приходят на короткий
# со слэшем и здесь нормализуются.
- @meraShortSlash path_regexp shortslash ^/(estimate|oferta|refund|privacy|articles|articles/kak-ocenit-kvartiru|docs)/$
+ @meraShortSlash path_regexp shortslash ^/(estimate|oferta|refund|privacy|articles|articles/kak-ocenit-kvartiru|docs|business)/$
handle @meraShortSlash {
redir * /{re.shortslash.1} permanent
}
@@ -446,7 +446,7 @@ meraocenka.ru {
# порядок директив внутри `handle` определяет Caddy, и без неё `redir`
# выполняется РАНЬШЕ `uri`, отдавая Location, равный исходному адресу
# (бесконечный цикл; поймано на локальном стенде).
- @meraLongPages path /trade-in/mera-public/estimate /trade-in/mera-public/estimate/ /trade-in/mera-public/oferta /trade-in/mera-public/oferta/ /trade-in/mera-public/refund /trade-in/mera-public/refund/ /trade-in/mera-public/privacy /trade-in/mera-public/privacy/ /trade-in/mera-public/articles /trade-in/mera-public/articles/ /trade-in/mera-public/articles/kak-ocenit-kvartiru /trade-in/mera-public/articles/kak-ocenit-kvartiru/ /trade-in/mera-public/docs /trade-in/mera-public/docs/
+ @meraLongPages path /trade-in/mera-public/estimate /trade-in/mera-public/estimate/ /trade-in/mera-public/oferta /trade-in/mera-public/oferta/ /trade-in/mera-public/refund /trade-in/mera-public/refund/ /trade-in/mera-public/privacy /trade-in/mera-public/privacy/ /trade-in/mera-public/articles /trade-in/mera-public/articles/ /trade-in/mera-public/articles/kak-ocenit-kvartiru /trade-in/mera-public/articles/kak-ocenit-kvartiru/ /trade-in/mera-public/docs /trade-in/mera-public/docs/ /trade-in/mera-public/business /trade-in/mera-public/business/
handle @meraLongPages {
route {
uri strip_prefix /trade-in/mera-public
diff --git a/scripts/smoke-mera-perimeter.sh b/scripts/smoke-mera-perimeter.sh
index 52357c7b..478adcda 100644
--- a/scripts/smoke-mera-perimeter.sh
+++ b/scripts/smoke-mera-perimeter.sh
@@ -90,6 +90,11 @@ check "meraocenka.ru/privacy — public 200" "$BASE_MERA/privacy" 200
# лэндинга. Отвалится handle — кнопки «Проверить» станут ссылками в 404.
check "meraocenka.ru/estimate — public 200" "$BASE_MERA/estimate" 200
+# 1d2. Страница «МЕРА для бизнеса» — короткий адрес, на который ведут пункт
+# шапки и подвала. До 31.08.2026 они вели в закрытый контур и приводили
+# человека на форму входа; отвалится handle — вернётся 404 вместо неё.
+check "meraocenka.ru/business — public 200" "$BASE_MERA/business" 200
+
# 1e. Длинные адреса поддерева отдают 301 на короткие: у страницы один
# канонический адрес, а старые ссылки и закладки продолжают работать.
# ГОЛЫЙ /trade-in/mera-public — регресс на баг 15.08.2026: прежний матчер
diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-numbers-gate.test.ts b/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-numbers-gate.test.ts
index 369ba1a0..328fbf4d 100644
--- a/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-numbers-gate.test.ts
+++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/landing-numbers-gate.test.ts
@@ -79,8 +79,57 @@ const V3_DIR = "_components/v3";
const GENERATED = ["ekb-districts.ts"];
const GENERATED_MARK = "СГЕНЕРИРОВАН";
+/**
+ * ПОДСТРАНИЦЫ ПЕРИМЕТРА тоже под гейтом, а не только корень и v3-секции.
+ *
+ * 31.08.2026 завели `/business`, и ревьюер заметил, что новая ПУБЛИЧНАЯ
+ * страница чисел ничем не сторожится: сканер знал ровно два места — корневой
+ * `page.tsx` и каталог `_components/v3`. Чисел там тогда не было, но правило
+ * держалось на том, что автор о нём помнил.
+ *
+ * Каталоги перечисляются ОБХОДОМ, а не списком: список — снимок, и следующую
+ * страницу в него снова забудут вписать. Обход находит её сам.
+ */
+/**
+ * ЮРИДИЧЕСКИЕ СТРАНИЦЫ ПОД ЭТОТ ГЕЙТ НЕ ПОПАДАЮТ — и это не послабление.
+ *
+ * Гейт ищет ФОРМУ величины: «14,5 %», «42 700», «150 ₽». Тексты оферты,
+ * политики и возврата состоят из ссылок на законы, и по форме они те же самые:
+ *
+ * Закон РФ от 07.02.1992 № 2300-1 «О защите прав потребителей»
+ * Федеральный закон от 27.07.2006 № 152-ФЗ
+ * ст. 18.1 152-ФЗ
+ *
+ * Отличить номер закона от замера рынка регулярным выражением нельзя, а
+ * подгонять под них исключения по одному значило бы дырявить сам сканер.
+ * Правило, которое гейт защищает, к этим страницам и не относится: там числа
+ * ЦИТИРУЮТСЯ из нормативных актов, а не утверждаются о рынке.
+ *
+ * Список ЯВНЫЙ и короткий: молчаливое расширение (например «все страницы с
+ * длинным текстом») однажды укроет и маркетинговую.
+ */
+const LEGAL_PAGES = new Set(["oferta", "privacy", "refund", "docs"]);
+
+const PAGE_DIRS = readdirSync(ROOT, { withFileTypes: true })
+ .filter(
+ (e) =>
+ e.isDirectory() &&
+ e.name !== "__tests__" &&
+ e.name !== "_components" &&
+ !LEGAL_PAGES.has(e.name),
+ )
+ .map((e) => e.name)
+ .sort();
+
+const SUBPAGES = PAGE_DIRS.flatMap((dir) =>
+ readdirSync(join(ROOT, dir))
+ .filter((name) => /^page\.tsx?$/u.test(name))
+ .map((name) => `${dir}/${name}`),
+);
+
const V3_SOURCES = [
LANDING_ROOT,
+ ...SUBPAGES,
...readdirSync(join(ROOT, V3_DIR))
.filter((name) => /\.tsx?$/u.test(name) && !GENERATED.includes(name))
.sort()
diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/InnerHeader.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/InnerHeader.tsx
index be973f8f..89a5a042 100644
--- a/tradein-mvp/frontend/src/app/mera-public/_components/InnerHeader.tsx
+++ b/tradein-mvp/frontend/src/app/mera-public/_components/InnerHeader.tsx
@@ -1,4 +1,3 @@
-import { B2B_URL } from "../content";
import styles from "../landing-v3.module.css";
import { PublicLink } from "./PublicLink";
@@ -57,9 +56,9 @@ export function InnerHeader() {
-
+
Для бизнеса
-
+
Проверить квартиру
diff --git a/tradein-mvp/frontend/src/app/mera-public/business/page.tsx b/tradein-mvp/frontend/src/app/mera-public/business/page.tsx
new file mode 100644
index 00000000..a56fcfcf
--- /dev/null
+++ b/tradein-mvp/frontend/src/app/mera-public/business/page.tsx
@@ -0,0 +1,155 @@
+import type { Metadata } from "next";
+
+import { safeUrl } from "@/lib/safeUrl";
+
+import { InnerHeader } from "../_components/InnerHeader";
+import { PublicLink } from "../_components/PublicLink";
+import { FooterV3 } from "../_components/v3/FooterV3";
+import {
+ B2B_URL,
+ SUPPORT_EMAIL,
+ SUPPORT_TELEGRAM_LABEL,
+ SUPPORT_TELEGRAM_URL,
+} from "../content";
+import styles from "../landing-v3.module.css";
+
+/**
+ * «МЕРА для бизнеса» — страница связи, а не вход.
+ *
+ * ЗАЧЕМ ЗАВЕДЕНА. Кнопка «Для бизнеса» в шапке и «МЕРА для бизнеса» в подвале
+ * вели на `B2B_URL`. Реальная цепочка на проде: 308 → `/trade-in` → 307 →
+ * `/trade-in/v2` → клиентский редирект → `/trade-in/login`. Посетитель
+ * публичного сайта упирался в форму «Логин / Пароль» от аккаунта, которого у
+ * него нет и завести который на этом экране нельзя. Владелец просил починить
+ * это дважды.
+ *
+ * ЧЕГО ЗДЕСЬ НЕТ И ПОЧЕМУ. Ни цен, ни сроков ответа, ни объёмов, ни числа
+ * клиентов: таких величин никто не мерил, а на странице они читались бы как
+ * обязательство (то же решение, что в `docs/page.tsx` про часы поддержки).
+ * Формы заявки тоже нет — принимать её в продукте нечем: анонимной ручки для
+ * B2B-лида не существует, и форма, которая никуда не пишет, хуже честной
+ * ссылки на живой канал. Каналы взяты из `content.ts` — те же, что в подвале и
+ * в документах, а не заведены для этой страницы отдельно.
+ *
+ * ССЫЛКА НА ВХОД ОСТАВЛЕНА, но второстепенной: у кого доступ уже есть, тот
+ * пришёл именно за ней, и убрать её значило бы чинить один дефект вторым.
+ */
+
+export const metadata: Metadata = {
+ title: "МЕРА для бизнеса — связаться",
+ // Тот же noindex, что у остального публичного дерева (см. layout.tsx и
+ // docs/page.tsx): страница открыта по адресу, но в выдачу не идёт.
+ robots: { index: false, follow: false },
+};
+
+export default function BusinessPage() {
+ const telegramHref = safeUrl(SUPPORT_TELEGRAM_URL);
+
+ return (
+ <>
+
+
+
+
+
+
ДЛЯ БИЗНЕСА
+
МЕРА для бизнеса
+
+ Оценка квартир по живым объявлениям под своей учётной записью —
+ для агентства, застройщика или компании, которая принимает квартиры
+ в зачёт.
+
+
+
+
+
+ Кому это нужно
+
+
+ Тем, кто считает не одну квартиру, а много и регулярно: агентствам
+ недвижимости, отделам продаж застройщиков, компаниям с программами
+ трейд-ин. Расчёт идёт по тем же источникам, что и бесплатная
+ проверка на сайте, — но под учётными записями вашей команды, с
+ сохранением истории и месячным лимитом на аккаунт.
+
+
+ Если задача разовая — квартира одна, и нужно понять цену, — идти
+ сюда незачем: бесплатная проверка на сайте отвечает на этот вопрос
+ без договорённостей и без регистрации.
+
+
+
+
+
+ Чем отличается от бесплатной проверки
+
+
+
+ Бесплатная проверка
+
+ Одна квартира, здесь и сейчас, без регистрации. Картина рынка
+ вокруг дома: сколько похожих квартир продаётся рядом и как давно
+ висят объявления
+
+
+
+ Бизнес-контур
+
+ Закрытый контур с доступом по учётной записи: история своих
+ расчётов, учётные записи команды, месячный лимит на аккаунт.
+ Доступ выдаётся вручную, после разговора, — самостоятельной
+ регистрации в нём нет
+
+
+
+
+ Что именно войдёт в работу — обсуждается: задача разная у
+ агентства, у застройщика и у трейд-ин программы. Поэтому на этой
+ странице нет ни тарифа, ни срока: назвать их до разговора значило бы
+ их придумать.
+
+
+
+
+
+ Как связаться
+
+
+ Напишите в любой из каналов — коротко, что за компания и какая
+ задача: сколько адресов, как часто, что должно быть на выходе.
+ Этого хватает, чтобы ответить по существу с первого письма.
+
+ Уже работаете с нами? Вход в бизнес-контур —
+ по логину и паролю, которые вам выдали.
+
+
+
+
+ >
+ );
+}
diff --git a/tradein-mvp/frontend/src/app/mera-public/content.ts b/tradein-mvp/frontend/src/app/mera-public/content.ts
index 375dfc94..4164842e 100644
--- a/tradein-mvp/frontend/src/app/mera-public/content.ts
+++ b/tradein-mvp/frontend/src/app/mera-public/content.ts
@@ -195,6 +195,10 @@ export const PUBLIC_ROUTES = {
articles: "/articles",
articleHowTo: "/articles/kak-ocenit-kvartiru",
docs: "/docs",
+ // Страница B2B-контура: связаться и оставить заявку. Заведена 31.08.2026
+ // взамен ссылки на `B2B_URL` в шапке и подвале — та вела в закрытый контур
+ // и заканчивалась формой входа для аккаунта, которого у посетителя нет.
+ business: "/business",
} as const;
/** Канонический публичный origin сервиса — база для canonical-ссылок. */
@@ -209,6 +213,12 @@ export const PUBLIC_ORIGIN = "https://meraocenka.ru";
* осознанно: это ссылка НАРУЖУ, из одного продукта в другой, и подставлять её
* из текущего хоста было бы неверно.
*/
+/**
+ * ⚠️ ЭТО ВХОД В ЗАКРЫТЫЙ КОНТУР, А НЕ ВИТРИНА. Цепочка редиректов приводит на
+ * `/trade-in/login` — форму «Логин / Пароль». Человеку с улицы показывать её
+ * нельзя, поэтому шапка и подвал ведут на `PUBLIC_ROUTES.business`, а этот
+ * адрес остался ссылкой «у меня уже есть доступ» внутри той страницы.
+ */
export const B2B_URL = "https://gendsgn.ru/trade-in/";
/** Короткие публичные адреса тех же страниц (то, что напечатано в документах). */
diff --git a/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx b/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx
index 229e03ec..6299c473 100644
--- a/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx
+++ b/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx
@@ -103,6 +103,7 @@ const PUBLIC_SHORT_PATHS = [
"/articles",
"/articles/kak-ocenit-kvartiru",
"/docs",
+ "/business",
];
export function isPublicPath(rawPath: string): boolean {