Добавлена микроразметка для улучшения SEO на страницах каталога, карточки товара, о компании и контактов. Внедрены схемы Organization, Product, BreadcrumbList и LocalBusiness для соответствующих страниц. Обновлены компоненты для поддержки новых атрибутов микроразметки.
This commit is contained in:
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
{/* Обновляем кнопку купить */}
|
||||
|
20
src/components/JsonLdScript.tsx
Normal file
20
src/components/JsonLdScript.tsx
Normal 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
257
src/lib/schema.ts
Normal 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
|
||||
}
|
||||
};
|
@ -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={[
|
||||
|
@ -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}
|
||||
|
@ -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 || 'Запчасти') :
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
Reference in New Issue
Block a user