Алексей Баранов

Обо мне

Cover Image for Добавляем JSON-LD разметку к блогу на Next.js

Добавляем JSON-LD разметку к блогу на Next.js

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

Пришло время ещё больше улучшить выдачу, а также поработать над выдачей в других поисковиках. А именно добавить на страницы разметку JSON-LD.

Что такое JSON-LD?

JSON-LD (JSON Lightweight Linked Data format) — это формат метаданных для поисковых систем о типе контента на каждой странице. В теории, наличие подобной разметки на сайте приводит к более высоким результатам в поисковой выдаче.

JSON-LD выглядит так:

{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Добавляем JSON-LD разметку к блогу на Next.js",
  "description": "Инструкция по добавлению JSON-LD разметки к блогу на Next.js",
  "datePublished": "2024-05-18",
  "genre": "Technology",
  "author": {
    "@type": "Person",
    "name": "Алексей Баранов",
    "url": "https://alexeybaranov.dev"
  },
  "image": "https://alexeybaranov.dev/assets/blog/nextjs-json-ld/cover.png"
}

Для чего нужен JSON-LD?

В поисковой выдаче Яндекса есть, как минимум, 2 элемента, которые напрямую зависят от JSON-LD:

  • Навигационные цепочки - они же хлебные крошки; Навигационные цепочки
  • Сниппет Вопрос-ответ; Вопрос-ответ

С поисковой выдачей Google всё ещё интереснее. Они называют такую разметку Структурированными данными.

В документации для разработчиков есть целый раздел посвящённый Структурированным данным и JSON-LD.

От Структурированных данных зависят такие функции поисковой выдачи как:

  • Статья - название говорит само за себя;
  • Строка навигации - аналог Навигационных цепочек Яндекса;
  • Карусель - аналог Турбо карусели Яндекса;
  • Часто задаваемые вопросы - аналог Вопрос-ответ от Яндекса;
  • Товары - описание товаров и их характеристик;
  • Видео - описание и основные моменты видео;
  • Организация - название, часы работы, телефоны и прочее;
  • Мероприятие - где и когда состоится;

и многие другие (я насчитал 36 штук).

Для начала мне хватит просто Навигационных цепочек и теоретического улучшения позиций в выдаче 🙂

Итак, для добавления JSON-LD нам нужно:

Ссылка на конечный результат.

Формирование JSON-LD

Как не трудно догадаться из названия, JSON-LD - это JSON объект, составленный по определённой схеме. Для валидации схемы уже есть npm пакет schema-dts.

Установим его:

npm i schema-dts

Далее создадим объект для поста этого блога.

import { BlogPosting, WithContext } from "schema-dts";

export const URL_BASE = "https://alexeybaranov.dev";
export const HOME_OG_IMAGE_URL = "https://alexeybaranov.dev/logo.png";

const blogPosting: WithContext<BlogPosting> = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  name: post.title,
  headline: post.title,
  description: post.description,
  datePublished: new Date(post.date).toISOString(),
  genre: "Technology",
  author: {
    "@type": "Person",
    name: post.author.name,
    url: `${URL_BASE}/about`,
  },
  publisher: {
    "@type": "Organization",
    name: "Алексей Баранов. Блог",
    logo: {
      "@type": "ImageObject",
      url: HOME_OG_IMAGE_URL,
    },
  },
  image: [`${URL_BASE}${post.ogImage.url}`],
  mainEntityOfPage: {
    "@type": "WebPage",
    "@id": `${URL_BASE}/posts/${post.slug}`,
  },
  inLanguage: "ru-RU",
};

Теперь необходимо сформировать BreadcrumbList для Навигационных цепочек:

import { BreadcrumbList, WithContext } from "schema-dts";

export const URL_BASE = "https://alexeybaranov.dev";

const breadcrumbList: WithContext<BreadcrumbList> = {
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  itemListElement: [
    {
      "@type": "ListItem",
      position: 1,
      name: "Посты",
      item: `${URL_BASE}/posts`,
    },
    {
      "@type": "ListItem",
      position: 2,
      name: post.title,
    },
  ],
};

Ну и на сладкое мы можем добавить информацию о видео содержащихся на странице:

import { VideoObject, WithContext } from "schema-dts";

const videoStructuredData: WithContext<VideoObject> = {
  "@context": "https://schema.org",
  "@type": "VideoObject",
  name: post.title,
  description: post.description,
  thumbnailUrl: `https://i.ytimg.com/vi/${post.youtubeId}/hqdefault.jpg`, // Миниатюра видео
  uploadDate: new Date(post.date).toISOString(),
  contentUrl: `https://www.youtube.com/watch?v=${post.youtubeId}`, // Ссылка на видео на Youtube
  embedUrl: `https://www.youtube.com/embed/${post.youtubeId}`, // Ссылка на встроенное видео с Youtube
};

