Задача: Настроить Open Graph / meta-теги для корректных превью страниц в мессенджерах

Настроить Open Graph / meta-теги для корректных превью страниц в мессенджерах

Явно задать для страниц сайта метаданные превью (заголовок, описание, изображение), чтобы мессенджеры и соцсети корректно формировали карточку ссылки.

Что нужно сделать

Добавить на страницы сайта явные метаданные для формирования превью ссылок в мессенджерах и соцсетях. В первую очередь — Open Graph (og:title, og:description, og:image, og:url, og:type), а при необходимости также Twitter Cards.

Сейчас картинка для превью явно не задана. Поэтому Telegram, WhatsApp, VK, Facebook и другие сервисы при отправке ссылки сами пытаются понять, какой заголовок, описание и изображение использовать. Они могут брать <title>, meta description, первое подходящее изображение со страницы, крупную картинку из контента или вообще сохранить ранее найденный вариант. В итоге превью может выглядеть непредсказуемо и отличаться от того, что мы хотим показать пользователю.

Почему это важно

Превью ссылки — это фактически мини-сниппет страницы внутри мессенджера. Оно влияет на то, как пользователь воспринимает ссылку до перехода: видит ли он понятный заголовок, релевантное описание и нормальную картинку товара/категории вместо случайного изображения.

Это не прямой фактор SEO-ранжирования, но относится к корректной разметке страницы и влияет на CTR, узнаваемость и качество расшаривания ссылок. Для поисковых систем и внешних сервисов также лучше, когда ключевые метаданные страницы заданы явно, а не определяются эвристически.

Что должно быть в разметке

Для каждой индексируемой страницы желательно формировать минимум:

<meta property="og:title" content="Название страницы">
<meta property="og:description" content="Краткое описание страницы">
<meta property="og:image" content="https://happybaby2000.ru/path/to/image.jpg">
<meta property="og:url" content="https://happybaby2000.ru/current-page/">
<meta property="og:type" content="website">

Для карточек товаров можно использовать og:type=product, если это соответствует текущей реализации и не создаёт проблем с поддержкой.

og:image должен содержать абсолютный публично доступный URL изображения. Для товаров желательно использовать основное фото товара, для категорий и информационных страниц — заранее определённое релевантное изображение или общий fallback.

Также проверить, чтобы изображение было доступно без авторизации, не блокировалось robots/firewall/CDN и корректно отдавалось внешним ботам.

Логика выбора изображения

Нужно определить понятный приоритет:

  1. Карточка товара — основное изображение товара.
  2. Категория — изображение категории, если оно задано.
  3. Информационная страница — отдельное изображение страницы, если задано.
  4. Если подходящего изображения нет — использовать общий fallback сайта.

Важно именно явно выводить og:image, а не рассчитывать на то, что мессенджер сам найдёт «правильную» картинку в HTML.

Как мессенджеры формируют превью

Когда пользователь впервые отправляет ссылку, сервер мессенджера обычно сам обращается к странице, считывает HTML и сохраняет найденные метаданные у себя. То есть превью часто формируется не на телефоне пользователя, а на стороне Telegram/WhatsApp/VK/другого сервиса.

Если Open Graph-разметка есть, сервис обычно ориентируется в первую очередь на неё. Если её нет или часть полей отсутствует, сервис использует собственные правила: может взять обычный <title>, description, первую крупную картинку, изображение из контента и т.д. Эти правила у разных сервисов отличаются, поэтому без явной разметки результат нельзя считать стабильным.

Отдельно про кэширование превью

Нужно учитывать, что мессенджеры кэшируют результат разбора URL. Например, если сегодня ссылка https://happybaby2000.ru/catalog/example/ уже была отправлена и мессенджер сохранил для неё старую картинку, то после замены картинки на сайте этот же URL некоторое время может продолжать показывать старое превью.

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

Поэтому после изменения og:image нельзя проверять результат только повторной отправкой того же самого URL и делать вывод, что разметка не работает.

Как проверить новое превью, обходя старый кэш

Для проверки можно добавить к URL произвольный GET-параметр, например:

https://happybaby2000.ru/catalog/example/?preview=2

или:

https://happybaby2000.ru/catalog/example/?v=20260916

Для браузера и сайта это всё та же страница, если эти параметры не используются в логике страницы. Но для мессенджера это уже другой URL, которого ещё нет в его кэше. Поэтому он с большей вероятностью заново запросит страницу и сформирует новое превью по актуальным Open Graph-тегам.

Это не «очищает» старый кэш мессенджера. Просто новый URL с другим query string создаёт отдельную запись кэша и позволяет проверить актуальную разметку, не дожидаясь истечения старого кэша.

При тестировании желательно менять параметр каждый раз, когда нужно гарантированно проверить новый вариант, например ?preview=1, затем ?preview=2.

Критерии готовности

  • На основных типах страниц явно выводятся Open Graph-теги.
  • og:title, og:description и og:image соответствуют содержимому конкретной страницы.
  • У og:image используется абсолютный URL.
  • Для страниц без собственного изображения предусмотрен fallback.
  • Проверено, что внешние боты могут получить изображение.
  • На нескольких типах страниц вручную проверено формирование превью в популярных мессенджерах.
  • При проверке изменений учтён кэш: новый вариант дополнительно тестируется через URL с уникальным GET-параметром.

Ворклоги

Усилена проверка og:image после ошибок Google

Google сообщил для части карточек ошибку «Недопустимый URL в поле image». Предполагаемая причина — исходные пути к изображениям содержали незакодированные кириллические символы и пробелы, которые прежняя проверка не выявляла.

