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
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:
parent
94d34e7dcb
commit
89d095f486
9 changed files with 765 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"Смотрите на цены сделок, а не объявлений. Берите 5–14 действительно взаимозаменяемых аналогов, считайте медиану цены за метр, применяйте поправки и обязательно проверяйте цену сроком продажи. Если считать некогда — это ровно то, что МЕРА делает автоматически.",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Статья «Сколько стоит метр в Екатеринбурге» (13.09.2026).
|
||||
*
|
||||
* ЗАМЕР, НА КОТОРОМ СТОИТ ТЕКСТ. Таблица `deals` — договоры купли-продажи
|
||||
* Росреестра, регион 66. Взяты четыре последних доступных квартала
|
||||
* (`deal_date` 2025-07-01, 2025-10-01, 2026-01-01, 2026-04-01 — дата в данных
|
||||
* штампуется началом квартала, отдельного дня сделки в выгрузке нет), то есть
|
||||
* июль 2025 — июнь 2026. Отсечены цены за метр вне коридора 20 000–500 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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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` в шапке и подвале — та вела в закрытый контур
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 на них не
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue