feat: dynamic SEO metadata and JSON-LD for products

This commit is contained in:
پارسا آقایی 2026-06-12 01:26:32 +03:30
parent bf1b11d6a9
commit 700f93e418
2 changed files with 80 additions and 3 deletions

View File

@ -1,6 +1,78 @@
import ProductPage from "../../../components/ProductPage";
import { productService } from "../../../lib/services/productService";
import type { Metadata, ResolvingMetadata } from 'next';
export async function generateMetadata(
{ params }: { params: Promise<{ id: string }> },
parent: ResolvingMetadata
): Promise<Metadata> {
const resolvedParams = await params;
const product = await productService.getProductById(resolvedParams.id);
if (!product) {
return {
title: 'محصول یافت نشد',
};
}
// Get raw data for custom fields if we added them (like metaTitle)
// Since we don't have local access to raw DB object without modifying productService,
// we can just use the mapped product fields for now.
const title = (product as any).metaTitle || product.name;
const description = (product as any).metaDescription || product.description;
const keywords = (product as any).keywords ? (product as any).keywords.split(',') : product.main_ingredients;
return {
title,
description,
keywords,
openGraph: {
title,
description,
images: [product.image],
},
alternates: {
canonical: (product as any).canonicalUrl || `/shop/${product.id}`,
}
};
}
export default async function ShopProductPage({ params }: { params: Promise<{ id: string }> }) {
const resolvedParams = await params;
const product = await productService.getProductById(resolvedParams.id);
if (!product) {
return <ProductPage productId={resolvedParams.id} />;
}
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
image: product.image,
description: (product as any).metaDescription || product.description,
sku: product.artNo,
brand: {
'@type': 'Brand',
name: 'Canina'
},
offers: {
'@type': 'Offer',
url: `https://canina-iran.com/shop/${product.id}`,
priceCurrency: 'IRR',
price: product.priceValue * 10, // Assuming priceValue is in Toman, converting to Rial
availability: 'https://schema.org/InStock',
itemCondition: 'https://schema.org/NewCondition'
}
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<ProductPage productId={resolvedParams.id} />
</>
);
}

View File

@ -1,7 +1,9 @@
import axios from 'axios';
const baseURL = typeof window !== 'undefined' ? '/api' : (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api');
const api = axios.create({
baseURL: '/api',
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
@ -11,7 +13,10 @@ const api = axios.create({
// Interceptor to add auth token in the future
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken');
let token = null;
if (typeof window !== 'undefined') {
token = localStorage.getItem('accessToken');
}
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}