fix(mera/b2c): вернуть потерянные вопросы FAQ, починить адрес превью и честность двух комментариев
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-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 1m0s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-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 1m0s
Правки по трём ревью ветки «v3 становится корнем».
FAQ. Из шести вопросов `content.ts` возражения v3 брали два, остальные
четыре (`how-accurate`, `vs-marketplace`, `personal-data`, `bank-report`)
не рендерились нигде: `Faq.tsx`, который выводил все шесть, удалён вместе
с лэндингом v1. Макетные возражения их тематически закрывали, но
пересказом — а оригиналы проходили юр-ревью. Содержательно пропали два
утверждения: «Имя, паспорт и документы на квартиру мы не спрашиваем» с
отсылкой к странице ПДн (152-ФЗ) и «показываем диапазон, а не одно число»
как прямой ответ про точность (рамка диапазона осталась только в
`AccuracyV3`, рядом с плейсхолдерными числами).
Теперь в секции один порядок чтения: все шесть вопросов из `FAQ` по id
плюс три макетных возражения, которым в `FAQ` пары нет («факт сделки»,
«почему так дёшево», «подписка»). Три макетных пересказа удалены в пользу
оригиналов; у «почему так дёшево» убран второй абзац — почти дословный
повтор второго абзаца `how-accurate`, который теперь стоит следующим.
Новых формулировок не добавлено. Отсылка к странице ПДн стала живой
ссылкой через `PublicLink` (обычный `<a>`, basePath не течёт); текст
ссылки — слова самого `content.ts`, найденные по вхождению.
Гейт стал двусторонним: тест перебирает `FAQ` и требует, чтобы КАЖДЫЙ id
был отрисован. Прежняя версия перечисляла два id руками и потому молчала
ровно в тот момент, когда четыре ответа исчезли со страницы. Проверено
мутацией: снятие вопроса, переименование id и переформулировка фразы про
ПДн красят тест по отдельности.
CADDY. `/trade-in/mera-public/v3` выбыл из @meraLongPages вместе со
страницей и никуда не попал — длинный адрес превью падал в catch-all 404,
хотя раньше вёл на страницу. Обе его формы добавлены в @meraV3Gone. Там же
редирект перестал терять query: было `redir * /`, стало `redir * {uri}`
со срезанием пути — как у @meraLongPages, где перенос UTM и обоснован.
Проверено на живом Caddy 2.11 по настоящему site-блоку: все четыре формы
дают 301 на `/` с сохранённой query, без параметров — чистый `/` без
хвоста `?`, соседние матчеры не задеты, лишний сегмент по-прежнему 404.
КОММЕНТАРИИ, ОПИСЫВАЮЩИЕ НЕСУЩЕСТВУЮЩЕЕ. Строка `page.tsx` в гейте
плейсхолдеров убрана: комментарий над ней утверждал, что устаревший путь
«перестал бы что-либо сканировать», но ни в корневом `page.tsx`, ни в
прежнем `v3/page.tsx` нет ни одного имени из `PLACEHOLDER_EXPORTS` —
страница только собирает секции. Замер подтверждает: множество найденных
плейсхолдеров со строкой и без неё совпадает, а сама по себе она не даёт
ничего. Держать её значило держать ложное ощущение охвата.
Дескриптор «Оценка вторичного жилья по рыночным данным · <регион>» на
внутренние страницы не возвращается — причина записана в докстринге
`InnerHeader`: эту шапку носят и `/estimate`, `/articles`, `/docs`, где
дескриптора не было никогда; требование юриста было про первый экран
лэндинга и там выполняется `HeroV3`; на самих юр-страницах то же сказано
сильнее — в тексте оферты (п. 1.3, 6.2) и в `FooterV3` на каждой странице.
This commit is contained in:
parent
6976ca8915
commit
49652f4413
7 changed files with 242 additions and 68 deletions
|
|
@ -372,12 +372,35 @@ meraocenka.ru {
|
||||||
# двух недель: владелец сравнивал по нему версии, и ссылка почти наверняка
|
# двух недель: владелец сравнивал по нему версии, и ссылка почти наверняка
|
||||||
# осталась в закладках и в переписке. Поэтому 301 на корень, а не молчание
|
# осталась в закладках и в переписке. Поэтому 301 на корень, а не молчание
|
||||||
# белого списка: человек по старой ссылке попадает ровно на то, что по ней
|
# белого списка: человек по старой ссылке попадает ровно на то, что по ней
|
||||||
# и смотрел, только теперь это главная. Обе формы — со слэшем и без:
|
# и смотрел, только теперь это главная.
|
||||||
# `/v3/` вышел из @meraShortSlash вместе с остальным превью и иначе упал бы
|
#
|
||||||
# в catch-all 404.
|
# ЧЕТЫРЕ ФОРМЫ, А НЕ ДВЕ. Превью раздавалось и коротким `/v3`, и длинным
|
||||||
@meraV3Gone path /v3 /v3/
|
# `/trade-in/mera-public/v3` — вторая пара до 29.08.2026 жила в
|
||||||
|
# @meraLongPages и оттуда выбыла вместе со страницей. Короткие формы её
|
||||||
|
# не подхватывают (матчер `path` требует точного совпадения), поэтому
|
||||||
|
# длинный адрес превью падал в catch-all 404 — тот же класс дефекта,
|
||||||
|
# ради которого длинные адреса вообще оставлены живыми. Все четыре формы
|
||||||
|
# ведут в один корень, поэтому и матчер один.
|
||||||
|
#
|
||||||
|
# QUERY ПЕРЕНОСИТСЯ — `redir * {uri}`, как в @meraLongPages, а не
|
||||||
|
# `redir * /`, как было здесь до этой правки. Причина ровно та же, что
|
||||||
|
# записана у @meraLongPages: по разосланной ссылке приходят с UTM-метками,
|
||||||
|
# и `/v3?utm=…` не должен терять атрибуцию на редиректе. Оба
|
||||||
|
# `uri strip_prefix` срезают путь целиком (для `/v3` — до пустого, для
|
||||||
|
# `/v3/` — до `/`), а `{uri}` при пустом пути даёт `/`, поэтому запрос без
|
||||||
|
# параметров отдаёт чистый `Location: /` без хвоста `?`. Обёртка `route`
|
||||||
|
# обязательна по той же причине, что у @meraLongPages: внутри `handle`
|
||||||
|
# порядок директив определяет Caddy, и без неё `redir` выполнится раньше
|
||||||
|
# `uri`. Проверено на живом Caddy 2.11 (прод — `caddy:2`): все четыре формы
|
||||||
|
# с параметрами и без дают 301 на `/` с сохранённой query, а лишний
|
||||||
|
# сегмент (`/trade-in/mera-public/v3/evil`) по-прежнему падает в 404.
|
||||||
|
@meraV3Gone path /v3 /v3/ /trade-in/mera-public/v3 /trade-in/mera-public/v3/
|
||||||
handle @meraV3Gone {
|
handle @meraV3Gone {
|
||||||
redir * / permanent
|
route {
|
||||||
|
uri strip_prefix /trade-in/mera-public
|
||||||
|
uri strip_prefix /v3
|
||||||
|
redir * {uri} permanent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Длинные адреса поддерева → 301 на короткие. Один канонический адрес у
|
# Длинные адреса поддерева → 301 на короткие. Один канонический адрес у
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,21 @@ function read(rel: string): string {
|
||||||
return readFileSync(join(ROOT, rel), "utf8");
|
return readFileSync(join(ROOT, rel), "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Компоненты лэндинга v3, которые могут ссылаться на плейсхолдеры. */
|
/**
|
||||||
|
* Компоненты лэндинга v3, которые могут ссылаться на плейсхолдеры.
|
||||||
|
*
|
||||||
|
* САМОЙ СТРАНИЦЫ (`page.tsx`) ЗДЕСЬ НЕТ, И ЭТО НЕ ПРОПУСК. 29.08.2026 лэндинг
|
||||||
|
* переехал из `v3/page.tsx` в корень, и путь в этом списке обновили вслед за
|
||||||
|
* файлом — с обоснованием «иначе строка перестала бы что-либо сканировать».
|
||||||
|
* Обоснование звучало как проверенный факт, но проверено не было: ни в
|
||||||
|
* `page.tsx`, ни в прежнем `v3/page.tsx` нет ни одного имени из
|
||||||
|
* `PLACEHOLDER_EXPORTS` — страница только собирает секции из импортов, а числа
|
||||||
|
* живут в самих секциях. То есть строка не давала покрытия ни до переезда, ни
|
||||||
|
* после, и держать её значило держать ложное ощущение охвата. Появится величина
|
||||||
|
* прямо в разметке страницы — строку сюда вернуть (и тогда она будет что-то
|
||||||
|
* значить).
|
||||||
|
*/
|
||||||
const V3_SOURCES = [
|
const V3_SOURCES = [
|
||||||
// 29.08.2026: лэндинг v3 переехал из `v3/page.tsx` в корень. Путь обязан
|
|
||||||
// ехать вместе с ним — `placeholdersInUse()` молча пропускает ненайденный
|
|
||||||
// файл (это правильно для переименований), поэтому устаревшая строка не
|
|
||||||
// покраснела бы, а просто перестала бы что-либо сканировать.
|
|
||||||
"page.tsx",
|
|
||||||
"_components/v3/HeroV3.tsx",
|
"_components/v3/HeroV3.tsx",
|
||||||
"_components/v3/AccuracyV3.tsx",
|
"_components/v3/AccuracyV3.tsx",
|
||||||
"_components/v3/FreeCheckCard.tsx",
|
"_components/v3/FreeCheckCard.tsx",
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,11 @@ describe("короткие адреса публичного домена", () =
|
||||||
*
|
*
|
||||||
* Правильный ответ ровно один — 301 на корень, и он проверяется здесь, а не
|
* Правильный ответ ровно один — 301 на корень, и он проверяется здесь, а не
|
||||||
* держится на памяти о том, почему в конфиге появился ещё один матчер.
|
* держится на памяти о том, почему в конфиге появился ещё один матчер.
|
||||||
|
*
|
||||||
|
* ФОРМ ЧЕТЫРЕ, А НЕ ДВЕ. Превью раздавалось и коротким `/v3`, и длинным
|
||||||
|
* `/trade-in/mera-public/v3`; вторая пара выбыла из @meraLongPages вместе со
|
||||||
|
* страницей и никуда не попала — длинный адрес превью отдавал 404, ровно тот
|
||||||
|
* же дефект, что проверка выше ловит для страниц.
|
||||||
*/
|
*/
|
||||||
it("адрес выбывшего превью /v3 отдаёт 301 на корень, а не 404", () => {
|
it("адрес выбывшего превью /v3 отдаёт 301 на корень, а не 404", () => {
|
||||||
const matcher = meraBlock.match(/@meraV3Gone path ([^\n]+)/)?.[1] ?? "";
|
const matcher = meraBlock.match(/@meraV3Gone path ([^\n]+)/)?.[1] ?? "";
|
||||||
|
|
@ -203,14 +208,32 @@ describe("короткие адреса публичного домена", () =
|
||||||
expect(
|
expect(
|
||||||
paths,
|
paths,
|
||||||
"нет матчера @meraV3Gone — старые ссылки на превью упадут в catch-all 404",
|
"нет матчера @meraV3Gone — старые ссылки на превью упадут в catch-all 404",
|
||||||
).toEqual(["/v3", "/v3/"]);
|
).toEqual([
|
||||||
|
"/v3",
|
||||||
|
"/v3/",
|
||||||
|
"/trade-in/mera-public/v3",
|
||||||
|
"/trade-in/mera-public/v3/",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Тело `handle` содержит вложенный `route { … }`, поэтому берём до
|
||||||
|
// закрывающей скобки НА УРОВНЕ handle (четыре пробела), а не до первой.
|
||||||
const handler =
|
const handler =
|
||||||
meraBlock.match(/handle @meraV3Gone \{([^}]*)\}/)?.[1] ?? "";
|
meraBlock.match(/handle @meraV3Gone \{([\s\S]*?)\n {4}\}/)?.[1] ?? "";
|
||||||
expect(
|
expect(
|
||||||
handler,
|
handler,
|
||||||
"матчер есть, а редиректа в нём нет — зонд выше прошёл бы вхолостую",
|
"матчер есть, а редиректа в нём нет — зонд выше прошёл бы вхолостую",
|
||||||
).toMatch(/redir\s+\*\s+\/\s+permanent/);
|
).toMatch(/redir\s+\*\s+\{uri\}\s+permanent/);
|
||||||
|
/**
|
||||||
|
* Query обязана переноситься — иначе `/v3?utm=…` теряет атрибуцию у уже
|
||||||
|
* разосланных ссылок. Один `redir * {uri}` этого не гарантирует: без
|
||||||
|
* срезания пути он вернул бы Location, равный исходному адресу (цикл).
|
||||||
|
* Поэтому проверяется вся связка, а не только форма редиректа.
|
||||||
|
*/
|
||||||
|
expect(
|
||||||
|
handler,
|
||||||
|
"путь не срезается — {uri} вернёт исходный адрес, редирект закольцуется",
|
||||||
|
).toContain("uri strip_prefix /v3");
|
||||||
|
expect(handler).toContain("uri strip_prefix /trade-in/mera-public");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { FAQ } from "../content";
|
||||||
* Новый дизайн жил на /v3 и 29.08.2026 стал корнем meraocenka.ru. Перед этим
|
* Новый дизайн жил на /v3 и 29.08.2026 стал корнем meraocenka.ru. Перед этим
|
||||||
* разведка нашла блоки лэндинга v1, которых в v3 не было вовсе: реестр
|
* разведка нашла блоки лэндинга v1, которых в v3 не было вовсе: реестр
|
||||||
* источников, «что вы получите» с дисклеймером 135-ФЗ, плашка покрытия и
|
* источников, «что вы получите» с дисклеймером 135-ФЗ, плашка покрытия и
|
||||||
* строка «мы не покупаем квартиры» в герое, два вопроса из FAQ, город в форме.
|
* строка «мы не покупаем квартиры» в герое, вопросы FAQ, город в форме.
|
||||||
* Они перенесены — здесь проверяется, что перенос ЖИВОЙ: секции подключены к
|
* Они перенесены — здесь проверяется, что перенос ЖИВОЙ: секции подключены к
|
||||||
* странице, а компоненты читают те же константы, а не копии строк.
|
* странице, а компоненты читают те же константы, а не копии строк.
|
||||||
*
|
*
|
||||||
|
|
@ -70,19 +70,62 @@ describe("секции читают константы, а не копии ст
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("два вопроса из FAQ v1 живут в возражениях v3", () => {
|
describe("КАЖДЫЙ вопрос FAQ живёт в возражениях v3", () => {
|
||||||
const objections = read("_components/v3/ObjectionsV3.tsx");
|
const objections = read("_components/v3/ObjectionsV3.tsx");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверка ДВУСТОРОННЯЯ: id должен и существовать в FAQ, и упоминаться в
|
* Список вопросов НЕ ЗАХАРДКОЖЕН — он перебирается из самого `FAQ`. В этом
|
||||||
* секции. Односторонняя пропустила бы главную ошибку — переименование id, от
|
* вся разница с прежней версией проверки: она перечисляла два id вручную и
|
||||||
* которого `FAQ.find` вернёт undefined, и вопрос молча исчезнет со страницы.
|
* потому была зелёной ровно в тот момент, когда `Faq.tsx` (единственный, кто
|
||||||
|
* рендерил все шесть) удалили вместе с лэндингом v1, а в возражения v3
|
||||||
|
* перенесли только эти два. Четыре ответа, прошедшие юр-ревью, не
|
||||||
|
* рендерились нигде, и ни один гейт этого не сказал.
|
||||||
|
*
|
||||||
|
* Перебор по `FAQ` краснеет в обе стороны:
|
||||||
|
* - вопрос убрали из ObjectionsV3 → его id в файле нет → красный;
|
||||||
|
* - id переименовали в content.ts → в файле остался СТАРЫЙ id, `FAQ.find`
|
||||||
|
* молча вернёт undefined и вопрос исчезнет со страницы → красный;
|
||||||
|
* - в content.ts добавили седьмой вопрос → он обязан быть либо отрисован,
|
||||||
|
* либо осознанно исключён (тогда правится и этот тест), а не потерян.
|
||||||
|
*
|
||||||
|
* Ищем `...fromFaq("id")` со спредом, а не просто id: спред — это то, что
|
||||||
|
* кладёт вопрос в `ITEMS`. Объявление без спреда собралось бы, тайпчек бы
|
||||||
|
* промолчал, а на странице вопроса не было бы.
|
||||||
*/
|
*/
|
||||||
it.each(["how-do-you-know", "why-region"])("%s", (id) => {
|
it("в FAQ вообще есть вопросы — иначе перебор ниже пустой", () => {
|
||||||
|
expect(FAQ.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(FAQ.map((f) => f.id))("%s отрисован", (id) => {
|
||||||
expect(
|
expect(
|
||||||
FAQ.map((f) => f.id),
|
objections,
|
||||||
`id пропал из FAQ — ObjectionsV3 отрендерит на один вопрос меньше и никто не заметит`,
|
`вопрос "${id}" не попадает в ITEMS — на странице его не будет, и заметить это может только человек`,
|
||||||
).toContain(id);
|
).toContain(`...fromFaq("${id}")`);
|
||||||
expect(objections).toContain(`"${id}"`);
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отсылка к странице про обработку персональных данных обязана быть ЖИВОЙ
|
||||||
|
* ССЫЛКОЙ. В `content.ts` она лежит текстом, ObjectionsV3 линкует её по
|
||||||
|
* дословному совпадению — значит переформулировка предложения в content.ts
|
||||||
|
* тихо превращает ссылку обратно в текст. Проверка сверяет обе стороны.
|
||||||
|
*/
|
||||||
|
it("отсылка к странице ПДн отдаётся ссылкой, а не текстом", () => {
|
||||||
|
const phrase =
|
||||||
|
objections.match(/const PRIVACY_PHRASE = "([^"]+)"/)?.[1] ?? "";
|
||||||
|
expect(
|
||||||
|
phrase,
|
||||||
|
"зонд сломан: в ObjectionsV3 не найдена константа связующей фразы",
|
||||||
|
).not.toBe("");
|
||||||
|
|
||||||
|
const answer = FAQ.find((f) => f.id === "personal-data")?.a.join(" ") ?? "";
|
||||||
|
expect(
|
||||||
|
answer,
|
||||||
|
"фраза в content.ts переформулирована — ссылка на страницу ПДн молча станет обычным текстом",
|
||||||
|
).toContain(phrase);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
objections,
|
||||||
|
"ссылка обязана идти через PublicLink (обычный <a>): next/link подставит basePath",
|
||||||
|
).toContain('<PublicLink to="privacy"');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,32 @@ import { PublicLink } from "./PublicLink";
|
||||||
*
|
*
|
||||||
* Логотип отсюда — ссылка на главную: это единственный обратный путь на
|
* Логотип отсюда — ссылка на главную: это единственный обратный путь на
|
||||||
* лэндинг, и он же то, что человек инстинктивно нажимает.
|
* лэндинг, и он же то, что человек инстинктивно нажимает.
|
||||||
|
*
|
||||||
|
* ДЕСКРИПТОРА «ОЦЕНКА ВТОРИЧНОГО ЖИЛЬЯ ПО РЫНОЧНЫМ ДАННЫМ · <РЕГИОН>» ЗДЕСЬ
|
||||||
|
* НЕТ — И ЭТО РЕШЕНИЕ, А НЕ ПОТЕРЯ. Строка жила в `SiteHeader.tsx` (шапка
|
||||||
|
* лэндинга v1) с формулировкой, о которой просил юрист 14.08.2026: слова «по
|
||||||
|
* рыночным данным» — то, чем сервис отличается от оценки по 135-ФЗ, и они
|
||||||
|
* должны стоять в заметном дескрипторе, а не только в дисклеймерах внизу.
|
||||||
|
* 29.08.2026 `SiteHeader` удалён, юр-страницы переведены на эту шапку, и
|
||||||
|
* строка с них ушла.
|
||||||
|
*
|
||||||
|
* Почему она сюда не возвращается:
|
||||||
|
* - ЭТО ШАПКА НЕ ТОЛЬКО ЮР-СТРАНИЦ. Её же носят `/estimate`, `/articles`,
|
||||||
|
* `/docs`, у которых дескриптора не было никогда: возврат «на место»
|
||||||
|
* означал бы новую строку на трёх страницах, которых правка не касается.
|
||||||
|
* - ТРЕБОВАНИЕ ЮРИСТА БЫЛО ПРО ЛЭНДИНГ — про первое, что видит посетитель.
|
||||||
|
* На корне оно выполняется и сейчас, только не шапкой: `HeroV3` первым же
|
||||||
|
* экраном пишет «„Мера“ — сервис оценки вторичного жилья по рыночным
|
||||||
|
* данным», и там же стоит граница покрытия.
|
||||||
|
* - НА САМИХ ЮР-СТРАНИЦАХ ЭТО СКАЗАНО СИЛЬНЕЕ, ЧЕМ В ДЕСКРИПТОРЕ. Оферта
|
||||||
|
* говорит то же в тексте договора (п. 1.3 «индикативный расчёт
|
||||||
|
* ориентировочной рыночной стоимости», п. 6.2 «не является отчётом об
|
||||||
|
* оценке по 135-ФЗ»), а `FooterV3` на каждой из этих страниц несёт и
|
||||||
|
* рыночную природу оценки с регионом («Оценка квартир на вторичном рынке
|
||||||
|
* по сделкам и объявлениям. <регион>»), и прямой отказ («не является
|
||||||
|
* офертой или отчётом об оценке»).
|
||||||
|
* Появится страница, где ни то ни другое не выполняется, — дескриптор нужно
|
||||||
|
* вернуть ей, а не этой общей шапке.
|
||||||
*/
|
*/
|
||||||
export function InnerHeader() {
|
export function InnerHeader() {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -12,17 +12,39 @@
|
||||||
* объявлений с публикации), а не макетной формулировкой про «отметки
|
* объявлений с публикации), а не макетной формулировкой про «отметки
|
||||||
* продавцов», механики которых не существует.
|
* продавцов», механики которых не существует.
|
||||||
*
|
*
|
||||||
* ДВА ВОПРОСА ИЗ v1 (`FAQ` в content.ts) ИМПОРТИРУЮТСЯ, А НЕ ПЕРЕПИСАНЫ.
|
* ВЕСЬ `FAQ` ИЗ content.ts ИМПОРТИРУЕТСЯ, А НЕ ПЕРЕПИСЫВАЕТСЯ. Так было не
|
||||||
* Макетные шесть возражений — про оплату и доверие; в v1 было два вопроса,
|
* сразу: при переносе брались только `how-do-you-know` и `why-region`, а
|
||||||
* которых среди них нет вовсе, и оба несут границы продукта:
|
* остальные четыре вопроса v1 (`how-accurate`, `vs-marketplace`,
|
||||||
* - `how-do-you-know` — как вообще получается цена именно этой квартиры;
|
* `personal-data`, `bank-report`) не рендерились НИГДЕ — старый компонент
|
||||||
* - `why-region` — почему одна область, и чем «полное покрытие» отличается
|
* `Faq.tsx`, который выводил все шесть, удалён вместе с лэндингом v1.
|
||||||
* от «данных меньше».
|
* Тематически их закрывали макетные возражения, но это были ПЕРЕСКАЗЫ, а
|
||||||
* Первый стоит ПЕРВЫМ (это вопрос до всяких сомнений об оплате), второй —
|
* оригиналы проходили юр-ревью. Две потери были содержательными:
|
||||||
* последним (география — то, с чем человек уходит). Берём их из `FAQ` по id, а
|
* - `personal-data` — единственное место на сайте, где сказано «Имя, паспорт
|
||||||
* не копией строк: копия разъедется с ответом на странице /docs и в v1, где
|
* и документы на квартиру мы не спрашиваем», и единственная отсылка к
|
||||||
* тот же текст уже прошёл юр-ревью. Если id переименуют, вопрос молча
|
* странице про обработку персональных данных (152-ФЗ). Макетная версия
|
||||||
* исчезнет — это гейтится `__tests__/v3-ported-copy.test.ts`.
|
* оставляла только «отдельную галочку согласия», без ссылки;
|
||||||
|
* - `how-accurate` — «показываем диапазон, а не одно число» как прямой ответ
|
||||||
|
* на вопрос о точности. После потери рамка диапазона осталась только в
|
||||||
|
* `AccuracyV3`, рядом с плейсхолдерными числами.
|
||||||
|
*
|
||||||
|
* ЧТО СДЕЛАНО. Список ниже — один порядок чтения, в который вплетены и все
|
||||||
|
* шесть вопросов `FAQ` (по id, а не копией строк), и те макетные возражения,
|
||||||
|
* которым в `FAQ` пары нет вовсе: «откуда вы знаете факт сделки», «почему так
|
||||||
|
* дёшево», «это подписка». Три остальных макетных возражения — про
|
||||||
|
* калькуляторы, звонки и отчёт для банка — УДАЛЕНЫ как пересказы
|
||||||
|
* `vs-marketplace` / `personal-data` / `bank-report`: держать рядом два ответа
|
||||||
|
* на один вопрос хуже, чем один, а выбор между пересказом и выверенным
|
||||||
|
* оригиналом очевиден. По той же причине у «почему так дёшево» убран второй
|
||||||
|
* абзац — он был почти дословным повтором второго абзаца `how-accurate`,
|
||||||
|
* который теперь стоит следующим вопросом и отвечает на «это точно работает?»
|
||||||
|
* целиком.
|
||||||
|
*
|
||||||
|
* Ни одной строки не написано здесь заново: тексты либо приходят из
|
||||||
|
* `content.ts`, либо это уже стоявшие в этом файле макетные возражения.
|
||||||
|
*
|
||||||
|
* Если id переименуют или вопрос выпадет из списка, он молча исчезнет со
|
||||||
|
* страницы — гейтится `__tests__/v3-ported-copy.test.ts` двусторонне: тест
|
||||||
|
* перебирает `FAQ` и требует, чтобы КАЖДЫЙ его id был отрисован здесь.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FAQ } from "../../content";
|
import { FAQ } from "../../content";
|
||||||
|
|
@ -35,7 +57,20 @@ interface Objection {
|
||||||
readonly a: readonly string[];
|
readonly a: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const OBJECTIONS: readonly Objection[] = [
|
/** Вопрос из `FAQ` по id — `[]`, если id переименовали (тест это ловит). */
|
||||||
|
function fromFaq(id: string): readonly Objection[] {
|
||||||
|
const item = FAQ.find((f) => f.id === id);
|
||||||
|
return item ? [{ q: item.q, a: item.a }] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Порядок чтения: как получается цена → откуда факт → сколько стоит и почему
|
||||||
|
* столько → насколько точно → чем отличается от бесплатного → как списывают
|
||||||
|
* деньги → что с данными → годится ли для банка → почему одна область.
|
||||||
|
* Сомнения про деньги стоят посередине, границы продукта — по краям.
|
||||||
|
*/
|
||||||
|
const ITEMS: readonly Objection[] = [
|
||||||
|
...fromFaq("how-do-you-know"),
|
||||||
{
|
{
|
||||||
q: "Откуда вы знаете факт сделки?",
|
q: "Откуда вы знаете факт сделки?",
|
||||||
a: [
|
a: [
|
||||||
|
|
@ -46,48 +81,50 @@ const OBJECTIONS: readonly Objection[] = [
|
||||||
q: "Почему так дёшево — это точно работает?",
|
q: "Почему так дёшево — это точно работает?",
|
||||||
a: [
|
a: [
|
||||||
"Дёшево, потому что расчёт автоматический: без выезда, без оценщика, без комиссии. Вы платите за данные, а не за часы специалиста.",
|
"Дёшево, потому что расчёт автоматический: без выезда, без оценщика, без комиссии. Вы платите за данные, а не за часы специалиста.",
|
||||||
"Точность прямо зависит от того, сколько нашлось сопоставимых объектов, — поэтому мы всегда пишем, на скольких объектах и сделках построен расчёт, и честно сообщаем, если данных по дому мало.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
q: "Чем это лучше бесплатных калькуляторов?",
|
|
||||||
a: [
|
|
||||||
"Калькулятор площадки считает по объявлениям этой же площадки — то есть по ценам, которые продавцы просят, а не получают.",
|
|
||||||
"Мы смотрим сразу несколько площадок и добавляем зарегистрированные сделки, чтобы была видна разница между запрашиваемой и реальной ценой.",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
...fromFaq("how-accurate"),
|
||||||
|
...fromFaq("vs-marketplace"),
|
||||||
{
|
{
|
||||||
q: "Это подписка? Спишут ли ещё раз?",
|
q: "Это подписка? Спишут ли ещё раз?",
|
||||||
a: [
|
a: [
|
||||||
"Нет. Это разовый платёж за один отчёт — без подписки, автопродлений и скрытых списаний. Отчёт остаётся у вас по ссылке.",
|
"Нет. Это разовый платёж за один отчёт — без подписки, автопродлений и скрытых списаний. Отчёт остаётся у вас по ссылке.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
...fromFaq("personal-data"),
|
||||||
q: "Мне начнут звонить после проверки?",
|
...fromFaq("bank-report"),
|
||||||
a: [
|
|
||||||
"Нет. Для расчёта нужны адрес и параметры квартиры — телефон мы не спрашиваем. Он появляется, только если вы сами решите оставить заявку, и всегда с отдельной галочкой согласия.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
q: "Это официальная оценка для банка?",
|
|
||||||
a: [
|
|
||||||
"Нет. Для ипотеки, суда, опеки и нотариуса нужен отчёт аккредитованного оценщика по 135-ФЗ — это отдельная процедура с выездом. МЕРА отвечает на другой вопрос: за сколько квартира реально продаётся на рынке сегодня.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Вопрос v1 по id — `[]`, если id переименовали (тест это ловит). */
|
|
||||||
function fromFaq(id: string): readonly Objection[] {
|
|
||||||
const item = FAQ.find((f) => f.id === id);
|
|
||||||
return item ? [{ q: item.q, a: item.a }] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ITEMS: readonly Objection[] = [
|
|
||||||
...fromFaq("how-do-you-know"),
|
|
||||||
...OBJECTIONS,
|
|
||||||
...fromFaq("why-region"),
|
...fromFaq("why-region"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отсылка к странице про обработку персональных данных внутри ответа
|
||||||
|
* `personal-data`. В `content.ts` она лежит обычным текстом (там нет разметки),
|
||||||
|
* и в старом `Faq.tsx` так текстом и рендерилась — то есть 152-ФЗ-документ
|
||||||
|
* назывался, но открыть его отсюда было нельзя.
|
||||||
|
*
|
||||||
|
* Фраза вынесена константой, а не зашита в разметку: это КОПИЯ куска текста из
|
||||||
|
* `content.ts`, и разъехаться она может молча — переформулируют предложение, и
|
||||||
|
* `indexOf` не найдёт ничего, а ссылка тихо станет обычным текстом. Ровно это
|
||||||
|
* и проверяет `v3-ported-copy.test.ts`: фраза обязана дословно встречаться в
|
||||||
|
* ответе `personal-data`.
|
||||||
|
*/
|
||||||
|
const PRIVACY_PHRASE = "странице про обработку персональных данных";
|
||||||
|
|
||||||
|
/** Абзац ответа: фраза выше отдаётся ссылкой, остальное — как есть. */
|
||||||
|
function renderAnswer(text: string) {
|
||||||
|
const at = text.indexOf(PRIVACY_PHRASE);
|
||||||
|
if (at < 0) return text;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{text.slice(0, at)}
|
||||||
|
<PublicLink to="privacy" className={styles.objAnswerLink}>
|
||||||
|
{PRIVACY_PHRASE}
|
||||||
|
</PublicLink>
|
||||||
|
{text.slice(at + PRIVACY_PHRASE.length)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ObjectionsV3() {
|
export function ObjectionsV3() {
|
||||||
return (
|
return (
|
||||||
<section className={styles.objSection} aria-labelledby="obj-v3-title">
|
<section className={styles.objSection} aria-labelledby="obj-v3-title">
|
||||||
|
|
@ -110,7 +147,7 @@ export function ObjectionsV3() {
|
||||||
</summary>
|
</summary>
|
||||||
<div className={styles.objAnswer}>
|
<div className={styles.objAnswer}>
|
||||||
{o.a.map((p) => (
|
{o.a.map((p) => (
|
||||||
<p key={p.slice(0, 24)}>{p}</p>
|
<p key={p.slice(0, 24)}>{renderAnswer(p)}</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
|
||||||
|
|
@ -2127,6 +2127,20 @@
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ссылка ВНУТРИ предложения (отсылка к странице про обработку персональных
|
||||||
|
данных в ответе `personal-data`) — то же inline-исключение из правила зоны
|
||||||
|
нажатия, что у .ftrInlineLink: раздувать строку текста было бы хуже. */
|
||||||
|
.objAnswerLink {
|
||||||
|
color: var(--b2c-accent-text);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objAnswerLink:hover {
|
||||||
|
color: var(--b2c-ink);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Статьи --------------------------------------------------------------- */
|
/* --- Статьи --------------------------------------------------------------- */
|
||||||
.artSection {
|
.artSection {
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue