feat(mera): две статьи на собственном замере вместо карточек «СКОРО»
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m27s

Раздел /articles с 29.08 стоял с одной живой статьёй и двумя карточками без
ссылок. Обе дописаны — но не по темам из макета: те темы данными не
обеспечены.

«Сколько стоит метр в Екатеринбурге: цены сделок, а не объявлений».
24 535 договоров Росреестра за июль 2025 — июнь 2026, регион 66. Медиана
123 626 ₽/м². Несущий факт статьи контринтуитивен и проверен: кривая цены
метра по комнатности не падает, а прогибается — дешевле всего метр в
двухкомнатной (116 910 ₽), дороже всего в студии (134 507 ₽). Плюс
поквартальная динамика за все десять доступных кварталов (+20,6%) и восемь
городов области с порогом публикации в сто сделок.

«Почему квартира не продаётся: что происходит с ценой, пока вы ждёте».
49 620 объявлений области с историей цены площадок. Цена двигалась у 43,5%,
из них вниз у 60,2%, вверх у 38,0%. Медиана снижения −3,85% и −291 000 ₽.

ПОЧЕМУ НЕ ТЕ ТЕМЫ, ЧТО В МАКЕТЕ. Карточка «Сколько продаётся квартира»
требовала срока экспозиции, а он не измеряется: `days_on_market` пуст у всех
435 тысяч сделок Росреестра, а снятие объявления и продажа в наших данных
неразличимы — 41% «снятий» приходится на шесть дат, совпадающих с провалами
собственного обхода. Мерили бы мы свой сборщик, а не рынок. Вторая карточка
называлась «Завысить и торговаться»; запрос «завысить цену на квартиру»
уведён юридическим смыслом (завышение суммы в ДКП ради ипотеки), и статья
попала бы в чужую выдачу.

ДИСЦИПЛИНА ЧИСЕЛ. Замер прошёл адверсариальную проверку: из десяти
первоначальных величин воспроизвелись две, остальные отброшены как
непригодные к печати. Обе статьи пересчитаны заново с нуля. У каждого блока
стоит своё поле source/footnote с датой, выборкой и границей — чем это число
НЕ является. Неизмеримое названо неизмеримым прямо в тексте, а не обойдено
молчанием: первый раздел второй статьи объясняет читателю, почему средний
срок продажи не назовёт никто честный.

Обвязка: два ключа в PUBLIC_ROUTES, две страницы, sitemap, три матчера в
apps.caddy на каждый адрес, PUBLIC_SHORT_PATHS у RouteGuard,
PUBLIC_INDEXED_FILES в гейте noindex. Блок «Читайте дальше» перестал быть
статичным «СКОРО» и делает карточку ссылкой, как только у неё появился
route. Витрина и лэндинг обновились сами — обе перебирают ARTICLE_CARDS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ8jqr4SFirX5tFLwdSrXh
This commit is contained in:
bot-backend 2026-09-13 14:41:10 +03:00
parent 94d34e7dcb
commit 89d095f486
9 changed files with 765 additions and 15 deletions

View file

