«Для бизнеса» вела на форму входа — заведена страница /business #3294

Merged
bot-backend merged 2 commits from feat/mera-business-page into main 2026-08-31 09:14:35 +00:00
9 changed files with 238 additions and 16 deletions

View file

@ -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

View file

@ -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: прежний матчер

View file

@ -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()

View file

@ -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() {
</PublicLink>
<div className={styles.hdrRight}>
<a href={B2B_URL} className={styles.hdrBizLink}>
<PublicLink to="business" className={styles.hdrBizLink}>
Для бизнеса
</a>
</PublicLink>
</div>
</div>
</header>

View file

@ -21,16 +21,20 @@
* а добавляет одну точку входа с тарифами и порядком возврата.
*
* ССЫЛКИ КОРОТКИЕ (15.08.2026). «Проверить квартиру» ведёт на `/estimate`
* отдельный экран проверки, а не на якорь формы в герое. «МЕРА для бизнеса»
* стала абсолютной: относительный `/` на публичном домене вёл на сам лэндинг,
* то есть пункт не работал. Все переходы обычным `<a>` через `PublicLink`,
* потому что `next/link` подставляет basePath (см. его шапку).
* отдельный экран проверки, а не на якорь формы в герое. Все переходы
* обычным `<a>` через `PublicLink`, потому что `next/link` подставляет
* basePath (см. его шапку).
*
* «МЕРА ДЛЯ БИЗНЕСА» ВЕДЁТ НА `/business`, А НЕ НА `B2B_URL` (31.08.2026).
* Абсолютный адрес B2B-контура заканчивался формой входа `/trade-in/login`
* посетителю публичного сайта предлагали залогиниться в аккаунт, которого у
* него нет. Теперь пункт ведёт на публичную страницу связи, а ссылка на вход
* живёт внутри неё, второстепенной.
*/
import { safeUrl } from "@/lib/safeUrl";
import {
B2B_URL,
LEGAL_ENTITY,
REGION_NAME,
SUPPORT_EMAIL,
@ -70,9 +74,9 @@ export function FooterV3() {
<PublicLink to="articles" className={styles.ftrLink}>
Статьи
</PublicLink>
<a href={B2B_URL} className={styles.ftrLink}>
<PublicLink to="business" className={styles.ftrLink}>
МЕРА для бизнеса
</a>
</PublicLink>
</div>
</div>

View file

@ -49,7 +49,6 @@
* был `CityPicker`).
*/
import { B2B_URL } from "../../content";
import styles from "../../landing-v3.module.css";
import { PublicLink } from "../PublicLink";
@ -87,9 +86,9 @@ export function HeaderV3() {
</nav>
<div className={styles.hdrRight}>
<a href={B2B_URL} className={styles.hdrBizLink}>
<PublicLink to="business" className={styles.hdrBizLink}>
Для бизнеса
</a>
</PublicLink>
<a href="#check" className={styles.hdrCta}>
Проверить квартиру
</a>

View file

@ -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 (
<>
<InnerHeader />
<main id="content" className={styles.artPage} tabIndex={-1}>
<nav className={styles.artCrumbs} aria-label="Хлебные крошки">
<PublicLink to="home" className={styles.artCrumbLink}>
Главная
</PublicLink>
<span aria-hidden="true">/</span>
<span aria-current="page">Для бизнеса</span>
</nav>
<header className={styles.artPageHead}>
<div className={styles.repEyebrow}>ДЛЯ БИЗНЕСА</div>
<h1 className={styles.artPageTitle}>МЕРА для бизнеса</h1>
<p className={styles.artPageLead}>
Оценка квартир по живым объявлениям под своей учётной записью
для агентства, застройщика или компании, которая принимает квартиры
в зачёт.
</p>
</header>
<section className={styles.docsSection} aria-labelledby="biz-who">
<h2 id="biz-who" className={styles.docsH2}>
Кому это нужно
</h2>
<p className={styles.artP}>
Тем, кто считает не одну квартиру, а много и регулярно: агентствам
недвижимости, отделам продаж застройщиков, компаниям с программами
трейд-ин. Расчёт идёт по тем же источникам, что и бесплатная
проверка на сайте, но под учётными записями вашей команды, с
сохранением истории и месячным лимитом на аккаунт.
</p>
<p className={styles.artP}>
Если задача разовая квартира одна, и нужно понять цену, идти
сюда незачем: бесплатная проверка на сайте отвечает на этот вопрос
без договорённостей и без регистрации.
</p>
</section>
<section className={styles.docsSection} aria-labelledby="biz-diff">
<h2 id="biz-diff" className={styles.docsH2}>
Чем отличается от бесплатной проверки
</h2>
<div className={styles.docsLinks}>
<PublicLink to="estimate" className={styles.docsLinkCard}>
<span className={styles.docsLinkTitle}>Бесплатная проверка</span>
<span className={styles.docsLinkNote}>
Одна квартира, здесь и сейчас, без регистрации. Картина рынка
вокруг дома: сколько похожих квартир продаётся рядом и как давно
висят объявления
</span>
</PublicLink>
<span className={styles.docsLinkCard}>
<span className={styles.docsLinkTitle}>Бизнес-контур</span>
<span className={styles.docsLinkNote}>
Закрытый контур с доступом по учётной записи: история своих
расчётов, учётные записи команды, месячный лимит на аккаунт.
Доступ выдаётся вручную, после разговора, самостоятельной
регистрации в нём нет
</span>
</span>
</div>
<p className={styles.artP}>
Что именно войдёт в работу обсуждается: задача разная у
агентства, у застройщика и у трейд-ин программы. Поэтому на этой
странице нет ни тарифа, ни срока: назвать их до разговора значило бы
их придумать.
</p>
</section>
<section className={styles.docsSection} aria-labelledby="biz-contact">
<h2 id="biz-contact" className={styles.docsH2}>
Как связаться
</h2>
<p className={styles.artP}>
Напишите в любой из каналов коротко, что за компания и какая
задача: сколько адресов, как часто, что должно быть на выходе.
Этого хватает, чтобы ответить по существу с первого письма.
</p>
<div className={styles.artKv}>
<div className={styles.artKvRow}>
<span className={styles.artKvKey}>ПОЧТА</span>
<span className={styles.artKvVal}>
<a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a>
</span>
</div>
{telegramHref ? (
<div className={styles.artKvRow}>
<span className={styles.artKvKey}>TELEGRAM</span>
<span className={styles.artKvVal}>
<a href={telegramHref} target="_blank" rel="noreferrer">
{SUPPORT_TELEGRAM_LABEL}
</a>
</span>
</div>
) : null}
</div>
<p className={styles.artFootnote}>
Уже работаете с нами? <a href={B2B_URL}>Вход в бизнес-контур</a>
по логину и паролю, которые вам выдали.
</p>
</section>
</main>
<FooterV3 />
</>
);
}

View file

@ -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/";
/** Короткие публичные адреса тех же страниц (то, что напечатано в документах). */

View file

@ -103,6 +103,7 @@ const PUBLIC_SHORT_PATHS = [
"/articles",
"/articles/kak-ocenit-kvartiru",
"/docs",
"/business",
];
export function isPublicPath(rawPath: string): boolean {