Доработан скрипт agent/scripts/check-page-image/index.ts проекта happybaby2000.ru:

  • проверяется исходное значение og:image до какой-либо нормализации;
  • URL должен быть абсолютным HTTP(S)-адресом с хостом;
  • выявляются незакодированные пробелы, кириллица, управляющие и другие недопустимые символы;
  • проверяется корректность %-escape-последовательностей;
  • выявляются незакодированные квадратные скобки в path/query/fragment и повторный # во фрагменте, при этом IPv6 в квадратных скобках допускается;
  • проверяются все og:image на странице, а не только первый;
  • диагностика указывает номер изображения, исходный URL и причину ошибки; для недопустимых символов также выводятся Unicode-код и позиция;
  • при ошибках скрипт возвращает код 1, при успехе — 0.

Рядом добавлен постоянный тест agent/scripts/check-page-image/index.test.ts на Vitest. Конфигурация Vitest расширена на scripts/**/*.test.ts. Набор покрывает 28 сценариев, включая валидные и невалидные URL, encoded-символы, IPv6, отсутствие изображения, несколько og:image и точность диагностики.

Проверка проходит успешно: 28 тестов пройдены, ESLint, Prettier и git diff --check — без ошибок.

Команда теста:

npm run test -- scripts/check-page-image/index.test.ts

Ограничение проверки: она валидирует синтаксис URL из og:image, но не проверяет доступность самого изображения и не гарантирует принятие URL сервисами Google. Автоматического исправления URL скрипт не выполняет.

Прогресс: сначала исполнимая проверка, потом реализация

Перед внесением изменений сначала был написан отдельный терминальный TypeScript-скрипт, который делает HTTP-запрос к странице, парсит HTML через cheerio и проверяет обязательные Open Graph-теги. Это дало объективный baseline до начала правок и готовый критерий приёмки для ИИ-агента.

Проверочный скрипт

import * as cheerio from "cheerio";

const REQUIRED_TAGS = [
  "og:title",
  "og:description",
  "og:image",
  "og:url",
  "og:type",
] as const;

function isAbsoluteHttpUrl(value: string): boolean {
  try {
    const url = new URL(value);
    return url.protocol === "http:" || url.protocol === "https:";
  } catch {
    return false;
  }
}

async function main() {
  const targetUrl = process.argv[2];

  if (!targetUrl) {
    console.error("Usage: npx tsx check-open-graph.ts <url>");
    process.exit(2);
  }

  const response = await fetch(targetUrl, {
    redirect: "follow",
    headers: {
      "User-Agent": "OpenGraphChecker/1.0",
    },
  });

  if (!response.ok) {
    console.error(`HTTP ${response.status} ${response.statusText}`);
    process.exit(1);
  }

  const html = await response.text();
  const $ = cheerio.load(html);

  let hasErrors = false;

  console.log(`HTTP: ${response.status}\n`);

  const values = Object.fromEntries(
    REQUIRED_TAGS.map((property) => {
      const value =
        $(`meta[property="${property}"]`).first().attr("content")?.trim() ?? "";

      if (!value) {
        console.log(`${property}: missing`);
        hasErrors = true;
      } else {
        console.log(`${property}: ${value}`);
      }

      return [property, value];
    }),
  );

  const image = values["og:image"];
  const ogUrl = values["og:url"];

  if (image && !isAbsoluteHttpUrl(image)) {
    console.log(`✗ og:image is not absolute: ${image}`);
    hasErrors = true;
  }

  if (ogUrl && !isAbsoluteHttpUrl(ogUrl)) {
    console.log(`✗ og:url is not absolute: ${ogUrl}`);
    hasErrors = true;
  }

  console.log(
    hasErrors
      ? "\nRESULT: invalid or incomplete"
      : "\nRESULT: OK",
  );

  process.exit(hasErrors ? 1 : 0);
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

Состояние до реализации

Первый запуск дал ожидаемо отрицательный результат:

HTTP: 200

✗ og:title: missing
✗ og:description: missing
✗ og:image: missing
✗ og:url: missing
✗ og:type: missing

RESULT: invalid or incomplete

После этого задача была передана ИИ-агенту уже не в абстрактной формулировке «добавить Open Graph», а вместе с конкретным скриптом проверки и воспроизводимым способом проверить результат.

Отчёт агента

Агент сообщил о следующих изменениях:

Готово. Изменения:

index.tsx — добавлены props image и ogType, выводятся og:title, og:description, og:image, og:url, og:type
index.tsx — вычисляется imageUrl и передаётся в SeoHeaders с ogType="product"
Можешь проверить скриптом:

npx tsx scripts/check-page-image/index.ts "http://localhost:3000/catalog/product.html"

Повторная проверка

После изменений тот же проверочный подход дал:

HTTP: 200

✓ og:title: Мелкогабаритная площадка Корабль
✓ og:description: Размер 3.48 х 1.78 х 2.32 м.
✓ og:image: http://localhost:3000/images/resized/middle/images/img.jpg
✓ og:url: http://localhost:3000/catalog/product.html
✓ og:type: product

RESULT: OK

Практический вывод

Подход оказался полезным: сначала создаётся маленькая исполнимая проверка, фиксирующая текущее состояние и критерий готовности, а затем эта же проверка передаётся исполнителю вместе с задачей. Это уменьшает неоднозначность постановки и делает результат проверяемым не по отчёту агента, а по фактическому поведению системы.

Следующий шаг — усиливать этот шаблон: если агенту даётся проверочный скрипт, в постановке сразу требовать не только внести изменения, но и самостоятельно запустить этот скрипт после реализации и приложить фактический результат. Тогда цикл становится замкнутым: reproduce → implement → verify, а человеку остаётся уже контрольная, а не первичная проверка.