@ -518,7 +518,7 @@ meraocenka.ru {
# Next отдаёт его КОРНЕВЫМ /trade-in/robots.txt (конвенция Next —
# robots.txt существует только в корне app/, не под mera-public/), эта
# форма под `{path}` не попадает — отсюда отдельный handle у него.
@meraPages path /estimate /oferta /refund /privacy /articles /articles/kak-ocenit-kvartiru /docs /business /sitemap.xml
@meraPages path /estimate /oferta /refund /privacy /articles /articles/kak-ocenit-kvartiru /articles/skolko-stoit-metr-v-ekaterinburge /articles/pochemu-kvartira-ne-prodaetsya /docs /business /sitemap.xml
handle @meraPages {
rewrite * /trade-in/mera-public{path}
reverse_proxy tradein-frontend:3000 {
@ -549,7 +549,7 @@ meraocenka.ru {
# переносит query как есть. Обёртка `route` обязательна по той же
# причине, что у соседей — внутри `handle` порядок директив определяет
# Caddy, и без неё `redir` выполнился бы раньше `uri`.
@meraShortSlash path_regexp shortslash ^/(estimate|oferta|refund|privacy|articles|articles/kak-ocenit-kvartiru|docs|business)/$
@meraShortSlash path_regexp shortslash ^/(estimate|oferta|refund|privacy|articles|articles/kak-ocenit-kvartiru|articles/skolko-stoit-metr-v-ekaterinburge|articles/pochemu-kvartira-ne-prodaetsya|docs|business)/$
handle @meraShortSlash {
route {
uri strip_suffix /
@ -637,7 +637,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/ /trade-in/mera-public/business /trade-in/mera-public/business/
@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/articles/skolko-stoit-metr-v-ekaterinburge /trade-in/mera-public/articles/skolko-stoit-metr-v-ekaterinburge/ /trade-in/mera-public/articles/pochemu-kvartira-ne-prodaetsya /trade-in/mera-public/articles/pochemu-kvartira-ne-prodaetsya/ /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

@ -330,6 +330,8 @@ describe("гейт витринных чисел (#2904)", () => {
"estimate/page.tsx",
"articles/page.tsx",
"articles/kak-ocenit-kvartiru/page.tsx",
"articles/skolko-stoit-metr-v-ekaterinburge/page.tsx",
"articles/pochemu-kvartira-ne-prodaetsya/page.tsx",
"docs/page.tsx",
"business/page.tsx",
];

View file

@ -45,7 +45,7 @@ export interface ArticleCardMeta {
* второй статьи не может увести её на первую (так и было, пока
* `to="articleHowTo"` стоял константой в JSX).
*/
readonly route: "articleHowTo" | null;
readonly route: "articleHowTo" | "articlePrices" | "articleWaiting" | null;
readonly rubric: string;
readonly title: string;
/** Подводка карточки — дословно из макета. */
@ -61,16 +61,16 @@ export const ARTICLE_CARDS: readonly ArticleCardMeta[] = [
lead: "Почему цена соседа — не ориентир, и как считать по аналогам.",
},
{
route: null,
rubric: "СРОКИ · 5 МИН",
title: "Сколько продаётся квартира в 2026 году и что «подвешивает» объявление",
lead: "Средние сроки по типам квартир и главные причины простоя.",
route: "articlePrices",
rubric: "ЦЕНЫ СДЕЛОК · 5 МИН",
title: "Сколько стоит метр в Екатеринбурге: цены сделок, а не объявлений",
lead: "Медиана по 24 535 сделкам Росреестра и почему метр в двушке самый дешёвый.",
},
{
route: null,
rubric: "ТОРГ · 7 МИН",
title: "Завысить и торговаться или поставить в рынок: что выгоднее",
lead: "Считаем на данных, сколько теряет «висящая» квартира каждый месяц.",
route: "articleWaiting",
rubric: "ЦЕНА И ОЖИДАНИЕ · 5 МИН",
title: "Почему квартира не продаётся: что происходит с ценой, пока вы ждёте",
lead: "Кто снижает цену, на сколько и за какой срок — по 49 620 объявлениям.",
},
];
@ -181,3 +181,262 @@ export const HOWTO = {
summary:
"Смотрите на цены сделок, а не объявлений. Берите 514 действительно взаимозаменяемых аналогов, считайте медиану цены за метр, применяйте поправки и обязательно проверяйте цену сроком продажи. Если считать некогда — это ровно то, что МЕРА делает автоматически.",
} as const;
/**
* Статья «Сколько стоит метр в Екатеринбурге» (13.09.2026).
*
* ЗАМЕР, НА КОТОРОМ СТОИТ ТЕКСТ. Таблица `deals` договоры купли-продажи
* Росреестра, регион 66. Взяты четыре последних доступных квартала
* (`deal_date` 2025-07-01, 2025-10-01, 2026-01-01, 2026-04-01 дата в данных
* штампуется началом квартала, отдельного дня сделки в выгрузке нет), то есть
* июль 2025 июнь 2026. Отсечены цены за метр вне коридора 20 000500 000
* как заведомый мусор выгрузки. По Екатеринбургу осталось 24 535 сделок.
*
* ГРАНИЦА, КОТОРУЮ НАЗЫВАЕМ В ТЕКСТЕ: Росреестр публикует цену договора. Она
* не всегда равна сумме, которую покупатель отдал на руки, и связки со
* сделавшим её объявлением в данных нет `cadastral_number` пуст и у сделок,
* и у объявлений. Поэтому «разрыв между ценой объявления и ценой сделки» в
* статье не считается: считать его нечем.
*
* ПОЧЕМУ ДИНАМИКА ДАНА ПОКВАРТАЛЬНО: в выгрузке ровно десять различных
* значений `deal_date`, все на первое число квартала. Месячного разрешения в
* данных нет вовсе.
*/
export const PRICES = {
rubric: "ЦЕНЫ СДЕЛОК · 5 МИН ЧТЕНИЯ",
title: "Сколько стоит метр в Екатеринбурге: цены сделок, а не объявлений",
lead: [
"Когда вы смотрите объявления, вы видите, сколько продавцы хотят получить. Сколько покупатели заплатили на самом деле, видно в другом месте — в договорах купли-продажи, которые проходят через Росреестр. Эти два числа не совпадают, и ориентироваться стоит на второе.",
"Ниже — медианы по реальным сделкам Екатеринбурга и области за последние четыре квартала. Без округлений до красивого и без слова «примерно».",
],
toc: [
{ id: "s1", title: "Сколько стоит метр по сделкам" },
{ id: "s2", title: "Почему метр в двушке самый дешёвый" },
{ id: "s3", title: "Что делала цена последние два с половиной года" },
{ id: "s4", title: "Города области: где метр дешевле" },
{ id: "s5", title: "Пять способов неправильно прочитать эти цифры" },
],
s1: {
id: "s1",
heading: "1. Сколько стоит метр по сделкам",
body: "Медианная цена квадратного метра в сделках по Екатеринбургу — 123 626 ₽. Медиана, а не среднее: среднее в недвижимости всегда тянут вверх несколько дорогих объектов, и к типичной квартире оно отношения не имеет. Четверть сделок прошла дешевле 100 000 ₽ за метр, четверть дороже 151 288 ₽ — вот настоящая ширина рынка, и она больше, чем кажется по объявлениям. Если ваша цена попадает в эту вилку, вы в рынке.",
source:
"ЗАМЕР 13.09.2026 · 24 535 ДОГОВОРОВ КУПЛИ-ПРОДАЖИ РОСРЕЕСТРА ПО ЕКАТЕРИНБУРГУ ЗА ИЮЛЬ 2025 — ИЮНЬ 2026 · ЭТО ЦЕНА ДОГОВОРА, А НЕ ЦЕНА ОБЪЯВЛЕНИЯ",
memo: "Цена объявления и цена договора — разные величины. Насколько именно разные, не скажет никто: Росреестр публикует сделки без привязки к объявлению, сопоставить одно с другим нечем.",
},
s2: {
id: "s2",
heading: "2. Почему метр в двушке самый дешёвый",
body: "«Чем больше квартира, тем дешевле метр» — самое живучее заблуждение о ценах. По сделкам кривая не падает, а прогибается: дешевле всего метр в двухкомнатной, а дальше он снова дорожает.",
table: {
head: ["КОМНАТНОСТЬ", "МЕДИАНА ЗА МЕТР"],
rows: [
{ name: "Студия · медиана 25 м² · 2 967 сделок", value: "134 507 ₽" },
{ name: "Однокомнатная · 38 м² · 8 342 сделки", value: "124 256 ₽" },
{ name: "Двухкомнатная · 51 м² · 6 548 сделок", value: "116 910 ₽" },
{ name: "Трёхкомнатная · 70 м² · 3 861 сделка", value: "121 835 ₽" },
{
name: "Четыре комнаты и больше · 108 м² · 2 817 сделок",
value: "132 122 ₽",
},
],
},
footnote:
"ТОТ ЖЕ ЗАМЕР · РЯДОМ С КАЖДОЙ СТРОКОЙ — МЕДИАННАЯ ПЛОЩАДЬ И ЧИСЛО СДЕЛОК, НА КОТОРЫХ ПОСЧИТАНА МЕДИАНА",
after:
"Объяснение простое: у студий и у больших квартир метр продаётся вместе с чем-то ещё. У студии — с низким порогом входа, её берут те, кому важна сумма целиком, а не цена метра. У четырёхкомнатной — с домом и районом, потому что такие квартиры почти не строят в дешёвых сериях. Двушка же самый массовый и самый конкурентный товар на рынке, и цена метра в ней прижата плотнее всего.",
},
s3: {
id: "s3",
heading: "3. Что делала цена последние два с половиной года",
body: "Рынок Екатеринбурга не падал ни одного квартала из десяти доступных. Медиана росла непрерывно, но по-разному: в 2024 году прибавка держалась около полутора процентов за квартал, в 2025-м и 2026-м ускорилась. Для продавца отсюда следует практический вывод: цена, посчитанная по сделкам полугодовой давности, сегодня занижена.",
tiles: [
{ value: "106 727 ₽", label: "метр в первом квартале 2024 года" },
{ value: "128 756 ₽", label: "метр во втором квартале 2026 года" },
{ value: "+20,6%", label: "рост медианы за два с половиной года" },
],
source:
"ТОТ ЖЕ ИСТОЧНИК, ВСЕ ДЕСЯТЬ ДОСТУПНЫХ КВАРТАЛОВ · ДАТА СДЕЛКИ В ВЫГРУЗКЕ РОСРЕЕСТРА ШТАМПУЕТСЯ НАЧАЛОМ КВАРТАЛА, МЕСЯЧНОГО РАЗРЕШЕНИЯ В ДАННЫХ НЕТ",
},
s4: {
id: "s4",
heading: "4. Города области: где метр дешевле",
body: "Внутри Свердловской области разброс больше, чем между комнатностями. Города-спутники Екатеринбурга идут почти по областной столице, дальние промышленные города — втрое дешевле.",
kv: [
{ k: "ВЕРХНЯЯ ПЫШМА", v: "102 888 ₽ за метр · 1 193 сделки" },
{ k: "ЗАРЕЧНЫЙ", v: "95 865 ₽ · 393 сделки" },
{ k: "БЕРЁЗОВСКИЙ", v: "95 745 ₽ · 858 сделок" },
{ k: "ПЕРВОУРАЛЬСК", v: "66 384 ₽ · 1 458 сделок" },
{ k: "КАМЕНСК-УРАЛЬСКИЙ", v: "63 763 ₽ · 1 994 сделки" },
{ k: "НИЖНИЙ ТАГИЛ", v: "62 637 ₽ · 3 799 сделок" },
{ k: "АСБЕСТ", v: "46 429 ₽ · 642 сделки" },
{ k: "НОВОУРАЛЬСК", v: "40 527 ₽ · 816 сделок" },
],
footnote:
"ТОТ ЖЕ ПЕРИОД · ПОКАЗАНЫ ГОРОДА, ГДЕ ЗА ГОД НАБРАЛОСЬ НЕ МЕНЬШЕ СТА СДЕЛОК · ГДЕ ИХ МЕНЬШЕ, МЕДИАНА НЕУСТОЙЧИВА И МЫ ЕЁ НЕ ПУБЛИКУЕМ",
},
cta: {
eyebrow: "МЕДИАНА ПО ГОРОДУ — ЭТО ЕЩЁ НЕ ВАША ЦЕНА",
title: "МЕРА считает по вашему дому, а не по городу",
body: `Между двумя домами в одном районе разница бывает больше, чем между районами. Мы находим аналоги вокруг вашего адреса, считаем медиану по ним и показываем, на чём она построена. Рынок вокруг — бесплатно, ваша цена и список аналогов — за ${SERVICE_PRICE_RUB} ₽.`,
},
s5: {
id: "s5",
heading: "5. Пять способов неправильно прочитать эти цифры",
mistakes: [
{
title: "Умножить медиану города на свою площадь",
body: "Получится цена абстрактной квартиры в абстрактном районе. Медиана по городу — ориентир порядка величины, а не оценка объекта.",
},
{
title: "Решить, что раз метр в двушке дешевле, двушку продавать невыгодно",
body: "Дешевле метр, а не квартира. Двушка всё равно стоит дороже однушки: 6 015 000 ₽ против 4 650 000 ₽ по медиане сделок.",
},
{
title: "Считать цену договора суммой на руки",
body: "Росреестр публикует то, что написано в договоре. Совпадает ли это с фактическим расчётом между сторонами, из данных не видно.",
},
{
title: "Брать медиану полугодовой давности",
body: "За последние четыре квартала медиана прибавляла около двух процентов за квартал. Полгода отставания — это несколько процентов мимо вашей цены.",
},
{
title: "Сравнивать свою квартиру с городом, а не с соседями",
body: "Между квартилями Екатеринбурга разброс от 100 000 до 151 288 ₽ за метр. Где внутри этой вилки стоит ваш дом, медиана города не скажет.",
},
],
},
summary:
"Медианный метр в Екатеринбурге стоит 123 626 ₽ по 24 535 сделкам за год. Дешевле всего метр в двухкомнатной, дороже всего в студии. Рынок рос все десять доступных кварталов и прибавил 20,6% с начала 2024 года. Города области идут от 40 527 ₽ в Новоуральске до 102 888 ₽ в Верхней Пышме. Всё это ориентиры порядка, а не оценка вашей квартиры: её считают по соседним домам.",
} as const;
/**
* Статья «Почему квартира не продаётся» (13.09.2026).
*
* ЗАМЕР, НА КОТОРОМ СТОИТ ТЕКСТ. Таблица `offer_price_history` история цены
* объявления, которую ведут сами площадки (cian, yandex, domklik, avito),
* склеенная с `listings` по региону 66. Срез 13.09.2026:
*
* 49 620 объявлений области, у которых есть хоть одна запись о цене
* 21 608 из них цена менялась хотя бы раз (43,5%)
* 13 009 итоговая цена ниже стартовой (60,2% от менявших)
* 8 212 итоговая цена ВЫШЕ стартовой (38,0% от менявших)
*
* Медиана по снизившим: 3,85%, 291 000 , 30 дней между первой и последней
* записью о цене.
*
* ГРАНИЦЫ, КОТОРЫЕ НАЗЫВАЕМ В ТЕКСТЕ. (1) Это движение ЗАПРАШИВАЕМОЙ цены, а
* не скидка на сделке: связки объявления со сделкой в данных нет. (2) Знаменатель
* 21 608 объявления, где цена двигалась; те, у кого запись одна, в долю не
* входят, потому что у них неподвижная цена и отсутствие наблюдения
* неразличимы. (3) «30 дней» срок между первой и последней записью о цене, а
* не срок продажи. Срока продажи в данных нет вовсе: поле `days_on_market` в
* сделках Росреестра пусто у всех 435 тысяч записей, а исчезновение объявления
* означает только исчезновение из выдачи, но не продажу.
*
* ПОЧЕМУ В СТАТЬЕ НЕТ СРЕДНЕГО СРОКА ПРОДАЖИ. Он не измеряется. Снятие
* объявления и продажа в наших данных неразличимы, а даты «снятий» частично
* совпадают с провалами собственного обхода то есть измеряли бы мы свой
* сборщик, а не рынок.
*/
export const WAITING = {
rubric: "ЦЕНА И ОЖИДАНИЕ · 5 МИН ЧТЕНИЯ",
title: "Почему квартира не продаётся: что происходит с ценой, пока вы ждёте",
lead: [
"Квартира висит третий месяц, звонков нет, и первый совет от всех вокруг — «подожди, твой покупатель найдётся». Иногда это правда. Чаще ожидание заканчивается тем же, чем закончилось у большинства: продавец снижает цену, только на несколько месяцев позже и на большую сумму.",
"Мы посмотрели, что на самом деле происходит с ценой в объявлениях Свердловской области, пока они висят. Ниже — что видно в данных и, не менее важно, чего в них не видно.",
],
toc: [
{ id: "s1", title: "Что вообще можно измерить, а что нельзя" },
{ id: "s2", title: "Кто двигает цену и в какую сторону" },
{ id: "s3", title: "На сколько снижают те, кто снижает" },
{ id: "s4", title: "Что делать, если звонков нет" },
{ id: "s5", title: "Пять причин простоя, которые не в цене" },
],
s1: {
id: "s1",
heading: "1. Что вообще можно измерить, а что нельзя",
body: "Честно о границах, прежде чем показывать числа. Среднего срока продажи мы вам не назовём, и никто честный не назовёт: чтобы его посчитать, нужно знать день, когда квартиру выставили, и день, когда её купили. Второго нет ни у кого, кроме сторон сделки. Росреестр публикует договоры без срока экспозиции, а исчезновение объявления с площадки означает только исчезновение с площадки — продал, передумал или переподал заново, из данных не отличить. Зато прекрасно видно другое: как продавец сам двигает цену, пока ждёт.",
source:
"ЗАМЕР 13.09.2026 · 49 620 ОБЪЯВЛЕНИЙ СВЕРДЛОВСКОЙ ОБЛАСТИ, У КОТОРЫХ ПЛОЩАДКА ВЕДЁТ ИСТОРИЮ ЦЕНЫ · ЭТО ЗАПРАШИВАЕМАЯ ЦЕНА, А НЕ ЦЕНА СДЕЛКИ",
memo: "Любая статья, которая называет вам «средний срок продажи квартиры», считает его по снятиям объявлений. Снятие — это не продажа. Спросите, как отличали одно от другого.",
},
s2: {
id: "s2",
heading: "2. Кто двигает цену и в какую сторону",
body: "Из 49 620 объявлений цена хотя бы раз сдвинулась у 21 608. Дальше начинается неожиданное: вниз ходят не все. Почти четыре из десяти движений — вверх.",
tiles: [
{ value: "43,5%", label: "объявлений, где цена вообще двигалась" },
{ value: "60,2%", label: "из двигавших — снизили" },
{ value: "38,0%", label: "из двигавших — подняли" },
],
source:
"ТОТ ЖЕ ЗАМЕР · ЗНАМЕНАТЕЛЬ ДОЛЕЙ — 21 608 ОБЪЯВЛЕНИЙ, У КОТОРЫХ ЦЕНА МЕНЯЛАСЬ · У ОСТАЛЬНЫХ ЗАПИСЬ ОДНА, И НЕПОДВИЖНУЮ ЦЕНУ ОТ ОТСУТСТВИЯ НАБЛЮДЕНИЯ НЕ ОТЛИЧИТЬ",
after:
"Подъём цены обычно означает не жадность, а переоценку: продавец начал слишком низко, получил шквал звонков в первую неделю и понял это. Ошибка в меньшую сторону чинится легко и быстро. Ошибка в большую сторону чинится месяцами — и это ровно та асимметрия, ради которой стоит потратить вечер на расчёт до публикации.",
},
s3: {
id: "s3",
heading: "3. На сколько снижают те, кто снижает",
body: "Медианное снижение выглядит скромно в процентах и совсем не скромно в рублях. И это не одна большая уступка в конце, а обычно две-три ступеньки подряд.",
table: {
head: ["ЧТО ИЗМЕРЕНО", "МЕДИАНА ПО 13 009 ОБЪЯВЛЕНИЯМ"],
rows: [
{ name: "Снижение от стартовой цены", value: "3,85%" },
{ name: "Снижение в рублях", value: "291 000 ₽" },
{ name: "Срок между первой и последней ценой", value: "30 дней" },
{ name: "Нижний квартиль снижения", value: "7,7%" },
{ name: "Верхний квартиль снижения", value: "2,4%" },
],
},
footnote:
"ТОТ ЖЕ ЗАМЕР · «30 ДНЕЙ» — СРОК МЕЖДУ ПЕРВОЙ И ПОСЛЕДНЕЙ ЗАПИСЬЮ О ЦЕНЕ, А НЕ СРОК ПРОДАЖИ · СРОК ПРОДАЖИ ИЗМЕРИТЬ НЕЧЕМ",
},
s4: {
id: "s4",
heading: "4. Что делать, если звонков нет",
body: "Ожидание само по себе не аргумент. Рынок отвечает на цену быстро, и молчание — тоже ответ. Порядок действий такой:",
kv: [
{ k: "ПЕРВАЯ НЕДЕЛЯ", v: "считаем показы и звонки, а не дни. Ноль звонков за неделю при живом объявлении — цена выше рынка" },
{ k: "ВТОРАЯ НЕДЕЛЯ", v: "пересчитываем по аналогам заново: что сейчас стоит рядом и что из этого реально ушло" },
{ k: "ПЕРЕД СНИЖЕНИЕМ", v: "проверяем фото, описание и этаж — часть простоя не в цене вовсе, см. раздел 5" },
{ k: "ЕСЛИ СНИЖАЕМ", v: "один заметный шаг лучше пяти незаметных: снижение на 1% рынок не видит" },
{ k: "ЧЕГО НЕ ДЕЛАТЬ", v: "снимать и публиковать заново ради «свежести» — история цены на площадках всё равно сохраняется" },
],
footnote:
"ЭТО ПОРЯДОК ДЕЙСТВИЙ, А НЕ ЗАМЕР · ИЗ ДАННЫХ ЗДЕСЬ ТОЛЬКО ОДНО: СНИЖАЮЩИЕ ДЕЛАЮТ ЭТО В МЕДИАНЕ ЗА ТРИ СТУПЕНЬКИ ЦЕНЫ",
},
cta: {
eyebrow: "ПЕРЕД ТЕМ КАК СНИЖАТЬ",
title: "Проверьте, действительно ли цена выше рынка",
body: `Снижать вслепую — это отдавать сотни тысяч на всякий случай. МЕРА найдёт аналоги вокруг вашего адреса и покажет, где ваша цена относительно них. Рынок вокруг — бесплатно, разбор вашей цены — за ${SERVICE_PRICE_RUB} ₽.`,
},
s5: {
id: "s5",
heading: "5. Пять причин простоя, которые не в цене",
mistakes: [
{
title: "Четыре фотографии, снятые вечером",
body: "Объявление конкурирует в ленте миниатюр, а не в тексте. Если карточку не открыли, цена значения не имела.",
},
{
title: "Описание из характеристик",
body: "«2/5, панель, 44 м²» покупатель уже видит в фильтре. В описании должно быть то, чего в фильтре нет.",
},
{
title: "Показы только по будням до шести",
body: "Ваш покупатель в это время на работе. Часть простоя — это не отказ рынка, а несовпадение расписаний.",
},
{
title: "Обременение, о котором вы скажете позже",
body: "Ипотека, доли, несовершеннолетний собственник. Всплывает на этапе аванса и отправляет вас в начало.",
},
{
title: "Продажа «в никуда»",
body: "Если вы не знаете, куда переезжаете, вы неосознанно тормозите сделку сами. Покупатель это чувствует раньше вас.",
},
],
},
summary:
"Срок продажи никто честно не измерит: снятие объявления и продажа в данных неразличимы. Зато видно движение цены. Она двигалась у 43,5% объявлений, и в 60,2% случаев вниз, в медиане на 3,85% и 291 000 ₽. Прежде чем снижать, проверьте цену по аналогам и проверьте само объявление: часть простоя не в цене.",
} as const;

View file

@ -202,8 +202,19 @@ export default function HowToArticlePage() {
</PublicLink>
</div>
<div className={styles.artGrid}>
{ARTICLE_CARDS.filter((c) => c.route !== "articleHowTo").map(
(c) => (
{ARTICLE_CARDS.filter((c) => c.route !== "articleHowTo").map((c) =>
c.route ? (
<PublicLink
key={c.title}
to={c.route}
className={styles.artCardLink}
>
<div className={styles.artRubric}>{c.rubric}</div>
<div className={styles.artCardTitle}>{c.title}</div>
<div className={styles.artCardLead}>{c.lead}</div>
<div className={styles.artReadMore}>Читать </div>
</PublicLink>
) : (
<article key={c.title} className={styles.artCard}>
<div className={styles.artRubric}>{c.rubric}</div>
<div className={styles.artCardTitle}>{c.title}</div>

View file

@ -0,0 +1,231 @@
import type { Metadata } from "next";
import { InnerHeader } from "../../_components/InnerHeader";
import { JsonLd } from "../../_components/JsonLd";
import { PublicLink } from "../../_components/PublicLink";
import { FooterV3 } from "../../_components/v3/FooterV3";
import { breadcrumbNode, ORGANIZATION_ID } from "../../_lib/schema";
import { PUBLIC_ORIGIN, PUBLIC_ROUTES } from "../../content";
import styles from "../../landing-v3.module.css";
import { ARTICLE_CARDS, WAITING } from "../articles-content";
/**
* Статья «Почему квартира не продаётся: что происходит с ценой, пока вы ждёте».
* Текст и все величины в `articles-content.ts` (там же замер, на котором они
* стоят, и его границы). В этом файле не должно быть ни одного числа: папка
* попадает в `SUBPAGES` гейта чисел.
*
* Серверный компонент без клиентского JS: содержание якорные ссылки,
* секции обычные заголовки с id.
*
* ИНДЕКСАЦИЯ ОТКРЫТА (10.09.2026, см. layout.tsx).
*/
const PUBLIC_WAITING_URL = `${PUBLIC_ORIGIN}${PUBLIC_ROUTES.articleWaiting}`;
const DESCRIPTION = WAITING.lead[0];
export const metadata: Metadata = {
title: `${WAITING.title}МЕРА`,
description: DESCRIPTION,
alternates: { canonical: PUBLIC_WAITING_URL },
openGraph: {
type: "article",
title: `${WAITING.title}МЕРА`,
description: DESCRIPTION,
url: PUBLIC_WAITING_URL,
},
};
/**
* JSON-LD `Article` для сниппетов поисковика. Без `datePublished`/
* `dateModified`: достоверной даты публикации в коде нет, а выдуманная дата
* то самое нечестное утверждение, которое запрещает шапка content.ts.
*/
const ARTICLE_JSON_LD = {
"@context": "https://schema.org",
"@type": "Article",
headline: WAITING.title,
description: DESCRIPTION,
author: { "@type": "Organization", name: "МЕРА" },
publisher: { "@id": ORGANIZATION_ID },
mainEntityOfPage: { "@type": "WebPage", "@id": PUBLIC_WAITING_URL },
};
const BREADCRUMB_JSON_LD = {
"@context": "https://schema.org",
...breadcrumbNode([
{ name: "Главная", url: `${PUBLIC_ORIGIN}${PUBLIC_ROUTES.home}` },
{ name: "Статьи", url: `${PUBLIC_ORIGIN}${PUBLIC_ROUTES.articles}` },
{ name: WAITING.title, url: PUBLIC_WAITING_URL },
]),
};
export default function WaitingArticlePage() {
return (
<>
<JsonLd data={ARTICLE_JSON_LD} />
<JsonLd data={BREADCRUMB_JSON_LD} />
<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>
<PublicLink to="articles" className={styles.artCrumbLink}>
Статьи
</PublicLink>
</nav>
<header className={styles.artPageHead}>
<div className={styles.repEyebrow}>{WAITING.rubric}</div>
<h1 className={styles.artPageTitle}>{WAITING.title}</h1>
</header>
<nav className={styles.artToc} aria-label="Содержание">
<div className={styles.artTocLabel}>СОДЕРЖАНИЕ</div>
<ol className={styles.artTocList}>
{WAITING.toc.map((t) => (
<li key={t.id}>
<a href={`#${t.id}`}>{t.title}</a>
</li>
))}
</ol>
</nav>
<div className={styles.artBody}>
{WAITING.lead.map((p) => (
<p key={p.slice(0, 24)} className={styles.artP}>
{p}
</p>
))}
<h2 id={WAITING.s1.id} className={styles.artH2}>
{WAITING.s1.heading}
</h2>
<p className={styles.artP}>{WAITING.s1.body}</p>
<div className={styles.artFootnote}>{WAITING.s1.source}</div>
<aside className={styles.artMemo}>
<div className={styles.artMemoLabel}>ЗАПОМНИТЬ</div>
<p>{WAITING.s1.memo}</p>
</aside>
<h2 id={WAITING.s2.id} className={styles.artH2}>
{WAITING.s2.heading}
</h2>
<p className={styles.artP}>{WAITING.s2.body}</p>
<div className={styles.artTiles}>
{WAITING.s2.tiles.map((t) => (
<div key={t.label} className={styles.artTile}>
<div className={styles.artTileValue}>{t.value}</div>
<div className={styles.artTileLabel}>{t.label}</div>
</div>
))}
</div>
<div className={styles.artFootnote}>{WAITING.s2.source}</div>
<p className={styles.artP}>{WAITING.s2.after}</p>
<h2 id={WAITING.s3.id} className={styles.artH2}>
{WAITING.s3.heading}
</h2>
<p className={styles.artP}>{WAITING.s3.body}</p>
<div className={styles.artTable} role="table">
<div className={styles.artTableHead} role="row">
<span role="columnheader">{WAITING.s3.table.head[0]}</span>
<span role="columnheader" className={styles.artTableRight}>
{WAITING.s3.table.head[1]}
</span>
</div>
{WAITING.s3.table.rows.map((r) => (
<div key={r.name} className={styles.artTableRow} role="row">
<span role="cell">{r.name}</span>
<span role="cell" className={styles.artTableRight}>
{r.value}
</span>
</div>
))}
</div>
<div className={styles.artFootnote}>{WAITING.s3.footnote}</div>
<h2 id={WAITING.s4.id} className={styles.artH2}>
{WAITING.s4.heading}
</h2>
<p className={styles.artP}>{WAITING.s4.body}</p>
<div className={styles.artKv}>
{WAITING.s4.kv.map((row) => (
<div key={row.k} className={styles.artKvRow}>
<span className={styles.artKvKey}>{row.k}</span>
<span className={styles.artKvVal}>{row.v}</span>
</div>
))}
</div>
<div className={styles.artFootnote}>{WAITING.s4.footnote}</div>
<div className={styles.artCta}>
<div className={styles.artCtaEyebrow}>{WAITING.cta.eyebrow}</div>
<div className={styles.artCtaTitle}>{WAITING.cta.title}</div>
<p className={styles.artCtaBody}>{WAITING.cta.body}</p>
<PublicLink to="estimate" className={styles.gameBtnAccent}>
Проверить квартиру
</PublicLink>
</div>
<h2 id={WAITING.s5.id} className={styles.artH2}>
{WAITING.s5.heading}
</h2>
<ol className={styles.artMistakes}>
{WAITING.s5.mistakes.map((m, i) => (
<li key={m.title} className={styles.artMistake}>
<span className={styles.artMistakeNum}>
{String(i + 1).padStart(2, "0")}
</span>
<div>
<div className={styles.artMistakeTitle}>{m.title}</div>
<div className={styles.artMistakeBody}>{m.body}</div>
</div>
</li>
))}
</ol>
<div className={styles.artSummary}>
<div className={styles.artTocLabel}>КОРОТКО</div>
<p>{WAITING.summary}</p>
</div>
</div>
<section className={styles.artMore} aria-label="Читайте дальше">
<div className={styles.artHead}>
<h2 className={styles.artTitle}>Читайте дальше</h2>
<PublicLink to="articles" className={styles.artAllLink}>
Все статьи
</PublicLink>
</div>
<div className={styles.artGrid}>
{ARTICLE_CARDS.filter((c) => c.route !== "articleWaiting").map((c) =>
c.route ? (
<PublicLink
key={c.title}
to={c.route}
className={styles.artCardLink}
>
<div className={styles.artRubric}>{c.rubric}</div>
<div className={styles.artCardTitle}>{c.title}</div>
<div className={styles.artCardLead}>{c.lead}</div>
<div className={styles.artReadMore}>Читать </div>
</PublicLink>
) : (
<article key={c.title} className={styles.artCard}>
<div className={styles.artRubric}>{c.rubric}</div>
<div className={styles.artCardTitle}>{c.title}</div>
<div className={styles.artCardLead}>{c.lead}</div>
<div className={styles.artSoon}>СКОРО</div>
</article>
),
)}
</div>
</section>
</main>
<FooterV3 />
</>
);
}

View file

@ -0,0 +1,231 @@
import type { Metadata } from "next";
import { InnerHeader } from "../../_components/InnerHeader";
import { JsonLd } from "../../_components/JsonLd";
import { PublicLink } from "../../_components/PublicLink";
import { FooterV3 } from "../../_components/v3/FooterV3";
import { breadcrumbNode, ORGANIZATION_ID } from "../../_lib/schema";
import { PUBLIC_ORIGIN, PUBLIC_ROUTES } from "../../content";
import styles from "../../landing-v3.module.css";
import { ARTICLE_CARDS, PRICES } from "../articles-content";
/**
* Статья «Сколько стоит метр в Екатеринбурге: цены сделок, а не объявлений».
* Текст и все величины в `articles-content.ts` (там же замер, на котором они
* стоят, и его границы). В этом файле не должно быть ни одного числа: папка
* попадает в `SUBPAGES` гейта чисел.
*
* Серверный компонент без клиентского JS: содержание якорные ссылки,
* секции обычные заголовки с id.
*
* ИНДЕКСАЦИЯ ОТКРЫТА (10.09.2026, см. layout.tsx).
*/
const PUBLIC_PRICES_URL = `${PUBLIC_ORIGIN}${PUBLIC_ROUTES.articlePrices}`;
const DESCRIPTION = PRICES.lead[0];
export const metadata: Metadata = {
title: `${PRICES.title}МЕРА`,
description: DESCRIPTION,
alternates: { canonical: PUBLIC_PRICES_URL },
openGraph: {
type: "article",
title: `${PRICES.title}МЕРА`,
description: DESCRIPTION,
url: PUBLIC_PRICES_URL,
},
};
/**
* JSON-LD `Article` для сниппетов поисковика. Без `datePublished`/
* `dateModified`: достоверной даты публикации в коде нет, а выдуманная дата
* то самое нечестное утверждение, которое запрещает шапка content.ts.
*/
const ARTICLE_JSON_LD = {
"@context": "https://schema.org",
"@type": "Article",
headline: PRICES.title,
description: DESCRIPTION,
author: { "@type": "Organization", name: "МЕРА" },
publisher: { "@id": ORGANIZATION_ID },
mainEntityOfPage: { "@type": "WebPage", "@id": PUBLIC_PRICES_URL },
};
const BREADCRUMB_JSON_LD = {
"@context": "https://schema.org",
...breadcrumbNode([
{ name: "Главная", url: `${PUBLIC_ORIGIN}${PUBLIC_ROUTES.home}` },
{ name: "Статьи", url: `${PUBLIC_ORIGIN}${PUBLIC_ROUTES.articles}` },
{ name: PRICES.title, url: PUBLIC_PRICES_URL },
]),
};
export default function PricesArticlePage() {
return (
<>
<JsonLd data={ARTICLE_JSON_LD} />
<JsonLd data={BREADCRUMB_JSON_LD} />
<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>
<PublicLink to="articles" className={styles.artCrumbLink}>
Статьи
</PublicLink>
</nav>
<header className={styles.artPageHead}>
<div className={styles.repEyebrow}>{PRICES.rubric}</div>
<h1 className={styles.artPageTitle}>{PRICES.title}</h1>
</header>
<nav className={styles.artToc} aria-label="Содержание">
<div className={styles.artTocLabel}>СОДЕРЖАНИЕ</div>
<ol className={styles.artTocList}>
{PRICES.toc.map((t) => (
<li key={t.id}>
<a href={`#${t.id}`}>{t.title}</a>
</li>
))}
</ol>
</nav>
<div className={styles.artBody}>
{PRICES.lead.map((p) => (
<p key={p.slice(0, 24)} className={styles.artP}>
{p}
</p>
))}
<h2 id={PRICES.s1.id} className={styles.artH2}>
{PRICES.s1.heading}
</h2>
<p className={styles.artP}>{PRICES.s1.body}</p>
<div className={styles.artFootnote}>{PRICES.s1.source}</div>
<aside className={styles.artMemo}>
<div className={styles.artMemoLabel}>ЗАПОМНИТЬ</div>
<p>{PRICES.s1.memo}</p>
</aside>
<h2 id={PRICES.s2.id} className={styles.artH2}>
{PRICES.s2.heading}
</h2>
<p className={styles.artP}>{PRICES.s2.body}</p>
<div className={styles.artTable} role="table">
<div className={styles.artTableHead} role="row">
<span role="columnheader">{PRICES.s2.table.head[0]}</span>
<span role="columnheader" className={styles.artTableRight}>
{PRICES.s2.table.head[1]}
</span>
</div>
{PRICES.s2.table.rows.map((r) => (
<div key={r.name} className={styles.artTableRow} role="row">
<span role="cell">{r.name}</span>
<span role="cell" className={styles.artTableRight}>
{r.value}
</span>
</div>
))}
</div>
<div className={styles.artFootnote}>{PRICES.s2.footnote}</div>
<p className={styles.artP}>{PRICES.s2.after}</p>
<h2 id={PRICES.s3.id} className={styles.artH2}>
{PRICES.s3.heading}
</h2>
<p className={styles.artP}>{PRICES.s3.body}</p>
<div className={styles.artTiles}>
{PRICES.s3.tiles.map((t) => (
<div key={t.label} className={styles.artTile}>
<div className={styles.artTileValue}>{t.value}</div>
<div className={styles.artTileLabel}>{t.label}</div>
</div>
))}
</div>
<div className={styles.artFootnote}>{PRICES.s3.source}</div>
<h2 id={PRICES.s4.id} className={styles.artH2}>
{PRICES.s4.heading}
</h2>
<p className={styles.artP}>{PRICES.s4.body}</p>
<div className={styles.artKv}>
{PRICES.s4.kv.map((row) => (
<div key={row.k} className={styles.artKvRow}>
<span className={styles.artKvKey}>{row.k}</span>
<span className={styles.artKvVal}>{row.v}</span>
</div>
))}
</div>
<div className={styles.artFootnote}>{PRICES.s4.footnote}</div>
<div className={styles.artCta}>
<div className={styles.artCtaEyebrow}>{PRICES.cta.eyebrow}</div>
<div className={styles.artCtaTitle}>{PRICES.cta.title}</div>
<p className={styles.artCtaBody}>{PRICES.cta.body}</p>
<PublicLink to="estimate" className={styles.gameBtnAccent}>
Проверить квартиру
</PublicLink>
</div>
<h2 id={PRICES.s5.id} className={styles.artH2}>
{PRICES.s5.heading}
</h2>
<ol className={styles.artMistakes}>
{PRICES.s5.mistakes.map((m, i) => (
<li key={m.title} className={styles.artMistake}>
<span className={styles.artMistakeNum}>
{String(i + 1).padStart(2, "0")}
</span>
<div>
<div className={styles.artMistakeTitle}>{m.title}</div>
<div className={styles.artMistakeBody}>{m.body}</div>
</div>
</li>
))}
</ol>
<div className={styles.artSummary}>
<div className={styles.artTocLabel}>КОРОТКО</div>
<p>{PRICES.summary}</p>
</div>
</div>
<section className={styles.artMore} aria-label="Читайте дальше">
<div className={styles.artHead}>
<h2 className={styles.artTitle}>Читайте дальше</h2>
<PublicLink to="articles" className={styles.artAllLink}>
Все статьи
</PublicLink>
</div>
<div className={styles.artGrid}>
{ARTICLE_CARDS.filter((c) => c.route !== "articlePrices").map((c) =>
c.route ? (
<PublicLink
key={c.title}
to={c.route}
className={styles.artCardLink}
>
<div className={styles.artRubric}>{c.rubric}</div>
<div className={styles.artCardTitle}>{c.title}</div>
<div className={styles.artCardLead}>{c.lead}</div>
<div className={styles.artReadMore}>Читать </div>
</PublicLink>
) : (
<article key={c.title} className={styles.artCard}>
<div className={styles.artRubric}>{c.rubric}</div>
<div className={styles.artCardTitle}>{c.title}</div>
<div className={styles.artCardLead}>{c.lead}</div>
<div className={styles.artSoon}>СКОРО</div>
</article>
),
)}
</div>
</section>
</main>
<FooterV3 />
</>
);
}

View file

@ -195,6 +195,8 @@ export const PUBLIC_ROUTES = {
// Макет 29.08: раздел статей и хаб документов.
articles: "/articles",
articleHowTo: "/articles/kak-ocenit-kvartiru",
articlePrices: "/articles/skolko-stoit-metr-v-ekaterinburge",
articleWaiting: "/articles/pochemu-kvartira-ne-prodaetsya",
docs: "/docs",
// Страница B2B-контура: связаться и оставить заявку. Заведена 31.08.2026
// взамен ссылки на `B2B_URL` в шапке и подвале — та вела в закрытый контур

View file

@ -22,7 +22,7 @@ import { PUBLIC_ORIGIN, PUBLIC_ROUTES } from "./content";
* актуальным целиком. Двигать дату вперёд нужно вручную, вместе с реальным
* изменением состава/контента страниц.
*/
const LAST_MODIFIED = new Date("2026-09-10T00:00:00Z");
const LAST_MODIFIED = new Date("2026-09-13T00:00:00Z");
export default function sitemap(): MetadataRoute.Sitemap {
const url = (path: string) => `${PUBLIC_ORIGIN}${path}`;
@ -52,6 +52,18 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: "monthly",
priority: 0.6,
},
{
url: url(PUBLIC_ROUTES.articlePrices),
lastModified: LAST_MODIFIED,
changeFrequency: "monthly",
priority: 0.6,
},
{
url: url(PUBLIC_ROUTES.articleWaiting),
lastModified: LAST_MODIFIED,
changeFrequency: "monthly",
priority: 0.6,
},
{
url: url(PUBLIC_ROUTES.business),
lastModified: LAST_MODIFIED,

View file

@ -102,6 +102,8 @@ const PUBLIC_SHORT_PATHS = [
"/privacy",
"/articles",
"/articles/kak-ocenit-kvartiru",
"/articles/skolko-stoit-metr-v-ekaterinburge",
"/articles/pochemu-kvartira-ne-prodaetsya",
"/docs",
"/business",
// Машинные артефакты для поисковых роботов, а не страницы. React на них не