All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m5s
Сайт открыт для индексации 10.09.2026, но поисковику он до сих пор
представлялся девятью страницами без единого структурированного факта о
том, кто их публикует и что продаёт, а ссылка на него в мессенджере
разворачивалась голым текстом без картинки.
МИКРОРАЗМЕТКА. Конструкторы узлов вынесены в `_lib/schema.ts` — чистый
модуль без React, по образцу соседнего `_lib/analytics.ts`. Все значения
берутся из `content.ts`, ни одной строки не продублировано:
- `Organization` + `WebSite` одним `@graph` в layout поддерева, то есть на
всех девяти страницах сразу. Остальные узлы ссылаются на организацию
через `@id`, а не повторяют реквизиты у себя.
- `Service` с `Offer` на лэндинге.
- `FAQPage` в блоке возражений.
- `BreadcrumbList` на статьях, статье, документах и странице для бизнеса —
ровно по тем крошкам, что видны на экране.
- Существующий `Article` переведён на ссылку `publisher: { "@id": ... }`.
РАЗМЕТКА НЕ ОБЕЩАЕТ ТОГО, ЧЕГО НЕТ. Два места, где это стоило внимания:
- `Offer.availability` вычисляется из `PUBLIC_ESTIMATE_ENABLED`. Флаг сейчас
`false` (платёжного контура в коде нет), поэтому в разметку уходит
`PreOrder`, а не `InStock`: цена опубликована, купить нельзя, и врать об
этом поисковику нечего. Включится приём оплаты — значение сменится само.
- `FAQPage` строится из того же массива `ITEMS`, который рисует `<details>`,
а не из `FAQ` напрямую: в `ITEMS` часть вопросов заменена макетными
формулировками, и разметка от сырого `FAQ` разошлась бы с видимым текстом
молча. Выдуманных дат, рейтингов и отзывов не добавлено нигде.
КАРТИНКИ. Две штуки, и это не дубль: `og-mera.png` 1200×630 — превью ссылки,
`logo-mera.png` 512×512 — логотип организации в разметке, который поисковик
обрезает близко к квадрату и где баннер превратился бы в обрезок надписи.
Исходники обеих лежат рядом в `scripts/og/*.html` вместе с командой
перерисовки: картинка должна оставаться правимой, а не только
переоткрываемой в графическом редакторе.
Готовые PNG, а не `opengraph-image.tsx`: satori внутри `ImageResponse` рисует
только переданными ему байтами шрифта и кириллицу по умолчанию не покрывает,
плюс не видит ни CSS Modules, ни наших `--b2c-*`. Разбор — в шапке
`scripts/og/og-mera.html`.
`images` продублирован в `twitter`: Next не переносит их из `openGraph`, когда
метаданные заданы объектом, и карточка обещала бы крупное превью без картинки.
ПЕРИМЕТР. Обе картинки лежат в `public/` и раздаются Next'ом по
basePath-корню, куда rewrite `@meraPages` не достаёт, — отсюда два отдельных
`handle` по образцу robots.txt. Оба названы в `ROOT_HANDLES_ALLOWED`, иначе
двусторонний гейт периметра покраснел бы, и это правильно: корневой `handle`
— вторая дверь в тот же периметр.
Смоук проверяет не только 200, но и Content-Type: перепутанный rewrite отдаёт
200 с HTML, обходчик мессенджера молча его отбрасывает, и по логам приложения
этого не видно — запрос туда не доходит.
Проверено: tsc, eslint, 215 тестов mera-public, изоляция B2C-дерева, caddy
validate, bash -n смоука, прод-сборка с basePath и разбор отрендеренного
HTML — JSON-LD парсится, og:image и twitter:image на месте.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiUFZ3rmTNpp3DRajUo8KQ
264 lines
8.5 KiB
HTML
264 lines
8.5 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>OG-карточка МЕРА</title>
|
||
<!--
|
||
ИСХОДНИК КАРТИНКИ ДЛЯ ПРЕВЬЮ ССЫЛОК (og:image), 1200x630.
|
||
|
||
Отсюда рендерится `public/og-mera.png` — тот файл и уходит в мессенджеры и
|
||
соцсети. Держим исходник в репозитории, чтобы картинку можно было
|
||
перерисовать, а не только переоткрыть в графическом редакторе.
|
||
|
||
КАК ПЕРЕРИСОВАТЬ (нужен установленный Chrome/Chromium):
|
||
chrome --headless --disable-gpu --hide-scrollbars \
|
||
--screenshot=public/og-mera.png --window-size=1200,630 \
|
||
scripts/og/og-mera.html
|
||
|
||
ПОЧЕМУ ГОТОВЫЙ PNG, А НЕ `opengraph-image.tsx` (ImageResponse из next/og).
|
||
Тот путь красивее по духу — картинка собиралась бы из кода на каждой сборке,
|
||
— но у него два практических изъяна ровно для нашего случая. Первый: satori
|
||
внутри ImageResponse рисует ТОЛЬКО теми шрифтами, которые ему передали
|
||
байтами, а дефолтный набор кириллицы не покрывает — пришлось бы положить
|
||
.ttf в репозиторий и читать его в рантайме. Второй: satori понимает лишь
|
||
подмножество CSS и не видит ни CSS Modules, ни наших `--b2c-*` переменных,
|
||
так что «те же токены, что на сайте» там всё равно превратились бы в
|
||
отдельный набор литералов. Готовый PNG отдаётся статикой, кэшируется
|
||
навсегда и не стоит ничего в рантайме.
|
||
|
||
ЦВЕТА ЗДЕСЬ ЛИТЕРАЛАМИ — это единственное место в публичном дереве, где так
|
||
можно: файл не участвует в сборке фронта, `var(--b2c-*)` в нём резолвить
|
||
некому. Значения обязаны совпадать с `src/app/mera-public/b2c-tokens.ts`;
|
||
меняются токены — меняется и эта карточка, руками.
|
||
-->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;800&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<style>
|
||
/* Значения — копия b2c-tokens.ts (см. комментарий выше). */
|
||
:root {
|
||
--page-bg: #F4F7F8;
|
||
--surface: #FFFFFF;
|
||
--surface-soft: #EAF5F7;
|
||
--ink: #121A1D;
|
||
--muted: #5B686C;
|
||
--line: #CBD7DA;
|
||
--accent: #16A9BC;
|
||
--accent-text: #0B6B79;
|
||
}
|
||
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
html, body {
|
||
width: 1200px;
|
||
height: 630px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
body {
|
||
background: var(--page-bg);
|
||
font-family: "Manrope", system-ui, sans-serif;
|
||
color: var(--ink);
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
.card {
|
||
position: relative;
|
||
width: 1200px;
|
||
height: 630px;
|
||
padding: 60px 72px 56px;
|
||
display: grid;
|
||
grid-template-columns: 640px 1fr;
|
||
grid-template-rows: auto 1fr auto;
|
||
column-gap: 56px;
|
||
align-items: start;
|
||
}
|
||
|
||
/* Акцентная полоса по левому краю — тот же приём, что у секций лэндинга. */
|
||
.card::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 10px;
|
||
background: var(--accent);
|
||
}
|
||
|
||
/* --- шапка: знак и словесный логотип, как в HeaderV3 --- */
|
||
.logo {
|
||
grid-column: 1 / -1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
|
||
.logoIcon {
|
||
width: 46px;
|
||
height: 46px;
|
||
border: 2.5px solid var(--accent);
|
||
border-radius: 7px;
|
||
display: grid;
|
||
place-items: center;
|
||
}
|
||
|
||
.logoDot {
|
||
width: 15px;
|
||
height: 15px;
|
||
background: var(--accent);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.logoText { display: flex; flex-direction: column; line-height: 1; }
|
||
.logoName { font-weight: 800; font-size: 30px; letter-spacing: 0.06em; }
|
||
.logoSub {
|
||
margin-top: 5px;
|
||
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
||
font-size: 14px;
|
||
letter-spacing: 0.18em;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* --- середина слева: заголовок и подзаголовок --- */
|
||
.pitch { align-self: center; }
|
||
|
||
.headline {
|
||
font-size: 58px;
|
||
line-height: 1.1;
|
||
font-weight: 800;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
|
||
.lead {
|
||
margin-top: 24px;
|
||
font-size: 25px;
|
||
line-height: 1.4;
|
||
font-weight: 500;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* --- середина справа: диапазон цены. Продукт называет ВИЛКУ, а не одно
|
||
число, и картинка обязана говорить то же самое. --- */
|
||
.range {
|
||
align-self: center;
|
||
padding: 28px 30px 32px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--line);
|
||
border-radius: 18px;
|
||
}
|
||
|
||
.rangeLabel {
|
||
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
||
font-size: 13px;
|
||
letter-spacing: 0.16em;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.rangeBar {
|
||
margin-top: 22px;
|
||
height: 12px;
|
||
border-radius: 6px;
|
||
background: var(--surface-soft);
|
||
position: relative;
|
||
}
|
||
|
||
.rangeFill {
|
||
position: absolute;
|
||
left: 18%;
|
||
right: 22%;
|
||
top: 0;
|
||
bottom: 0;
|
||
border-radius: 6px;
|
||
background: var(--accent);
|
||
}
|
||
|
||
.rangeMark {
|
||
position: absolute;
|
||
left: 52%;
|
||
top: -7px;
|
||
width: 4px;
|
||
height: 26px;
|
||
border-radius: 2px;
|
||
background: var(--ink);
|
||
}
|
||
|
||
.rangeEnds {
|
||
margin-top: 16px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: var(--ink);
|
||
}
|
||
|
||
.rangeNote {
|
||
margin-top: 18px;
|
||
font-size: 15px;
|
||
line-height: 1.45;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* --- подвал: регион слева, домен справа --- */
|
||
.foot {
|
||
grid-column: 1 / -1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 32px;
|
||
padding-top: 28px;
|
||
border-top: 1px solid var(--line);
|
||
}
|
||
|
||
.region {
|
||
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
||
font-size: 17px;
|
||
letter-spacing: 0.14em;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.domain {
|
||
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
||
font-size: 24px;
|
||
font-weight: 500;
|
||
color: var(--accent-text);
|
||
}
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="card">
|
||
<div class="logo">
|
||
<div class="logoIcon"><div class="logoDot"></div></div>
|
||
<div class="logoText">
|
||
<div class="logoName">МЕРА</div>
|
||
<div class="logoSub">ДЛЯ СЕБЯ</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pitch">
|
||
<!-- Дословно h1 лэндинга (HeroV3.tsx) — превью не должно обещать
|
||
другого, чем страница, на которую ведёт ссылка. -->
|
||
<div class="headline">Реальная цена вашей квартиры — чтобы не продешевить</div>
|
||
<div class="lead">Сделки Росреестра и объявления площадок — по вашему адресу.</div>
|
||
</div>
|
||
|
||
<div class="range">
|
||
<div class="rangeLabel">ДИАПАЗОН ЦЕНЫ</div>
|
||
<div class="rangeBar">
|
||
<div class="rangeFill"></div>
|
||
<div class="rangeMark"></div>
|
||
</div>
|
||
<div class="rangeEnds"><span>ниже</span><span>выше</span></div>
|
||
<!-- Ни одного числа: любая подставленная сумма читалась бы как обещание
|
||
конкретной оценки. Показываем форму ответа, а не выдуманный ответ. -->
|
||
<div class="rangeNote">Вилка, а не одно число: рядом видно, на скольких объектах она построена.</div>
|
||
</div>
|
||
|
||
<div class="foot">
|
||
<div class="region">СВЕРДЛОВСКАЯ ОБЛАСТЬ</div>
|
||
<div class="domain">meraocenka.ru</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|