- Add 4-locale keyword configurations (SR, EN, DE, FR) - Create schema generators (Product, Organization, Breadcrumb) - Add React components for JSON-LD rendering - Implement caching for keyword performance - Abstract all SEO logic for maintainability
105 lines
2.8 KiB
TypeScript
105 lines
2.8 KiB
TypeScript
import { ProductSchema } from './types';
|
|
import { Locale } from '../keywords/types';
|
|
import { getBrandKeywords, getCategoryKeywords } from '../keywords';
|
|
|
|
interface ProductData {
|
|
name: string;
|
|
slug: string;
|
|
description: string;
|
|
images: string[];
|
|
price: {
|
|
amount: number;
|
|
currency: string;
|
|
};
|
|
sku?: string;
|
|
availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
|
|
category?: string;
|
|
rating?: {
|
|
value: number;
|
|
count: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate Product schema (JSON-LD)
|
|
* Pure function - takes product data, returns schema object
|
|
*
|
|
* @param baseUrl - Site base URL
|
|
* @param locale - Locale code
|
|
* @param product - Product data
|
|
* @returns ProductSchema object
|
|
*/
|
|
export function generateProductSchema(
|
|
baseUrl: string,
|
|
locale: Locale,
|
|
product: ProductData
|
|
): ProductSchema {
|
|
const brandKeywords = getBrandKeywords(locale);
|
|
const productUrl = `${baseUrl}/${locale === 'sr' ? '' : locale + '/'}products/${product.slug}`;
|
|
|
|
// Build full image URLs
|
|
const imageUrls = product.images.map(img =>
|
|
img.startsWith('http') ? img : `${baseUrl}${img}`
|
|
);
|
|
|
|
const schema: ProductSchema = {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Product',
|
|
name: product.name,
|
|
image: imageUrls,
|
|
description: product.description.slice(0, 5000), // Schema.org limit
|
|
sku: product.sku,
|
|
brand: {
|
|
'@type': 'Brand',
|
|
name: brandKeywords.companyName,
|
|
},
|
|
offers: {
|
|
'@type': 'Offer',
|
|
url: productUrl,
|
|
price: product.price.amount.toString(),
|
|
priceCurrency: product.price.currency,
|
|
availability: `https://schema.org/${product.availability || 'InStock'}`,
|
|
itemCondition: 'https://schema.org/NewCondition',
|
|
},
|
|
};
|
|
|
|
// Add aggregate rating if available
|
|
if (product.rating && product.rating.count > 0) {
|
|
schema.aggregateRating = {
|
|
'@type': 'AggregateRating',
|
|
ratingValue: product.rating.value.toString(),
|
|
reviewCount: product.rating.count.toString(),
|
|
};
|
|
}
|
|
|
|
return schema;
|
|
}
|
|
|
|
/**
|
|
* Generate Product schema with category context
|
|
* Uses category-specific keywords for enhanced SEO
|
|
*
|
|
* @param baseUrl - Site base URL
|
|
* @param locale - Locale code
|
|
* @param product - Product data
|
|
* @param categoryKey - Category key for keyword targeting
|
|
* @returns ProductSchema object
|
|
*/
|
|
export function generateCategorizedProductSchema(
|
|
baseUrl: string,
|
|
locale: Locale,
|
|
product: ProductData,
|
|
categoryKey: 'antiAging' | 'hydration' | 'glow' | 'sensitive' | 'natural' | 'organic'
|
|
): ProductSchema {
|
|
const categoryKeywords = getCategoryKeywords(locale, categoryKey);
|
|
|
|
// Enhance description with category keywords
|
|
const enhancedDescription = product.description +
|
|
' ' + categoryKeywords.slice(0, 3).join('. ');
|
|
|
|
return generateProductSchema(baseUrl, locale, {
|
|
...product,
|
|
description: enhancedDescription,
|
|
});
|
|
}
|