
Алексей Баранов
Добавляем 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.webp"
}
Для чего нужен 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.webp";
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,
item: `${URL_BASE}/posts/${post.slug}`,
},
],
};
Ну и на сладкое мы можем добавить информацию о видео содержащихся на странице:
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 🙂



