From 2d8edd2c4fce5cdbaa0b015e963e737f37e8ab95 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 29 Aug 2026 17:53:44 +0500 Subject: [PATCH] =?UTF-8?q?feat(mera/b2c):=20=D1=80=D0=B0=D0=B7=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=20=D1=81=D1=82=D0=B0=D1=82=D0=B5=D0=B9=20=D0=B8=20?= =?UTF-8?q?=D1=85=D0=B0=D0=B1=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20(=D0=BC=D0=B0=D0=BA=D0=B5=D1=82=2029.08)?= =?UTF-8?q?=20+=20=D1=87=D0=B8=D1=81=D0=BB=D0=B0=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=D0=B8=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Последняя часть макета Макса «B2C модуль для МЕРА»: /articles, /articles/kak-ocenit-kvartiru, /docs. Проводка ссылок доведена до конца — «Статьи» и «Проверьте себя» в шапке и подвале перестали быть заглушками, EstimateHeader переименован в InnerHeader (он теперь стоит на трёх страницах, а не только на проверке). ЧИСЛА СТАТЬИ — НЕ ИЗ МАКЕТА. Макет утверждал «по нашим данным, средний разрыв между первой ценой в объявлении и ценой сделки — 4,1%, а у провисевших дольше трёх месяцев доходит до 9%». Прод-замер (poincare, 29.08) показал, что величины такого рода у нас нет вовсе: связки листинга со сделкой в данных не существует (cadastral_number пуст у всех 108 623 сделок и 111 693 объявлений). Заменено на то, что измеряется парно и честно: по 6202 объявлениям Домклик в ЕКБ (срез 18.07.2026) цену снижали 44%, а среди провисевших дольше трёх месяцев — 62% в медиане на 4,9%. Границы замера названы в самом тексте: это движение цены В ОБЪЯВЛЕНИИ, а не скидка на сделке, и объявлений моложе трёх недель в выборке нет (прогон обогащения их не захватил), поэтому по всему живому рынку доля ниже. Заодно сняты остальные непроверяемые утверждения: «расхождение с кадастровой до 40%» (кадастровой стоимости ПОМЕЩЕНИЙ нет ни в одной базе — проверить нечем), поправки §3 помечены как рыночная практика, а не наш замер, пороги фильтра §2 приведены к тем, что реально стоят в оценщике (радиус 1 км, площадь ±15%, минимум 5 аналогов + каскад послаблений). Плейсхолдеров marketing-v3 в статье не осталось. Периметр закрыт во всех трёх местах: @meraPages / @meraShortSlash / @meraLongPages в apps.caddy, PUBLIC_ROUTES и PUBLIC_SHORT_PATHS в гварде. @meraShortSlash до сих пор не проверялся ничем — добавлен гейт в public-perimeter.test.ts (фальсифицирован: снятие /docs из альтернации красит тест). Гейт #2904 теперь видит и статью (articles-content.ts в V3_SOURCES) — фальсифицировано отдельно: с одним этим файлом в списке снятие noindex красит гейт. Честные отличия от макета — в докстрингах страниц: масок реквизитов и макетной редакции оферты нет (источник правды — /oferta, /privacy, /refund от 13.08), регламент ответа поддержки «9:00-19:00» снят (никто его не устанавливал), фильтр рубрик и обещание «новые статьи каждую неделю» не рендерятся. Проверено: tsc, eslint, vitest 69/69, изоляция mera-public, скриншоты трёх страниц на 1180/375/320px без горизонтального переполнения. --- caddy/sites/apps.caddy | 6 +- .../marketing-placeholders-gate.test.ts | 4 + .../__tests__/public-perimeter.test.ts | 26 ++ .../EstimateHeader.tsx => InnerHeader.tsx} | 21 +- .../mera-public/_components/v3/ArticlesV3.tsx | 59 ++- .../mera-public/_components/v3/FooterV3.tsx | 18 +- .../mera-public/_components/v3/HeaderV3.tsx | 37 +- .../mera-public/articles/articles-content.ts | 181 ++++++++ .../articles/kak-ocenit-kvartiru/page.tsx | 178 ++++++++ .../src/app/mera-public/articles/page.tsx | 98 ++++ .../frontend/src/app/mera-public/content.ts | 4 + .../src/app/mera-public/docs/page.tsx | 191 ++++++++ .../src/app/mera-public/estimate/page.tsx | 4 +- .../src/app/mera-public/landing-v3.module.css | 422 ++++++++++++++++++ .../src/components/auth/RouteGuard.tsx | 12 +- 15 files changed, 1190 insertions(+), 71 deletions(-) rename tradein-mvp/frontend/src/app/mera-public/_components/{estimate/EstimateHeader.tsx => InnerHeader.tsx} (60%) create mode 100644 tradein-mvp/frontend/src/app/mera-public/articles/articles-content.ts create mode 100644 tradein-mvp/frontend/src/app/mera-public/articles/kak-ocenit-kvartiru/page.tsx create mode 100644 tradein-mvp/frontend/src/app/mera-public/articles/page.tsx create mode 100644 tradein-mvp/frontend/src/app/mera-public/docs/page.tsx diff --git a/caddy/sites/apps.caddy b/caddy/sites/apps.caddy index df2f75e0..ef58bab2 100644 --- a/caddy/sites/apps.caddy +++ b/caddy/sites/apps.caddy @@ -351,7 +351,7 @@ meraocenka.ru { # страница: владелец сравнивает его с текущим лэндингом. Оно `noindex` и # ни с одной страницы на него нет ссылки. Убрать эту строку в тот момент, # когда вариант выберут и он станет корнем. - @meraPages path /estimate /oferta /refund /privacy /v3 + @meraPages path /estimate /oferta /refund /privacy /v3 /articles /articles/kak-ocenit-kvartiru /docs handle @meraPages { rewrite * /trade-in/mera-public{path} reverse_proxy tradein-frontend:3000 { @@ -365,7 +365,7 @@ meraocenka.ru { # голый 404 (так было и до этого PR, с момента #2615). Заодно это # замыкает цепочку для длинных адресов со слэшем: они приходят на короткий # со слэшем и здесь нормализуются. - @meraShortSlash path_regexp shortslash ^/(estimate|oferta|refund|privacy|v3)/$ + @meraShortSlash path_regexp shortslash ^/(estimate|oferta|refund|privacy|v3|articles|articles/kak-ocenit-kvartiru|docs)/$ handle @meraShortSlash { redir * /{re.shortslash.1} permanent } @@ -413,7 +413,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/v3 /trade-in/mera-public/v3/ + @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/v3 /trade-in/mera-public/v3/ /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/ handle @meraLongPages { route { uri strip_prefix /trade-in/mera-public diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/marketing-placeholders-gate.test.ts b/tradein-mvp/frontend/src/app/mera-public/__tests__/marketing-placeholders-gate.test.ts index cd7441a6..272b4390 100644 --- a/tradein-mvp/frontend/src/app/mera-public/__tests__/marketing-placeholders-gate.test.ts +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/marketing-placeholders-gate.test.ts @@ -44,6 +44,10 @@ const V3_SOURCES = [ // Макет 29.08: лента и игра тоже стоят на реквизитных величинах. "_components/v3/DealsTickerV3.tsx", "_components/v3/GuessGameV3.tsx", + // Раздел статей (29.08) — плитки §4 статьи берут те же плейсхолдеры. Без + // этой строки гейт остался бы зелёным при снятии noindex: он сканировал бы + // только лэндинг, а величины уже живут и на статье. + "articles/articles-content.ts", ]; const PLACEHOLDER_EXPORTS = [ diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/public-perimeter.test.ts b/tradein-mvp/frontend/src/app/mera-public/__tests__/public-perimeter.test.ts index 5e7949c1..d7150cbc 100644 --- a/tradein-mvp/frontend/src/app/mera-public/__tests__/public-perimeter.test.ts +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/public-perimeter.test.ts @@ -147,6 +147,32 @@ describe("короткие адреса публичного домена", () = ).toBe(true); } }); + + /** + * ТРЕТИЙ СПИСОК, до 29.08.2026 не проверявшийся ничем. `@meraPages path` + * матчит адрес БЕЗ завершающего слэша, поэтому `/docs/` мимо него проходит и + * улетает в catch-all 404. Нормализует такие адреса отдельный матчер + * `@meraShortSlash`, и его альтернация — ещё одно место, где список страниц + * можно молча не дополнить. Ровно тот же класс дефекта, ради которого + * заведены две проверки выше; здесь он до сих пор был не закрыт. + */ + it("каждый короткий адрес нормализуется и со слэшем на конце", () => { + const slashLine = + meraBlock.match(/@meraShortSlash path_regexp \w+ \^\/\(([^)]+)\)/)?.[1] ?? + ""; + const alternatives = slashLine.split("|").filter(Boolean); + expect( + alternatives.length, + "не найдена альтернация @meraShortSlash — зонд сломан, проверка ниже пройдёт вхолостую", + ).toBeGreaterThan(0); + for (const route of [...Object.values(PUBLIC_ROUTES), ...PREVIEW_ONLY]) { + if (route === "/") continue; // корень слэшем и так заканчивается + expect( + alternatives.includes(route.slice(1)), + `${route}/ (со слэшем) не нормализуется — адрес отдаст 404 вместо 301`, + ).toBe(true); + } + }); }); describe("basePath не протекает в публичные ссылки", () => { diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/estimate/EstimateHeader.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/InnerHeader.tsx similarity index 60% rename from tradein-mvp/frontend/src/app/mera-public/_components/estimate/EstimateHeader.tsx rename to tradein-mvp/frontend/src/app/mera-public/_components/InnerHeader.tsx index c1bc65ca..318afde2 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/estimate/EstimateHeader.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/_components/InnerHeader.tsx @@ -1,21 +1,22 @@ -import { B2B_URL } from "../../content"; -import styles from "../../landing-v3.module.css"; -import { PublicLink } from "../PublicLink"; +import { B2B_URL } from "../content"; +import styles from "../landing-v3.module.css"; +import { PublicLink } from "./PublicLink"; /** - * Шапка страницы проверки — намеренно короче лэндинговой (`v3/HeaderV3.tsx`). + * Короткая шапка внутренних страниц — проверка (`estimate/`), статьи + * (`articles/`), документы (`docs/`). Намеренно короче лэндинговой + * (`v3/HeaderV3.tsx`). * * Из неё убраны навигация по секциям лэндинга, селектор города и CTA - * «Проверить за 30 секунд». Причина у всех трёх одна: человек уже на экране - * проверки. Якоря вроде `#accuracy` тут просто не на что навести (секций нет - * на этой странице), город выбирается прямо в форме — а второй селектор - * города в шапке означал бы два места, задающих одно значение, — а CTA вёл бы - * на страницу, где посетитель уже стоит. + * «Проверить за 30 секунд». Причина у всех трёх одна: человек уже ушёл с + * лэндинга. Якоря вроде `#accuracy` тут просто не на что навести (секций нет + * на этих страницах), город выбирается прямо в форме проверки — а второй + * селектор города в шапке означал бы два места, задающих одно значение. * * Логотип отсюда — ссылка на главную: это единственный обратный путь на * лэндинг, и он же то, что человек инстинктивно нажимает. */ -export function EstimateHeader() { +export function InnerHeader() { return (
diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/ArticlesV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/ArticlesV3.tsx index e3dc1102..714718dc 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/ArticlesV3.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/ArticlesV3.tsx @@ -1,34 +1,13 @@ /** * ArticlesV3 — «Разборы на данных» (макет 29.08, секция «V3 — статьи»). * - * Раздела статей в продукте НЕ СУЩЕСТВУЕТ (страницы «МЕРА статьи»/«МЕРА - * статья» из макета — следующий шард). Поэтому карточки рендерятся
, - * а НЕ : ссылка «в никуда» на превью читалась бы как сломанная навигация. - * Ссылка «Все статьи →» по той же причине не рендерится вовсе — появится - * вместе с разделом. Темы и рубрики карточек — из макета дословно. + * Раздел статей существует с 29.08: живая статья одна — её карточка ведёт на + * страницу; остальные две честно помечены «СКОРО» и ссылками не являются. */ +import { PublicLink } from "../PublicLink"; import styles from "../../landing-v3.module.css"; - -interface ArticleCard { - readonly rubric: string; - readonly title: string; -} - -const ARTICLES: readonly ArticleCard[] = [ - { - rubric: "ЦЕНООБРАЗОВАНИЕ · 6 МИН", - title: "Как оценить квартиру для продажи — без риелтора и «на глазок»", - }, - { - rubric: "СРОКИ · 5 МИН", - title: "Сколько продаётся квартира в 2026 году и что «подвешивает» объявление", - }, - { - rubric: "ТОРГ · 7 МИН", - title: "Завысить и торговаться или поставить в рынок: что выгоднее", - }, -]; +import { ARTICLE_CARDS } from "../../articles/articles-content"; export function ArticlesV3() { return ( @@ -37,14 +16,32 @@ export function ArticlesV3() {

Разборы на данных

+ + Все статьи → +
- {ARTICLES.map((a) => ( -
-
{a.rubric}
-
{a.title}
-
- ))} + {ARTICLE_CARDS.map((a) => + a.route ? ( + +
{a.rubric}
+
{a.title}
+
{a.lead}
+
Читать →
+
+ ) : ( +
+
{a.rubric}
+
{a.title}
+
{a.lead}
+
СКОРО
+
+ ), + )}
); diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/FooterV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/FooterV3.tsx index e8087ab1..9a313872 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/FooterV3.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/FooterV3.tsx @@ -14,8 +14,11 @@ * приём платежей не подключён (см. TODO у нижней строки) — заявлять способы * оплаты, которых нет, подпадает под ст. 5 ФЗ «О рекламе». * - * «Статьи» в «Разделах» неактивны тем же образом, что в шапке (HeaderV3): - * раздела нет в коде. + * «Статьи» в «Разделах» стали ссылкой 29.08.2026 — раздел появился + * (`articles/`). До этого пункт был неактивен тем же образом, что в шапке + * (HeaderV3). «Документы и условия» — хаб `docs/`: он НЕ заменяет прямые + * ссылки на оферту/возврат/политику (они обязаны быть в подвале и остаются), + * а добавляет одну точку входа с тарифами и порядком возврата. * * ССЫЛКИ — КОРОТКИЕ (15.08.2026). «Проверить квартиру» ведёт на `/estimate` — * отдельный экран проверки, а не на якорь формы в герое. «МЕРА для бизнеса» @@ -64,13 +67,9 @@ export function FooterV3() { Проверить квартиру - + Статьи - +
МЕРА для бизнеса @@ -115,6 +114,9 @@ export function FooterV3() { Обработка персональных данных + + Документы и условия + diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/HeaderV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/HeaderV3.tsx index 9eb91105..49f98774 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/HeaderV3.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/HeaderV3.tsx @@ -5,16 +5,17 @@ * Логотип НЕ ссылка: на публичном домене корень — это сам лэндинг, то есть * ссылка вела бы на текущую же страницу. * - * НЕАКТИВНЫЕ ПУНКТЫ. «Статьи», «Проверьте себя» и «Продажа под ключ» — - * разделов нет в коде: ни маршрута, ни секции. Раньше три якоря вели на - * `/mera-public#game` и `#turnkey`, которых не существует НИГДЕ, — то есть - * клик уводил на другую страницу и там ничего не находил. Пункты приведены к - * тому же виду, что «Статьи»: видны, но не кликабельны, пока раздела нет. + * НЕАКТИВНЫЕ ПУНКТЫ (29.08.2026). Осталась одна «Продажа под ключ» — + * партнёрской формы нет ни в коде, ни у владельца, и пункт остаётся видимым, + * но некликабельным. «Проверьте себя» и «Статьи» раньше стояли рядом с ней по + * той же причине; теперь секция игры (`#game`, GuessGameV3) и раздел статей + * (`/articles`) существуют, поэтому оба пункта снова ведут по назначению. + * Правило прежнее: пункт становится ссылкой ТОЛЬКО когда цель существует. * * ЯКОРЯ — голые (`#accuracy`, `#check`), потому что эта шапка рендерится * только на странице, где эти секции и лежат. На юридических страницах стоит * другая шапка (`_components/SiteHeader.tsx`), на `/estimate` — своя, - * укороченная (`_components/estimate/EstimateHeader.tsx`): туда переносить + * укороченная (`_components/InnerHeader.tsx`): туда переносить * навигацию по секциям лэндинга незачем, там человек уже пришёл считать. * * «Для бизнеса» ведёт на АБСОЛЮТНЫЙ адрес B2B-контура: относительный «/» на @@ -26,6 +27,7 @@ import { B2B_URL } from "../../content"; import styles from "../../landing-v3.module.css"; +import { PublicLink } from "../PublicLink"; import { CityPicker } from "./CityPicker"; @@ -47,16 +49,19 @@ export function HeaderV3() { Точность - {["Проверьте себя", "Продажа под ключ", "Статьи"].map((name) => ( - - {name} - - ))} + + Проверьте себя + + + Продажа под ключ + + + Статьи +
diff --git a/tradein-mvp/frontend/src/app/mera-public/articles/articles-content.ts b/tradein-mvp/frontend/src/app/mera-public/articles/articles-content.ts new file mode 100644 index 00000000..da7996db --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/articles/articles-content.ts @@ -0,0 +1,181 @@ +/** + * Контент раздела «Статьи» — макет 29.08 («МЕРА статья.dc.html», текст Макса). + * + * ЧИСЛА ИЗ МАКЕТА СНЯТЫ (29.08.2026). Макет утверждал «по нашим данным, + * средний разрыв между первой ценой в объявлении и ценой сделки — 4,1%, а у + * провисевших дольше трёх месяцев доходит до 9%», «расхождение с кадастровой + * до 40%» и плитки −4,3% / ×2,4 / 83%. Прод-замер (poincare, 29.08.2026) + * не подтвердил ни одно: + * + * | макет | что показал замер | + * | разрыв объявление↔сделка | ИЗМЕРИТЬ НЕЛЬЗЯ: связки листинга со сделкой | + * | | в данных нет (cadastral_number пуст у всех | + * | | 108 623 сделок и 111 693 объявлений) | + * | «до 9%» у долгостоя | 62% снижали, медиана −4,9% (n=2513) | + * | кадастровая «до 40%» | ПРОВЕРИТЬ НЕЧЕМ: кадастровой стоимости | + * | | ПОМЕЩЕНИЙ нет ни в одной базе | + * | −4,3% за месяц простоя | медиана внутрилистинговой просадки −1,1%/мес | + * | ×2,4 дольше продаётся | 79 дн против 76 → ×1,04 | + * | 83% в ценовой коридор | коридор шириной 47% от цены — не точность | + * + * Поэтому §1 и §4 стоят на СВОЁМ замере (см. поля `source` — там основание и + * его границы), кадастровое число снято, а поправки §3 честно помечены как + * рыночная практика, а не наш замер. Плейсхолдеров marketing-v3 в статье + * больше нет — но строка `articles/articles-content.ts` в `V3_SOURCES` гейта + * #2904 оставлена намеренно: вернётся плейсхолдер — гейт его увидит. + * + * ЗАМЕР, НА КОТОРОМ СТОИТ ТЕКСТ. 6202 объявления квартир Домклик в + * Екатеринбурге, срез 18.07.2026, парно «цена публикации → цена на дату + * среза». Две границы названы прямо в тексте, потому что без них число врёт: + * (1) это движение ЗАПРАШИВАЕМОЙ цены, а не скидка на сделке; (2) объявлений + * моложе трёх недель в выборке нет вовсе (прогон обогащения 18.07 их не + * захватил), а они снижают цену реже всех — поэтому доля по всему живому + * рынку ниже примерно на 3 п.п. Цифра по долгостою от этой дыры не зависит. + * Дерево mera-public закрыто noindex (layout.tsx). + */ + +import { SERVICE_PRICE_RUB } from "../content"; + +export interface ArticleCardMeta { + /** + * Ключ маршрута в `PUBLIC_ROUTES` — или `null`, если статьи ещё нет. + * Именно ключ, а не слаг: карточка сама знает, куда ведёт, и добавление + * второй статьи не может увести её на первую (так и было, пока + * `to="articleHowTo"` стоял константой в JSX). + */ + readonly route: "articleHowTo" | null; + readonly rubric: string; + readonly title: string; + /** Подводка карточки — дословно из макета. */ + readonly lead: string; +} + +/** Карточки списка. slug=null — статьи ещё нет (карточка без ссылки). */ +export const ARTICLE_CARDS: readonly ArticleCardMeta[] = [ + { + route: "articleHowTo", + rubric: "ЦЕНООБРАЗОВАНИЕ · 6 МИН", + title: "Как оценить квартиру для продажи — без риелтора и «на глазок»", + lead: "Почему цена соседа — не ориентир, и как считать по аналогам.", + }, + { + route: null, + rubric: "СРОКИ · 5 МИН", + title: "Сколько продаётся квартира в 2026 году и что «подвешивает» объявление", + lead: "Средние сроки по типам квартир и главные причины простоя.", + }, + { + route: null, + rubric: "ТОРГ · 7 МИН", + title: "Завысить и торговаться или поставить в рынок: что выгоднее", + lead: "Считаем на данных, сколько теряет «висящая» квартира каждый месяц.", + }, +]; + +export const HOWTO = { + rubric: "ЦЕНООБРАЗОВАНИЕ · 6 МИН ЧТЕНИЯ", + title: "Как оценить квартиру для продажи — без риелтора и «на глазок»", + lead: [ + "Большинство продавцов назначают цену одним из трёх способов: смотрят объявление соседа, берут кадастровую стоимость или спрашивают знакомого риелтора. Все три дают ошибку в обе стороны — и обе стоят денег. Завышенная цена держит квартиру в продаже месяцами, заниженная уносит сотни тысяч сразу.", + "Правильный способ один: сравнение с реальными аналогами. Ниже — как сделать это самому за вечер.", + ], + /** + * Оглавление. `id` — тот же якорь, что стоит у заголовка секции ниже: + * страница рендерит `id` ИЗ ЭТОГО списка, а не из захардкоженных «s1…s5», + * иначе перестановка пункта молча увела бы ссылку не туда. + */ + toc: [ + { id: "s1", title: "Почему цена соседа — плохой ориентир" }, + { id: "s2", title: "Кто такие «аналоги» и как их отобрать" }, + { id: "s3", title: "Считаем цену за метр" }, + { id: "s4", title: "Проверяем цену сроком продажи" }, + { id: "s5", title: "Пять ошибок, которые стоят дороже всего" }, + ], + s1: { + id: "s1", + heading: "1. Почему цена соседа — плохой ориентир", + body: "В объявлении вы видите цену предложения, а не сделки. Между ними почти всегда есть разрыв: продавец заложил торг, а покупатель этот торг забрал. Насколько именно — не знает никто: цены сделок Росреестр публикует без привязки к объявлению, и сопоставить одно с другим нельзя. Зато отлично видно, как продавец сам двигает цену, пока квартира не продаётся. По объявлениям Екатеринбурга, провисевшим дольше трёх месяцев, цену снижали 62% продавцов — в медиане на 4,9% от той, с которой они начинали. В первый месяц продажи снижают только 7%.", + source: + "ЗАМЕР 18.07.2026 · 6202 ОБЪЯВЛЕНИЯ ДОМКЛИК В ЕКАТЕРИНБУРГЕ, ПРОВИСЕВШИЕ НЕ МЕНЬШЕ ТРЁХ НЕДЕЛЬ · ЭТО ДВИЖЕНИЕ ЦЕНЫ В ОБЪЯВЛЕНИИ, А НЕ СКИДКА НА СДЕЛКЕ", + memo: "Ориентироваться нужно на цены сделок, а не на цены объявлений. Объявление говорит, чего хочет продавец; сделка — что готов заплатить рынок.", + }, + s2: { + id: "s2", + heading: "2. Кто такие «аналоги» и как их отобрать", + body: "Аналог — это квартира, за которую тот же покупатель рассматривал бы вашу. Не «такая же по площади», а взаимозаменяемая по совокупности параметров. Рабочий фильтр выглядит так:", + filter: [ + { k: "РАССТОЯНИЕ", v: "до 1 км от вашего дома — дальше начинается другой рынок" }, + { k: "ПЛОЩАДЬ", v: "±15% от вашей — «двушка 54 м²» и «двушка 68 м²» это разные товары" }, + { k: "КОМНАТЫ", v: "ровно столько же; студия и однушка не взаимозаменяемы" }, + { k: "ТИП ДОМА", v: "панель к панели, монолит к монолиту, год постройки ±10 лет" }, + { k: "ЭТАЖ", v: "не первый и не последний в отдельную группу — по ним скидка своя" }, + ], + after: + "Если под фильтр попадает меньше пяти квартир — расширяйте радиус, а не критерии. Сравнивать «двушку» с «трёшкой» бессмысленно, а соседний квартал обычно живёт по той же цене. МЕРА поступает так же: сначала перестаёт требовать близкий год постройки, потом расширяет радиус до 2 км, потом допускает площадь ±25% и соседнюю комнатность — и лишь в крайнем случае уходит на 3 и 5 км.", + footnote: + "ЭТО ТЕ ЖЕ ПОРОГИ, ЧТО СТОЯТ В ОЦЕНЩИКЕ МЕРЫ: РАДИУС 1 КМ, ПЛОЩАДЬ ±15%, МИНИМУМ ПЯТЬ АНАЛОГОВ", + }, + s3: { + id: "s3", + heading: "3. Считаем цену за метр и приводим к своей квартире", + body: "Возьмите отобранные аналоги, посчитайте по каждому цену за квадратный метр и отбросьте два крайних значения — сверху и снизу. Медиана оставшихся и есть база вашей цены. Дальше добавьте поправки.", + factors: [ + { name: "Первый или последний этаж", value: "−5…−8%" }, + { name: "Требуется косметический ремонт", value: "−4…−7%" }, + { name: "Окна во двор, не на трассу", value: "+2…+4%" }, + { name: "Свежий ремонт «под ключ»", value: "+3…+6%" }, + { name: "Пешая доступность метро (до 10 мин)", value: "+5…+9%" }, + { name: "Свободная продажа, без ипотечных обременений", value: "+1…+3%" }, + ], + footnote: + "ОРИЕНТИРЫ РЫНОЧНОЙ ПРАКТИКИ, А НЕ ЗАМЕР МЕРЫ · В КОНКРЕТНОМ ДОМЕ ПОПРАВКА ОТЛИЧАЕТСЯ", + }, + s4: { + id: "s4", + heading: "4. Проверяем цену сроком продажи", + body: "Цена без срока — половина ответа. Одна и та же квартира уйдёт за 30 дней по одной цене и за 90 — по другой. Проверить свою цену сроком можно и без нас: посмотрите, как часто продавцы вокруг сдаются и снижают её. Чем дольше объявление висит, тем чаще это происходит.", + tiles: [ + { value: "7%", label: "снижают цену в первый месяц продажи" }, + { value: "38%", label: "снижают на втором и третьем месяце" }, + { + value: "62%", + label: "снижают, если квартира висит дольше трёх месяцев", + }, + ], + source: + "ТОТ ЖЕ ЗАМЕР · САМЫХ СВЕЖИХ ОБЪЯВЛЕНИЙ (МОЛОЖЕ ТРЁХ НЕДЕЛЬ) В ВЫБОРКЕ НЕТ, А ОНИ СНИЖАЮТ РЕЖЕ ВСЕХ — ПО ВСЕМУ ЖИВОМУ РЫНКУ ДОЛЯ БЫЛА БЫ НИЖЕ", + }, + cta: { + eyebrow: "ЕСЛИ СЧИТАТЬ ВРУЧНУЮ НЕКОГДА", + title: "МЕРА делает всё это за 30 секунд", + body: `Мы находим аналоги, считаем медиану, применяем поправки и проверяем цену сроком продажи. Бесплатно покажем рынок вокруг, за ${SERVICE_PRICE_RUB} ₽ — вашу цену и прогноз.`, + }, + s5: { + id: "s5", + heading: "5. Пять ошибок, которые стоят дороже всего", + mistakes: [ + { + title: "Закладывать «запас на торг» в 10%", + body: "Покупатель не увидит вашу квартиру в фильтре по цене — торговаться будет некому.", + }, + { + title: "Считать по кадастровой стоимости", + body: "Она нужна для налога, а не для рынка: её считают массовой моделью по площади, типу и году дома, не заходя в квартиру. С ценой конкретного объекта она связана слабо — и в обе стороны.", + }, + { + title: "Добавлять к цене стоимость ремонта", + body: "Рынок оплачивает состояние, а не ваши чеки. Ремонт возвращается частично и не всегда.", + }, + { + title: "Сравнивать с квартирой, которая уже полгода висит", + body: "Это не ориентир, а пример того, как делать не надо.", + }, + { + title: "Не пересматривать цену через месяц", + body: "Если за 30 дней нет ни одного показа — цена выше рынка, и рынок вам об этом уже сказал.", + }, + ], + }, + summary: + "Смотрите на цены сделок, а не объявлений. Берите 5–14 действительно взаимозаменяемых аналогов, считайте медиану цены за метр, применяйте поправки и обязательно проверяйте цену сроком продажи. Если считать некогда — это ровно то, что МЕРА делает автоматически.", +} as const; diff --git a/tradein-mvp/frontend/src/app/mera-public/articles/kak-ocenit-kvartiru/page.tsx b/tradein-mvp/frontend/src/app/mera-public/articles/kak-ocenit-kvartiru/page.tsx new file mode 100644 index 00000000..055587d0 --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/articles/kak-ocenit-kvartiru/page.tsx @@ -0,0 +1,178 @@ +import type { Metadata } from "next"; + +import { InnerHeader } from "../../_components/InnerHeader"; +import { PublicLink } from "../../_components/PublicLink"; +import { FooterV3 } from "../../_components/v3/FooterV3"; +import styles from "../../landing-v3.module.css"; +import { ARTICLE_CARDS, HOWTO } from "../articles-content"; + +/** + * Статья «Как оценить квартиру для продажи» — макет 29.08 («МЕРА статья»), + * текст из макета целиком. Числа — реквизит макета: см. шапку + * articles-content.ts; страница, как и всё дерево, под noindex из layout. + * + * Серверный компонент без клиентского JS: содержание — якорные ссылки, + * секции — обычные заголовки с id. + */ + +export const metadata: Metadata = { + title: `${HOWTO.title} — МЕРА`, + robots: { index: false, follow: false }, +}; + +export default function HowToArticlePage() { + return ( + <> + +
+ + +
+
{HOWTO.rubric}
+

{HOWTO.title}

+
+ + + +
+ {HOWTO.lead.map((p) => ( +

+ {p} +

+ ))} + +

+ {HOWTO.s1.heading} +

+

{HOWTO.s1.body}

+
{HOWTO.s1.source}
+ + +

+ {HOWTO.s2.heading} +

+

{HOWTO.s2.body}

+
+ {HOWTO.s2.filter.map((row) => ( +
+ {row.k} + {row.v} +
+ ))} +
+
{HOWTO.s2.footnote}
+

{HOWTO.s2.after}

+ +

+ {HOWTO.s3.heading} +

+

{HOWTO.s3.body}

+
+
+ ФАКТОР + + ПОПРАВКА К ЦЕНЕ + +
+ {HOWTO.s3.factors.map((f) => ( +
+ {f.name} + + {f.value} + +
+ ))} +
+
{HOWTO.s3.footnote}
+ +

+ {HOWTO.s4.heading} +

+

{HOWTO.s4.body}

+
+ {HOWTO.s4.tiles.map((t) => ( +
+
{t.value}
+
{t.label}
+
+ ))} +
+
{HOWTO.s4.source}
+ +
+
{HOWTO.cta.eyebrow}
+
{HOWTO.cta.title}
+

{HOWTO.cta.body}

+ + Проверить за 30 секунд + +
+ +

+ {HOWTO.s5.heading} +

+
    + {HOWTO.s5.mistakes.map((m, i) => ( +
  1. + + {String(i + 1).padStart(2, "0")} + +
    +
    {m.title}
    +
    {m.body}
    +
    +
  2. + ))} +
+ +
+
КОРОТКО
+

{HOWTO.summary}

+
+
+ +
+
+

Читайте дальше

+ + Все статьи → + +
+
+ {ARTICLE_CARDS.filter((c) => c.route !== "articleHowTo").map( + (c) => ( +
+
{c.rubric}
+
{c.title}
+
{c.lead}
+
СКОРО
+
+ ), + )} +
+
+
+ + + ); +} diff --git a/tradein-mvp/frontend/src/app/mera-public/articles/page.tsx b/tradein-mvp/frontend/src/app/mera-public/articles/page.tsx new file mode 100644 index 00000000..646bdcc1 --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/articles/page.tsx @@ -0,0 +1,98 @@ +import type { Metadata } from "next"; + +import { InnerHeader } from "../_components/InnerHeader"; +import { PublicLink } from "../_components/PublicLink"; +import { FooterV3 } from "../_components/v3/FooterV3"; +import { SERVICE_PRICE_RUB } from "../content"; +import styles from "../landing-v3.module.css"; +import { ARTICLE_CARDS } from "./articles-content"; + +/** + * Список статей — макет 29.08 («МЕРА статьи»). + * + * ОТЛИЧИЯ ОТ МАКЕТА И ПОЧЕМУ: + * - В макете девять карточек и подпись «показано 9 из 9 · новые статьи — + * каждую неделю». Написана одна. Остальные две (их рубрики и подводки взяты + * из макета) стоят карточками без ссылки с меткой «СКОРО», а обещания + * еженедельного выхода нет: расписания публикаций никто не устанавливал. + * - Фильтр по рубрикам («Все · Ценообразование · Сроки · Торг · Процесс») не + * рендерится: на трёх карточках фильтровать нечего, а неработающий контрол + * хуже отсутствующего. + * - Шапка — короткая (`InnerHeader`), а не лэндинговая, как в макете: + * лэндинговая содержит якоря на секции (`#accuracy`, `#game`), которых на + * этой странице нет, и клик по ним никуда бы не привёл. + */ + +export const metadata: Metadata = { + title: "Статьи — МЕРА · разборы на данных", + robots: { index: false, follow: false }, +}; + +export default function ArticlesPage() { + return ( + <> + +
+ + +
+
СТАТЬИ И ГАЙДЫ
+

+ Продажа квартиры: разборы на данных, а не на мнениях +

+

+ Как отбирать аналоги, считать цену за метр и проверять её сроком + продажи. Каждый разбор заканчивается практическим шагом — + бесплатной проверкой вашей квартиры. +

+
+ +
+ {ARTICLE_CARDS.map((c) => + c.route ? ( + +
{c.rubric}
+
{c.title}
+
{c.lead}
+
Читать →
+
+ ) : ( +
+
{c.rubric}
+
{c.title}
+
{c.lead}
+
СКОРО
+
+ ), + )} +
+ +
+
+
+ Хватит читать — проверьте свою квартиру +
+

+ Бесплатно: сколько похожих квартир продаётся рядом и насколько + свежие это объявления. Полный отчёт — {SERVICE_PRICE_RUB} ₽. +

+
+ + Проверить за 30 секунд + +
+
+ + + ); +} diff --git a/tradein-mvp/frontend/src/app/mera-public/content.ts b/tradein-mvp/frontend/src/app/mera-public/content.ts index 77d86fa2..375dfc94 100644 --- a/tradein-mvp/frontend/src/app/mera-public/content.ts +++ b/tradein-mvp/frontend/src/app/mera-public/content.ts @@ -191,6 +191,10 @@ export const PUBLIC_ROUTES = { oferta: "/oferta", refund: "/refund", privacy: "/privacy", + // Макет 29.08: раздел статей и хаб документов. + articles: "/articles", + articleHowTo: "/articles/kak-ocenit-kvartiru", + docs: "/docs", } as const; /** Канонический публичный origin сервиса — база для canonical-ссылок. */ diff --git a/tradein-mvp/frontend/src/app/mera-public/docs/page.tsx b/tradein-mvp/frontend/src/app/mera-public/docs/page.tsx new file mode 100644 index 00000000..0a71dd91 --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/docs/page.tsx @@ -0,0 +1,191 @@ +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 { + LEGAL_ENTITY, + SERVICE_PRICE_RUB, + SUPPORT_EMAIL, + SUPPORT_TELEGRAM_LABEL, + SUPPORT_TELEGRAM_URL, +} from "../content"; +import styles from "../landing-v3.module.css"; + +/** + * Хаб «Документы и условия» — макет 29.08 («МЕРА документы»). + * + * ЧЕСТНЫЕ АДАПТАЦИИ: + * - Юридические тексты НЕ дублируются и не переписываются макетной редакцией: + * источник правды — существующие /oferta, /privacy, /refund (опубликованы + * юридической волной 13.08). Хаб только ведёт к ним. + * - Реквизиты из макета (ИНН 66700XXXXX, [АДРЕС], mera.example) — маска, а не + * данные; до появления настоящих не публикуются — то же решение, что в + * FooterV3. + * - Тарифная строка «Продажа под ключ» повторяет ПУТЬ 2 лендинга + * (`TwoPathsV3`) и ничего сверх него не обещает: формы заявки в продукте + * нет, единственный живой канал — поддержка в Telegram. Цена — «по + * договору», а не число: тарифа под ключ в коде не существует. + * - Часов работы поддержки страница НЕ называет. В макете стояло «по будням + * 9:00-19:00», но регламента ответа никто не устанавливал, а срок ответа на + * странице документов читается как обязательство. + * - Цена расчёта берётся из `SERVICE_PRICE_RUB` — того же литерала, что + * назван в оферте (п. 4.1) и политике возврата. Второй раз числом её здесь + * писать нельзя: тогда цена в документах и цена в тарифах смогут разойтись. + * + * ⚠️ ЧЕГО ЭТА СТРАНИЦА НЕ ДОКАЗЫВАЕТ. «Разовый платёж» и «никаких подписок и + * автосписаний» описывают порядок оплаты из оферты, а не работающий контур: + * приём платежей не подключён (`PUBLIC_ESTIMATE_ENABLED = false`, таблицы + * `payments`/`payment_entitlements` на проде пусты, см. TODO в FooterV3). + * Пока дерево под noindex, расхождения «написано против доступно» никто не + * увидит; открывать страницу наружу можно только вместе с платёжным контуром + * (#2896) — либо сняв тарифную таблицу. + */ + +export const metadata: Metadata = { + title: "Документы и условия — МЕРА", + robots: { index: false, follow: false }, +}; + +const TARIFFS: readonly { + readonly name: string; + readonly note: string; + readonly price: string; +}[] = [ + { + name: "Бесплатная проверка", + note: "Число аналогов рядом и возраст объявлений по дому", + price: "0 ₽", + }, + { + name: "Полный отчёт", + note: "Цена, прогноз срока, диапазон, аналоги, факторы. Разовый платёж", + price: `${SERVICE_PRICE_RUB} ₽`, + }, + { + name: "Продажа под ключ", + note: "Объявление, фото, показы, переговоры, сделка — если вести продажу самому некогда. Условия обсуждаются в поддержке", + price: "по договору", + }, +] as const; + +export default function DocsPage() { + const telegramHref = safeUrl(SUPPORT_TELEGRAM_URL); + + return ( + <> + +
+ + +
+
ДОКУМЕНТЫ
+

Документы и условия

+
+ +
+ +
+ + Публичная оферта + + Договор оказания информационных услуг + + + + Персональные данные + + Политика обработки и ваши права по 152-ФЗ + + + + Возврат оплаты + + Когда и как возвращаются {SERVICE_PRICE_RUB} ₽ + + +
+
+ +
+

+ Тарифы +

+

+ Никаких подписок и автосписаний. Все цены — окончательные. +

+
+
+ УСЛУГА + + ЦЕНА + +
+ {TARIFFS.map((t) => ( +
+ + {t.name} + {t.note} + + + {t.price} + +
+ ))} +
+
+ +
+

+ Контакты и реквизиты +

+ {LEGAL_ENTITY ? ( +
+
+ ОРГАНИЗАЦИЯ + {LEGAL_ENTITY.name} +
+
+ ИНН · ОГРН + + {LEGAL_ENTITY.inn} · {LEGAL_ENTITY.ogrn} + +
+
+ АДРЕС + {LEGAL_ENTITY.address} +
+
+ ПОЧТА + + {SUPPORT_EMAIL} + +
+
+ ) : null} + {telegramHref ? ( +

+ Поддержка в Telegram:{" "} + + {SUPPORT_TELEGRAM_LABEL} + + . Это же единственный канал для вопросов по возврату и по продаже + под ключ. +

+ ) : null} +
+
+ + + ); +} diff --git a/tradein-mvp/frontend/src/app/mera-public/estimate/page.tsx b/tradein-mvp/frontend/src/app/mera-public/estimate/page.tsx index a2545770..42b52822 100644 --- a/tradein-mvp/frontend/src/app/mera-public/estimate/page.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/estimate/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { EstimateFlow } from "../_components/estimate/EstimateFlow"; -import { EstimateHeader } from "../_components/estimate/EstimateHeader"; +import { InnerHeader } from "../_components/InnerHeader"; import { FooterV3 } from "../_components/v3/FooterV3"; import { PUBLIC_ESTIMATE_URL } from "../content"; import styles from "../landing-v3.module.css"; @@ -35,7 +35,7 @@ export const metadata: Metadata = { export default function MeraEstimatePage() { return ( <> - +

БЕСПЛАТНАЯ ПРОВЕРКА

diff --git a/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css b/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css index 367eb195..b8ad0f37 100644 --- a/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css +++ b/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css @@ -56,6 +56,10 @@ align-items: center; gap: 12px; color: var(--b2c-ink); + /* На лэндинге логотип —
, на внутренних страницах (InnerHeader) — + ссылка на главную; без сброса подчёркивание браузера рисовалось прямо + по слову «МЕРА». */ + text-decoration: none; } .hdrLogoIcon { @@ -1812,6 +1816,9 @@ text-align: center; cursor: pointer; box-sizing: border-box; + /* На лэндинге это