Разработка сайтов и интернет-магазинов Блог веб-разработчика

Сниппет modx генерации JSON микроразметки для разных страниц

Изображение сгенерированно ИИ и принадлежит qwen.ai
Изображение сгенерированно ИИ и принадлежит qwen.ai

Микроразметка schema.org для расширенных сниппетов в выдаче: хлебные крошки, логотип, контакты, статьи. Чтобы JSON-LD не прописывать для каждой страницы я написал небольшой сниппет, который генерирует всю разметку автоматически.

$context            = $modx->getOption('context', $scriptProperties, 'web');
$showOrganization   = (bool)$modx->getOption('organization', $scriptProperties, 1);
$showBreadcrumbs    = (bool)$modx->getOption('breadcrumbs', $scriptProperties, 1);
$showWebSite        = (bool)$modx->getOption('website', $scriptProperties, 1);
$explicitType       = $modx->getOption('type', $scriptProperties, '');

$resource = $modx->resource;
if (!$resource) return '';

$id          = $resource->get('id');
$pagetitle   = $resource->get('pagetitle');
$description = $resource->get('description');
$menutitle   = $resource->get('menutitle') ?: $pagetitle;
$template    = $resource->get('template');

// Собираем настройки сайта
$siteUrl = $modx->getOption('site_url', null, '', true);
$siteName = $modx->getOption('site_name', null, '');
$logo = $modx->getOption('logo', null, $siteUrl . 'images/logo.png');

$sameAs = [];
if ($socialTg) $sameAs[] = $tg;

// Определяем тип страницы по установленному ID шаблона
if (!empty($explicitType)) {
    $pageType = $explicitType;
} else {
    // Автоопределение по шаблонам (подставьте свои ID)
    $mainTemplate = 1;        // ID шаблона главной
    $contactsTemplate = 3;    // ID шаблона контактов
    $blogListTemplate = 4;    // ID шаблона списка статей
    $articleTemplate = 5;     // ID шаблона отдельной статьи
    $aboutTemplate = 2;       // ID шаблона о компании

    if ($template == $mainTemplate) {
        $pageType = 'main';
    } elseif ($template == $contactsTemplate) {
        $pageType = 'contacts';
    } elseif ($template == $blogListTemplate) {
        $pageType = 'bloglist';
    } elseif ($template == $articleTemplate) {
        $pageType = 'article';
    } elseif ($template == $aboutTemplate) {
        $pageType = 'about';
    } else {
        $pageType = 'default'; // обычная WebPage
    }
}

// Формируем граф
$graph = [];

// Organization
if ($showOrganization) {
    $orgType = ($pageType == 'main') ? 'LocalBusiness' : 'Organization';
    $organization = [
        '@type' => $orgType,
        '@id' => $siteUrl . '#organization',
        'name' => $siteName,
        'url' => $siteUrl,
        'logo' => $logo,
    ];
    if (!empty($sameAs)) {
        $organization['sameAs'] = $sameAs;
    }
    // Для главной можно добавить адрес и телефон (если есть настройки)
    if ($pageType == 'main') {
        $address = $modx->getOption('address', null, '');
        $phone = $modx->getOption('phone', null, '');
        if ($address) {
            $organization['address'] = [
                '@type' => 'PostalAddress',
                'addressLocality' => $address
            ];
        }
        if ($phone) {
            $organization['telephone'] = $phone;
        }
    }
    $graph[] = $organization;
}

// WebSite ---
if ($showWebSite) {
    $graph[] = [
        '@type' => 'WebSite',
        '@id' => $siteUrl . '#website',
        'name' => $siteName,
        'url' => $siteUrl,
        'publisher' => [
            '@id' => $siteUrl . '#organization'
        ]
    ];
}

// Основной блок страницы
$currentUrl = $modx->makeUrl($id, $context, '', 'full');

// Базовый набор свойств для всех страниц
$pageData = [
    '@id' => $currentUrl . '#webpage',
    'url' => $currentUrl,
    'name' => $pagetitle,
    'isPartOf' => [
        '@id' => $siteUrl . '#website'
    ],
    'inLanguage' => 'ru',
    'description' => $description ?: $pagetitle
];

// Определяем @type в зависимости от $pageType
switch ($pageType) {
    case 'main':
        $pageData['@type'] = 'WebPage'; // либо можно 'AboutPage', но лучше оставить WebPage
        // Можно добавить свойство mainEntity, если нужно
        break;
    case 'contacts':
        $pageData['@type'] = 'ContactPage';
        break;
    case 'about':
        $pageData['@type'] = 'AboutPage';
        break;
    case 'bloglist':
        $pageData['@type'] = 'CollectionPage';
        break;
    case 'article':
        $pageData['@type'] = 'BlogPosting'; // или Article
        // Добавляем специфичные для статьи свойства
        // Автор: можно взять из TV или из настроек
        $authorName = $modx->getOption('default_author', null, $siteName);
        $pageData['author'] = [
            '@type' => 'Person',
            'name' => $authorName
        ];
        // Дата публикации (если есть TV) или createdon
        $createdon = $resource->get('createdon');
        if ($createdon) {
            $pageData['datePublished'] = date('Y-m-d', strtotime($createdon));
        }
        $editedon = $resource->get('editedon');
        if ($editedon && $editedon != $createdon) {
            $pageData['dateModified'] = date('Y-m-d', strtotime($editedon));
        }
        // Изображение для статьи (например, TV-параметр 'image')
        $image = $modx->getOption('tv_image', null, ''); // если есть TV
        if (empty($image)) {
            // можно взять первую картинку из контента или просто использовать лого
            $image = $logo;
        }
        if ($image) {
            $pageData['image'] = $image;
        }
        break;
    default:
        $pageData['@type'] = 'WebPage';
        break;
}

