惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

IT之家
IT之家
Last Week in AI
Last Week in AI
博客园_首页
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 叶小钗
大猫的无限游戏
大猫的无限游戏
人人都是产品经理
人人都是产品经理
V
Visual Studio Blog
宝玉的分享
宝玉的分享
博客园 - Franky
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
月光博客
月光博客
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园 - 聂微东
S
SegmentFault 最新的问题
博客园 - 司徒正美
罗磊的独立博客
V
V2EX
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
美团技术团队
小众软件
小众软件
Jina AI
Jina AI

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Как JSON‑LD начинает врать — даже когда валидатор зелёный
Сергей Купряшкин · 2026-06-19 · via Все публикации подряд на Хабре

Средний

6 мин

0

На странице услуги пользователь видел цену «от 25 000 ₽», а meta description и JSON‑LD продолжали отдавать 50000. Валидатор был зелёным.

И это был собственный сайт веб‑студии!

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

Во время аудита одного из сайтов я заметил странное расхождение. На странице услуги в видимой карточке было написано: от 25 000 ₽ В теге description и JSON‑LD при этом оставалось старое значение: от 50 000 ₽ Одна страница, одна услуга, две цены.

И вот что особенно хорошо характеризует эту ошибку: это был собственный сайт веб‑студии. Компании, которая сама разрабатывает сайты и должна понимать, что карточка, description и JSON‑LD не могут жить как три независимых источника данных.

Сайт при этом работал. Страница открывалась, вёрстка не ехала, JSON‑LD разбирался. Валидатор не ругался, потому что число 50000 было записано совершенно правильно. Просто это было не то число.

Видимая карточка: от 25000 руб.
Meta description: разработка сайта от 50000 руб.
JSON-LD offers.price: 50000

Откуда взялись две цены

Сайт работал на Yii. Код ниже обезличен, но логика ошибки сохранена.

Карточка брала цену из модели:

<?php
/** @var app\models\Service $service */
$visiblePrice = $service->price_from;
?>
<div class="service-card__price">
    от <?= number_format($visiblePrice, 0, ',', ' ') ?> руб.
</div>

Meta description жил отдельно:

<?php
$this->registerMetaTag([
    'name' => 'description',
    'content' => 'Разработка сайта от 50 000 рублей',
]);

Рядом собирался JSON‑LD:

<?php
$schema = [
    '@context' => 'https://schema.org',
    '@type' => 'Service',
    'name' => $service->title,
    'offers' => [
        '@type' => 'Offer',
        'price' => 50000,
        'priceCurrency' => 'RUB',
    ],
];

Ничего экзотического. Одно значение брали из модели, два других когда‑то написали руками. Пока цена не менялась, все три версии совпадали. Потом карточку обновили, а две строки в шаблоне забыли.

Можно заменить 50000 на 25000. Через несколько месяцев повторится то же самое. Поэтому исправлять надо не число, а способ его получения.

<?php
use yii\helpers\Html;
use yii\helpers\Json;
/** @var app\models\Service $service */
$rawPrice = $service->price_from;
$priceFrom = $rawPrice !== null && (int) $rawPrice > 0 ? (int) $rawPrice : null;
$description = $service->title;
$schema = [
    '@context' => 'https://schema.org',    
    '@type' => 'Service',    
'name' => $service->title,
];
if ($priceFrom !== null) {
    $formattedPrice = number_format($priceFrom, 0, ',', ' ');
    $description .= " от {$formattedPrice} рублей";
    $schema['offers'] = [
        '@type' => 'Offer',
        'price' => $priceFrom,
        'priceCurrency' => 'RUB',
    ];
}
$this->registerMetaTag([
    'name' => 'description',
    'content' => $description,
]);
echo Html::tag('script', Json::htmlEncode($schema), [
    'type' => 'application/ld+json',
]);

Теперь цена в карточке, meta description и JSON‑LD берётся из одного поля. Если публичной цены нет, код не превращает пустое значение в ноль и не формирует offers.

А если цены нет вообще

Для услуг это обычный случай: стоимость появляется после брифа или расчёта. Тут часто ставят 0, потому что поле хочется заполнить.

'offers' => [
    '@type' => 'Offer',
    'price' => 0,
    'priceCurrency' => 'RUB',
]

Но 0 не означает «цена неизвестна». Для машины это конкретная цена — ноль рублей. В этом проекте правило простое: нет публичной цены — нет offers.

В другом проекте решение может отличаться. Выдумывать сумму ради заполненного свойства всё равно не надо.

Почему валидатор был зелёным