Объединяем всё это вместе в один объект:

const jsonLd = [blogPosting, breadcrumbList, videoStructuredData];

Добавление на страницу

Для того чтобы добавить разметку на страницу, создадим компонент JsonLd:

import React from "react";

type Props = {
  data: unknown;
};

const JsonLd: React.FC<Props> = ({ data }) => (
  <script
    type="application/ld+json"
    dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
  />
);

export default JsonLd;

Далее просто добавляем этот компонент на страницу с постом и передаём в него данные:

const jsonLd = [blogPosting, breadcrumbList, videoStructuredData];

<JsonLd data={jsonLd} />

Теперь необходимо проверить что у нас всё получилось как надо.

Тестирование

Заходим на страницу и смотрим разметку:

Разметка страницы

Итак, разметка появилась на странице.

Теперь необходимо проверить что разметка валидная. Для этого у гугла есть специальный инструмент для проверки.

Инструмент для проверки

Вбиваем в него адрес нашей страницы, если она уже где-то хостится, либо HTML-разметку страницы.

Результат проверки

Отлично, на этом всё! 🎉

Теперь выкладываем страницу и ждём индексации.

Готовое решение

Прикладываю полный код решения.

// types.ts
export type Author = {
  name: string;
  picture: string;
};

export type Post = {
  slug: string;
  title: string;
  description: string;
  date: string;
  coverImage: string;
  author: Author;
  excerpt: string;
  ogImage: {
    url: string;
  };
  keywords: string[];
  content: string;
  preview?: boolean;
  youtubeId?: string;
};
// src/lib/jsonLd.ts
import {
  BlogPosting,
  BreadcrumbList,
  VideoObject,
  WithContext,
} from "schema-dts";

import { Post } from "@/interfaces/post";

import { HOME_OG_IMAGE_URL, URL_BASE } from "./constants";

export const getPostJsonLd = (post: Post) => {
  const blogPosting: WithContext<BlogPosting> = {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    name: post.title,
    headline: post.title,
    description: post.description,
    datePublished: new Date(post.date).toISOString(),
    genre: "Technology",
    author: {
      "@type": "Person",
      name: post.author.name,
      url: `${URL_BASE}/about`,
    },
    publisher: {
      "@type": "Organization",
      name: "Алексей Баранов. Блог",
      logo: {
        "@type": "ImageObject",
        url: HOME_OG_IMAGE_URL,
      },
    },
    image: [`${URL_BASE}${post.ogImage.url}`],
    mainEntityOfPage: {
      "@type": "WebPage",
      "@id": `${URL_BASE}/posts/${post.slug}`,
    },
    inLanguage: "ru-RU",
  };

  const breadcrumbList: WithContext<BreadcrumbList> = {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    itemListElement: [
      {
        "@type": "ListItem",
        position: 1,
        name: "Посты",
        item: `${URL_BASE}/posts`,
      },
      {
        "@type": "ListItem",
        position: 2,
        name: post.title,
      },
    ],
  };

  if (post.youtubeId) {
    const videoStructuredData: WithContext<VideoObject> = {
      "@context": "https://schema.org",
      "@type": "VideoObject",
      name: post.title,
      description: post.description,
      thumbnailUrl: `https://i.ytimg.com/vi/${post.youtubeId}/hqdefault.jpg`, // this is the thumbnail for the video straight from youtube
      uploadDate: new Date(post.date).toISOString(),
      contentUrl: `https://www.youtube.com/watch?v=${post.youtubeId}`, // this is the URL for the video on youtube
      embedUrl: `https://www.youtube.com/embed/${post.youtubeId}`, // this is the URL for the video embed on youtube
    };
    return [blogPosting, breadcrumbList, videoStructuredData];
  }

  return [blogPosting, breadcrumbList];
};
// src/app/posts/[slug]/page.ts
export default async function Post({ params }: Params) {
  const jsonLd = getPostJsonLd(post);

  return (
    <>
      <JsonLd data={jsonLd} />
      ...
      </>
  );
}

Так же не забудьте подписаться на мой YouTube канал и Telegram 🙂

Поделиться

Вам может быть интересно:

Cover Image for Добавляем Яндекс Турбо-страницы к блогу на Next.js

Добавляем Яндекс Турбо-страницы к блогу на Next.js

Для того чтобы красиво отображаться в поисковой выдаче Яндекса решил добавить Яндекс Турбо-страницы к блогу...

Cover Image for Подключение счётчика Яндекс Метрики к Next.js приложению

Подключение счётчика Яндекс Метрики к Next.js приложению

Рассказ о том как я счётчик Яндекс Метрики к блогу подключал...