// Добавляем блок страницы в граф
$graph[] = $pageData;

// BreadcrumbList кроме главной
if ($showBreadcrumbs && $pageType != 'main') {
    $parents = $modx->getParentIds($id, 10, ['context' => $context]);
    $parents = array_reverse($parents);
    $parents[] = $id;

    $items = [];
    $position = 1;
    foreach ($parents as $resId) {
        if ($resId == 0) continue;
        $res = $modx->getObject('modResource', $resId);
        if (!$res) continue;

        $title = $res->get('menutitle') ?: $res->get('pagetitle');
        $url = $modx->makeUrl($resId, $context, '', 'full');

        $items[] = [
            '@type' => 'ListItem',
            'position' => $position,
            'name' => $title,
            'item' => $url
        ];
        $position++;
    }

    if (!empty($items)) {
        $graph[] = [
            '@type' => 'BreadcrumbList',
            'itemListElement' => $items
        ];
    }
}

// Вывод
$output = [
    '@context' => 'https://schema.org',
    '@graph' => $graph
];

$json = json_encode($output, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return '';

Что делает сниппет микроразметки

Он собирает граф сущностей в формате JSON-LD и выводит его в <script type="application/ld+json">. Внутри — Organization, WebSite, текущая WebPage (или её наследники), а также BreadcrumbList. Всё это связывается через @id, как того требует современный подход разметки данных.

Тип страницы определяется по ID шаблона (задайте свои старницы и их шаблоны). Если шаблон не совпал, тогда поставится базовый WebPage. Для статей дополнительно подтягиваются даты публикации/изменения, автор и изображение (также для автора и изображения задайте свои переменные их хранения).

Быстрая настройка

  1. Создайте новый сниппет (например, schemaorg) и вставьте код.
  2. Вызовите его в шаблоне перед </head>:
  3. Пропишите свои ID шаблонов в переменных $mainTemplate, $contactsTemplate и т.д.
  4. В &type= передайти свой тип страницы (например, &type=AboutPage).

Параметры сниппета позволяют гибко управлять выводом:

Параметр Описание По умолчанию
organization Включает/выключает блок Organization 1 вкл
breadcrumbs Включает/выключает BreadcrumbList 1 вкл
website Включает/выключает блок WebSite 1 вкл
type Явно задаёт тип страницы (например AboutPage) пусто

Что получаем на выходе

Для обычной страницы (например, «Услуги») будет сгенерирован такой граф:

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://site.ru/#organization",
      "name": "Моя компания",
      "url": "https://site.ru/",
      "logo": "https://site.ru/images/logo.png"
    },
    {
      "@type": "WebSite",
      "@id": "https://site.ru/#website",
      "name": "Моя компания",
      "url": "https://site.ru/",
      "publisher": {"@id": "https://site.ru/#organization"}
    },
    {
      "@type": "WebPage",
      "@id": "https://site.ru/services/#webpage",
      "url": "https://site.ru/services/",
      "name": "Услуги",
      "isPartOf": {"@id": "https://site.ru/#website"},
      "inLanguage": "ru",
      "description": "..."
    },
    {
      "@type": "BreadcrumbList",
      "itemListElement": [...]
    }
  ]
}

Для статей тип меняется на BlogPosting и добавляются datePublished, author и image. Хлебные крошки строятся автоматически по дереву ресурсов.

Почему микроразметка важна для SEO

  • Расширенные сниппеты: хлебные крошки прямо в выдаче, логотип в поиске по бренду, информация об организации.
  • Google рекомендует именно @graph для связывания сущностей.

Пара нюансов

  • Сниппет берёт данные из системных настроек (site_url, site_name, logo, address, phone). Убедитесь, что они заполнены или используйте свои поля и укажите их в сниппете
  • Если используете кэширование, вызывайте сниппет некэшируемым () данные зависят от страницы.

Пробуйте, делитесь результатами. Валидность можно проверить в инструментах Google и Яндекс Веб-мастер. Если появятся вопросы напишите в комментариях, разберём.

Теги:

❗️ Код и технические решения в блоге — это исключительно субъективное мнение автора, предоставленное по принципу «как есть» (AS IS) в ознакомительных целях. Не является готовым продуктом или призывом к действию. Автор не гарантирует работоспособность и безопасность кода в вашем окружении. Вы обязаны самостоятельно тестировать решения перед использованием в продакшене. Автор снимает с себя любую ответственность за потерю данных, сбои систем, уязвимости, финансовые убытки или любые другие последствия, возникшие из-за применения материалов блога.

Обсудим вашу задачу и рассчитаем стоимость?

Отвечаю лично в течение пары часов. Никакого спама и автоматических рассылок.