Потому что с JSON всё было нормально. Тип Service существовал, Offer был записан корректно, цена была числом, валюта — RUB. Валидатор не мог знать, что в карточке уже показывается другая сумма.

Это вообще разные проверки. Сначала надо убедиться, что данные можно разобрать. Потом — что они подходят под требования конкретного поисковика. И отдельно проверить, что они совпадают с самой страницей.

Последняя часть обычно и выпадает. Инструмент видит 50000, но не знает, что в интерфейсе уже стоит 25000.

В правилах Google для структурированных данных отдельно сказано, что разметка должна соответствовать содержимому страницы. Зелёная галочка этого соответствия не доказывает.

Небольшая оговорка про Service

Service подходит для описания услуги, его можно связать с Offer, указать цену и валюту. Но валидная разметка не означает, что Google обязан показать расширенный результат. У поисковика есть отдельный список поддерживаемых типов, и отдельного результата для Service там нет.

Это не делает тип бесполезным. Просто Schema.org описывает сущность, а поисковик сам решает, как использовать эти данные.

Как я проверяю страницы

Я никогда не начинаю с валидатора. Сначала открываю страницу как пользователь и смотрю, какие факты она сообщает: цену, наличие, рейтинг, адрес, автора, дату изменения. Потом ищу те же значения в meta description, Open Graph, JSON‑LD, фидах и других выгрузках.

Дальше вопрос простой: откуда взялось каждое значение? Если карточка читает service.price_from, а JSON‑LD — строку в шаблоне, ошибка уже найдена, даже когда сегодня цифры случайно совпадают.

Валидаторы я запускаю позже. Они помогают увидеть ошибки структуры и требования конкретного потребителя. Причину старой цены они не найдут.

Один тест на всякий случай

Для критичных шаблонов полезно проверить не наличие JSON‑LD, а совпадение значений. Вспомогательные методы здесь условные, чтобы не тащить в статью весь тестовый код.

public function testServicePriceMarkup(): void
{    
  $service = ServiceFactory::create([
        'title' => 'Разработка сайта',
        'price_from' => 25000,
  ]);
  $html = $this->renderServicePage($service);    
  $this->assertStringContainsString('от 25 000 руб.', $html);
  $this->assertStringContainsString(
        'Разработка сайта от 25 000 рублей',
        $this->extractMetaDescription($html)
  );
  $schema = $this->extractJsonLdByType($html, 'Service');
  $this->assertSame(25000, $schema['offers']['price']);
  $this->assertSame('RUB', $schema['offers']['priceCurrency']);
}

Для страницы без цены нужен второй сценарий: в карточке не появляется «от 0 ₽», в meta description нет выдуманной суммы, а в JSON‑LD отсутствует offers.

Как поставить задачу разработчику

«Добавить Schema.org на страницы услуг» — плохая задача. После неё разработчик начинает сам решать, откуда брать цену, что делать без цены и какие поля считать основными.

Нормальная постановка выглядит примерно так:

На страницах услуг вывести JSON‑LD типа Service. Цену брать из service.price_from, как и в карточке. Видимая цена, meta description и offers.price должны использовать одно значение. Если публичной цены нет, не выводить offers и не писать сумму вручную в description. Проверить услугу с ценой и без неё.

Такую реализацию можно отдать джуниору под ревью. Но правило, какое поле считается источником цены и что делать в пограничных случаях, должен определить не он.

Где ещё это всплывает

С ценами случай самый наглядный, но далеко не единственный. После удаления блока отзывов в JSON‑LD может остаться AggregateRating. Филиал переехал, адрес на странице поправили, а старая микроразметка продолжает жить в региональном шаблоне. У товара поменялась логика наличия, но Offer по‑прежнему отдаёт InStock.

Обычно это происходит не в день внедрения. В этот день всё проверили и сдали. Расхождения появляются потом, когда одну часть страницы меняют, а про остальные представления никто не вспоминает.

Сейчас к поисковикам и агрегаторам добавились системы генерации ответов. Суть от этого не поменялась: машинным потребителям тоже не стоит отдавать старые данные.

Вместо вывода

Микроразметка сама по себе не врёт. Врут данные, которым разрешили жить отдельно друг от друга. Интерфейс может выглядеть нормально, JSON‑LD может быть валидным, а страница всё равно будет сообщать разным читателям разные вещи.

Не чините строку. Чините правило, которое позволило строке устареть.


А у вас JSON‑LD собирается из тех же данных, что и видимая страница, или это отдельный шаблон, который проверяли только при внедрении?