Добавлена микроразметка для улучшения SEO на страницах каталога, карточки товара, о компании и контактов. Внедрены схемы Organization, Product, BreadcrumbList и LocalBusiness для соответствующих страниц. Обновлены компоненты для поддержки новых атрибутов микроразметки.

This commit is contained in:
Bivekich
2025-07-06 18:46:00 +03:00
parent 2b5f787fbe
commit 8284385e3c
9 changed files with 399 additions and 10 deletions

View File

@ -26,17 +26,35 @@ const CatalogInfoHeader: React.FC<CatalogInfoHeaderProps> = ({
<div className="w-layout-blockcontainer container info w-container">
<div className="w-layout-vflex flex-block-9">
{breadcrumbs && breadcrumbs.length > 0 && (
<div className="w-layout-hflex flex-block-7">
<div
className="w-layout-hflex flex-block-7"
itemScope
itemType="https://schema.org/BreadcrumbList"
>
{breadcrumbs.map((bc, idx) => (
<React.Fragment key={idx}>
{idx > 0 && <div className="text-block-3"></div>}
{bc.href ? (
<a href={bc.href} className="link-block w-inline-block">
<div>{bc.label}</div>
<a
href={bc.href}
className="link-block w-inline-block"
itemProp="itemListElement"
itemScope
itemType="https://schema.org/ListItem"
>
<div itemProp="name">{bc.label}</div>
<meta itemProp="position" content={String(idx + 1)} />
<meta itemProp="item" content={bc.href} />
</a>
) : (
<span className="link-block-2 w-inline-block">
<div>{bc.label}</div>
<span
className="link-block-2 w-inline-block"
itemProp="itemListElement"
itemScope
itemType="https://schema.org/ListItem"
>
<div itemProp="name">{bc.label}</div>
<meta itemProp="position" content={String(idx + 1)} />
</span>
)}
</React.Fragment>

View File

@ -95,7 +95,12 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
};
return (
<div className="w-layout-vflex flex-block-15-copy" data-article-card="visible">
<div
className="w-layout-vflex flex-block-15-copy"
data-article-card="visible"
itemScope
itemType="https://schema.org/Product"
>
<div
className={`favcardcat ${isItemFavorite ? 'favorite-active' : ''}`}
onClick={handleFavoriteClick}
@ -113,7 +118,15 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
{/* Делаем картинку и контент кликабельными для перехода на card */}
<Link href={cardUrl} className="div-block-4" style={{ textDecoration: 'none', color: 'inherit' }}>
<img src={displayImage} loading="lazy" width="Auto" height="Auto" alt="" className="image-5" />
<img
src={displayImage}
loading="lazy"
width="Auto"
height="Auto"
alt={title}
className="image-5"
itemProp="image"
/>
<div className="text-block-7">{discount}</div>
</Link>
@ -122,12 +135,18 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
{priceElement ? (
<div className="text-block-8">{priceElement}</div>
) : (
<div className="text-block-8">{price}</div>
<div className="text-block-8" itemProp="offers" itemScope itemType="https://schema.org/Offer">
<span itemProp="price">{price}</span>
<meta itemProp="priceCurrency" content={currency} />
</div>
)}
<div className="text-block-9">{oldPrice}</div>
</div>
<div className="text-block-10">{title}</div>
<div className="text-block-11">{brand}</div>
<div className="text-block-10" itemProp="name">{title}</div>
<div className="text-block-11" itemProp="brand" itemScope itemType="https://schema.org/Brand">
<span itemProp="name">{brand}</span>
</div>
<meta itemProp="sku" content={articleNumber || ''} />
</Link>
{/* Обновляем кнопку купить */}

View File

@ -0,0 +1,20 @@
import React from 'react';
import { generateJsonLdScript } from '@/lib/schema';
interface JsonLdScriptProps {
schema: object;
}
// Компонент для вставки JSON-LD разметки
const JsonLdScript: React.FC<JsonLdScriptProps> = ({ schema }) => {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: generateJsonLdScript(schema)
}}
/>
);
};
export default JsonLdScript;

257
src/lib/schema.ts Normal file
View File

@ -0,0 +1,257 @@
// Утилиты для генерации микроразметки schema.org
export interface SchemaOrgProduct {
name: string;
description?: string;
brand: string;
sku: string;
image?: string;
category?: string;
offers: SchemaOrgOffer[];
}
export interface SchemaOrgOffer {
price: number;
currency: string;
availability: string;
seller: string;
deliveryTime?: string;
warehouse?: string;
}
export interface SchemaOrgBreadcrumb {
name: string;
url: string;
}
export interface SchemaOrgOrganization {
name: string;
description?: string;
url: string;
logo?: string;
contactPoint?: {
telephone: string;
email?: string;
contactType: string;
};
address?: {
streetAddress: string;
addressLocality: string;
addressRegion: string;
postalCode: string;
addressCountry: string;
};
}
export interface SchemaOrgLocalBusiness extends SchemaOrgOrganization {
openingHours?: string[];
geo?: {
latitude: number;
longitude: number;
};
}
// Генератор микроразметки для товара
export const generateProductSchema = (product: SchemaOrgProduct): object => {
return {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
description: product.description || `${product.brand} ${product.sku} - ${product.name}`,
brand: {
"@type": "Brand",
name: product.brand
},
sku: product.sku,
mpn: product.sku,
image: product.image,
category: product.category || "Автозапчасти",
offers: product.offers.map(offer => ({
"@type": "Offer",
price: offer.price,
priceCurrency: offer.currency,
availability: offer.availability,
seller: {
"@type": "Organization",
name: offer.seller
},
deliveryLeadTime: offer.deliveryTime,
availableAtOrFrom: {
"@type": "Place",
name: offer.warehouse || "Склад"
}
}))
};
};
// Генератор микроразметки для организации
export const generateOrganizationSchema = (org: SchemaOrgOrganization): object => {
const schema: any = {
"@context": "https://schema.org",
"@type": "Organization",
name: org.name,
url: org.url,
description: org.description
};
if (org.logo) {
schema.logo = org.logo;
}
if (org.contactPoint) {
schema.contactPoint = {
"@type": "ContactPoint",
telephone: org.contactPoint.telephone,
email: org.contactPoint.email,
contactType: org.contactPoint.contactType
};
}
if (org.address) {
schema.address = {
"@type": "PostalAddress",
streetAddress: org.address.streetAddress,
addressLocality: org.address.addressLocality,
addressRegion: org.address.addressRegion,
postalCode: org.address.postalCode,
addressCountry: org.address.addressCountry
};
}
return schema;
};
// Генератор микроразметки для местного бизнеса
export const generateLocalBusinessSchema = (business: SchemaOrgLocalBusiness): object => {
const schema: any = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
name: business.name,
url: business.url,
description: business.description
};
if (business.contactPoint) {
schema.contactPoint = {
"@type": "ContactPoint",
telephone: business.contactPoint.telephone,
email: business.contactPoint.email,
contactType: business.contactPoint.contactType
};
}
if (business.address) {
schema.address = {
"@type": "PostalAddress",
streetAddress: business.address.streetAddress,
addressLocality: business.address.addressLocality,
addressRegion: business.address.addressRegion,
postalCode: business.address.postalCode,
addressCountry: business.address.addressCountry
};
}
if (business.openingHours) {
schema.openingHours = business.openingHours;
}
if (business.geo) {
schema.geo = {
"@type": "GeoCoordinates",
latitude: business.geo.latitude,
longitude: business.geo.longitude
};
}
return schema;
};
// Генератор микроразметки для хлебных крошек
export const generateBreadcrumbSchema = (breadcrumbs: SchemaOrgBreadcrumb[]): object => {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: breadcrumbs.map((breadcrumb, index) => ({
"@type": "ListItem",
position: index + 1,
name: breadcrumb.name,
item: breadcrumb.url
}))
};
};
// Генератор микроразметки для сайта с поиском
export const generateWebSiteSchema = (name: string, url: string, searchUrl?: string): object => {
const schema: any = {
"@context": "https://schema.org",
"@type": "WebSite",
name: name,
url: url
};
if (searchUrl) {
schema.potentialAction = {
"@type": "SearchAction",
target: {
"@type": "EntryPoint",
urlTemplate: `${searchUrl}?q={search_term_string}`
},
"query-input": "required name=search_term_string"
};
}
return schema;
};
// Утилита для конвертации доступности товара в schema.org формат
export const convertAvailability = (stock: string | number): string => {
const stockNum = typeof stock === 'string' ? parseInt(stock) || 0 : stock;
if (stockNum > 0) {
return "https://schema.org/InStock";
} else {
return "https://schema.org/OutOfStock";
}
};
// Утилита для генерации JSON-LD скрипта
export const generateJsonLdScript = (schema: object): string => {
return JSON.stringify(schema, null, 2);
};
// Интерфейс для компонента JSON-LD (компонент будет в отдельном файле)
export interface JsonLdScriptProps {
schema: object;
}
// Данные организации Protek
export const PROTEK_ORGANIZATION: SchemaOrgOrganization = {
name: "Protek",
description: "Protek - широкий ассортимент автозапчастей и аксессуаров для всех марок автомобилей. Быстрая доставка по России, гарантия качества, низкие цены.",
url: "https://protek.ru",
logo: "https://protek.ru/images/logo.svg",
contactPoint: {
telephone: "+7-800-555-0123",
email: "info@protek.ru",
contactType: "customer service"
},
address: {
streetAddress: "ул. Примерная, 123",
addressLocality: "Москва",
addressRegion: "Москва",
postalCode: "123456",
addressCountry: "RU"
}
};
// Данные для LocalBusiness
export const PROTEK_LOCAL_BUSINESS: SchemaOrgLocalBusiness = {
...PROTEK_ORGANIZATION,
openingHours: [
"Mo-Fr 09:00-18:00",
"Sa 10:00-16:00"
],
geo: {
latitude: 55.7558,
longitude: 37.6176
}
};

View File

@ -10,13 +10,26 @@ import AboutProtekInfo from "@/components/about/AboutProtekInfo";
import AboutHelp from "@/components/about/AboutHelp";
import MetaTags from "@/components/MetaTags";
import { getMetaByPath } from "@/lib/meta-config";
import JsonLdScript from "@/components/JsonLdScript";
import { generateOrganizationSchema, generateBreadcrumbSchema, PROTEK_ORGANIZATION } from "@/lib/schema";
export default function About() {
const metaData = getMetaByPath('/about');
// Генерируем микроразметку Organization для страницы "О компании"
const organizationSchema = generateOrganizationSchema(PROTEK_ORGANIZATION);
// Генерируем микроразметку BreadcrumbList
const breadcrumbSchema = generateBreadcrumbSchema([
{ name: "Главная", url: "https://protek.ru/" },
{ name: "О компании", url: "https://protek.ru/about" }
]);
return (
<>
<MetaTags {...metaData} />
<JsonLdScript schema={organizationSchema} />
<JsonLdScript schema={breadcrumbSchema} />
<CatalogInfoHeader
title="О компании"
breadcrumbs={[

View File

@ -1,5 +1,7 @@
import MetaTags from "../components/MetaTags";
import { getMetaByPath, createProductMeta } from "../lib/meta-config";
import JsonLdScript from "@/components/JsonLdScript";
import { generateProductSchema, convertAvailability, type SchemaOrgProduct } from "@/lib/schema";
import { useRouter } from "next/router";
import { useEffect, useState, useMemo } from "react";
import { useQuery, useLazyQuery } from "@apollo/client";
@ -199,6 +201,30 @@ export default function CardPage() {
price: allOffers.length > 0 ? Math.min(...allOffers.map(offer => offer.sortPrice)) : undefined
}) : getMetaByPath('/card');
// Генерируем микроразметку Product
const productSchema = useMemo(() => {
if (!result || allOffers.length === 0) return null;
const schemaProduct: SchemaOrgProduct = {
name: result.name,
description: `${result.brand} ${result.articleNumber} - ${result.name}`,
brand: result.brand,
sku: result.articleNumber,
image: mainImageUrl || (result?.partsIndexImages && result.partsIndexImages.length > 0 ? result.partsIndexImages[0].url : undefined),
category: "Автозапчасти",
offers: allOffers.map(offer => ({
price: offer.sortPrice,
currency: "RUB",
availability: convertAvailability(offer.quantity || 0),
seller: offer.type === 'internal' ? 'Protek' : 'AutoEuro',
deliveryTime: offer.deliveryTime ? `${offer.deliveryTime} дней` : undefined,
warehouse: offer.warehouse || 'Склад'
}))
};
return generateProductSchema(schemaProduct);
}, [result, allOffers, mainImageUrl]);
if (loading) {
return (
<>
@ -226,6 +252,7 @@ export default function CardPage() {
ogTitle={metaConfig.ogTitle}
ogDescription={metaConfig.ogDescription}
/>
{productSchema && <JsonLdScript schema={productSchema} />}
<InfoCard
brand={result ? result.brand : brandQuery}
articleNumber={result ? result.articleNumber : searchQuery}

View File

@ -26,6 +26,8 @@ import toast from 'react-hot-toast';
import CartIcon from '@/components/CartIcon';
import MetaTags from "@/components/MetaTags";
import { getMetaByPath, createCategoryMeta } from "@/lib/meta-config";
import JsonLdScript from "@/components/JsonLdScript";
import { generateBreadcrumbSchema, generateWebSiteSchema } from "@/lib/schema";
const mockData = Array(12).fill({
image: "",
@ -512,9 +514,24 @@ export default function Catalog() {
const categoryNameDecoded = decodeURIComponent(categoryName as string || 'Каталог');
const metaData = createCategoryMeta(categoryNameDecoded, visibleProductsCount || undefined);
// Генерируем микроразметку для каталога
const breadcrumbSchema = generateBreadcrumbSchema([
{ name: "Главная", url: "https://protek.ru/" },
{ name: "Каталог", url: "https://protek.ru/catalog" },
...(categoryName ? [{ name: categoryNameDecoded, url: `https://protek.ru/catalog?categoryName=${categoryName}` }] : [])
]);
const websiteSchema = generateWebSiteSchema(
"Protek - Каталог автозапчастей",
"https://protek.ru",
"https://protek.ru/search"
);
return (
<>
<MetaTags {...metaData} />
<JsonLdScript schema={breadcrumbSchema} />
<JsonLdScript schema={websiteSchema} />
<CatalogInfoHeader
title={
isPartsAPIMode ? decodeURIComponent(categoryName as string || 'Запчасти') :

View File

@ -10,13 +10,19 @@ import OrderContacts from "@/components/contacts/OrderContacts";
import LegalContacts from "@/components/contacts/LegalContacts";
import MetaTags from "@/components/MetaTags";
import { getMetaByPath } from "@/lib/meta-config";
import JsonLdScript from "@/components/JsonLdScript";
import { generateLocalBusinessSchema, PROTEK_LOCAL_BUSINESS } from "@/lib/schema";
const Contacts = () => {
const metaData = getMetaByPath('/contacts');
// Генерируем микроразметку LocalBusiness для страницы контактов
const localBusinessSchema = generateLocalBusinessSchema(PROTEK_LOCAL_BUSINESS);
return (
<>
<MetaTags {...metaData} />
<JsonLdScript schema={localBusinessSchema} />
<InfoContacts />
<section className="main">
<div className="w-layout-blockcontainer container w-container">

View File

@ -13,6 +13,8 @@ import NewsAndPromos from "@/components/index/NewsAndPromos";
import AboutHelp from "@/components/about/AboutHelp";
import MetaTags from "@/components/MetaTags";
import { getMetaByPath } from "@/lib/meta-config";
import JsonLdScript from "@/components/JsonLdScript";
import { generateOrganizationSchema, generateWebSiteSchema, PROTEK_ORGANIZATION } from "@/lib/schema";
const geistSans = Geist({
variable: "--font-geist-sans",
@ -27,9 +29,19 @@ const geistMono = Geist_Mono({
export default function Home() {
const metaData = getMetaByPath('/');
// Генерируем микроразметку для главной страницы
const organizationSchema = generateOrganizationSchema(PROTEK_ORGANIZATION);
const websiteSchema = generateWebSiteSchema(
"Protek - Автозапчасти и аксессуары",
"https://protek.ru",
"https://protek.ru/search"
);
return (
<>
<MetaTags {...metaData} />
<JsonLdScript schema={organizationSchema} />
<JsonLdScript schema={websiteSchema} />
<HeroSlider />
<CatalogSection />
<div className="w-layout-blockcontainer container w-container">