Add complete SEO content for all 36 product pages

- Add keywords, seoContent, specifications, features, FAQ, and Schema.org data
- Extend types for ProductSpecification and FAQItem
- Update product page to render SEO sections with structured data
- All content in Thai for Thai market SEO optimization
This commit is contained in:
Kunthawat Greethong
2026-02-28 14:49:18 +07:00
parent 1d43a583cd
commit 3908ddc765
3 changed files with 1785 additions and 138 deletions

View File

@@ -2,7 +2,7 @@ import { notFound } from 'next/navigation';
import Image from 'next/image';
import Link from 'next/link';
import { productCategories, portfolioProjects } from '@/data/site-config';
import type { ProductCategory, FAQItem } from '@/types';
interface Props {
params: { slug: string[] };
@@ -30,7 +30,7 @@ export async function generateStaticParams() {
type ContentType = 'product' | 'portfolio';
function findContentBySlug(slug: string[]): { type: ContentType; data: typeof productCategories[0] | typeof portfolioProjects[0] } | null {
function findContentBySlug(slug: string[]): { type: ContentType; data: ProductCategory | typeof portfolioProjects[0] } | null {
// Decode URL-encoded slug parts
const decodedSlug = slug.map(part => decodeURIComponent(part));
const fullPath = '/' + decodedSlug.join('/') + '/';
@@ -60,11 +60,21 @@ export async function generateMetadata({ params }: Props) {
const { type, data } = content;
if (type === 'product') {
const product = data as typeof productCategories[0];
const product = data as ProductCategory;
const title = product.keywords?.[0]
? `${product.name} | ${product.keywords[0]} - ดีลพลัสเทค`
: `${product.name} - ${product.nameEn} | ดีลพลัสเทค`;
return {
title: `${product.name} - ${product.nameEn}`,
title,
description: product.description,
keywords: product.keywords,
keywords: product.keywords?.join(', '),
openGraph: {
title: product.name,
description: product.description,
images: [product.image],
type: 'website',
},
};
}
@@ -74,6 +84,68 @@ export async function generateMetadata({ params }: Props) {
};
}
// Schema.org JSON-LD for Products
function ProductSchema({ product }: { product: ProductCategory }) {
const schema = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.image,
brand: {
'@type': 'Brand',
name: product.schemaData?.brand || product.nameEn,
},
manufacturer: {
'@type': 'Organization',
name: product.schemaData?.manufacturer || 'ดีลพลัสเทค',
},
...(product.schemaData?.sku && { sku: product.schemaData.sku }),
...(product.schemaData?.mpn && { mpn: product.schemaData.mpn }),
...(product.schemaData?.material && { material: product.schemaData.material }),
category: product.schemaData?.category || 'Industrial Pipe & Equipment',
offers: {
'@type': 'Offer',
availability: 'https://schema.org/InStock',
priceCurrency: 'THB',
seller: {
'@type': 'Organization',
name: 'ดีลพลัสเทค',
},
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// FAQ Schema for SEO
function FAQSchema({ faq }: { faq: FAQItem[] }) {
const schema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faq.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
export default function DynamicPage({ params }: Props) {
const content = findContentBySlug(params.slug);
@@ -89,111 +161,264 @@ export default function DynamicPage({ params }: Props) {
}
// Render product page
return <ProductPage product={data as typeof productCategories[0]} />;
return <ProductPage product={data as ProductCategory} />;
}
// Product Page Component
function ProductPage({ product }: { product: typeof productCategories[0] }) {
// Find related products in same category
const relatedProducts = productCategories
.filter((p) => p.slug === product.slug && p.id !== product.id)
.slice(0, 4);
function ProductPage({ product }: { product: ProductCategory }) {
// Find related products - either by explicit IDs or same category
const relatedProducts = product.relatedProductIds
? productCategories.filter((p) => product.relatedProductIds?.includes(p.id))
: productCategories
.filter((p) => p.slug === product.slug && p.id !== product.id)
.slice(0, 4);
return (
<div className="pt-32 pb-16">
<div className="container mx-auto px-4">
{/* Breadcrumb */}
<nav className="mb-6">
<ol className="flex items-center gap-2 text-sm">
<li>
<Link href="/" className="text-secondary-500 hover:text-primary-600">
</Link>
</li>
<li className="text-secondary-400">/</li>
<li>
<Link href="/product" className="text-secondary-500 hover:text-primary-600">
</Link>
</li>
<li className="text-secondary-400">/</li>
<li className="text-primary-600 font-medium">{product.name}</li>
</ol>
</nav>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Product Image */}
<div className="relative aspect-video bg-secondary-100 rounded-xl overflow-hidden">
<Image
src={product.image}
alt={product.name}
fill
className="object-cover"
priority
/>
</div>
{/* Product Info */}
<div>
<span className="text-primary-600 font-semibold">{product.nameEn}</span>
<h1 className="text-3xl md:text-4xl font-bold text-secondary-900 mt-2 mb-4">
{product.name}
</h1>
<p className="text-secondary-600 text-lg mb-6">
{product.description}
</p>
{/* CTA */}
<div className="flex flex-wrap gap-4">
<Link href="/contact-us" className="btn-primary">
</Link>
<a
href="tel:090-555-1415"
className="btn-outline"
>
</a>
</div>
</div>
</div>
{/* Related Products */}
{relatedProducts.length > 0 && (
<div className="mt-16">
<h2 className="text-2xl font-bold text-secondary-900 mb-6">
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{relatedProducts.map((related) => (
<Link
key={related.id}
href={related.href}
className="card group"
>
<div className="relative aspect-video bg-secondary-100">
<Image
src={related.image}
alt={related.name}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div className="p-4">
<span className="text-xs text-primary-600 font-semibold">
{related.nameEn}
</span>
<h3 className="text-lg font-bold text-secondary-900 mt-1 group-hover:text-primary-600 transition-colors">
{related.name}
</h3>
</div>
<>
{/* Schema.org Structured Data */}
<ProductSchema product={product} />
{product.faq && product.faq.length > 0 && <FAQSchema faq={product.faq} />}
<div className="pt-24 pb-16">
<div className="container mx-auto px-4">
{/* Breadcrumb */}
<nav className="mb-6">
<ol className="flex items-center gap-2 text-sm flex-wrap">
<li>
<Link href="/" className="text-secondary-500 hover:text-primary-600">
</Link>
))}
</li>
<li className="text-secondary-400">/</li>
<li>
<Link href="/product" className="text-secondary-500 hover:text-primary-600">
</Link>
</li>
<li className="text-secondary-400">/</li>
<li className="text-primary-600 font-medium">{product.name}</li>
</ol>
</nav>
{/* Product Header */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-12">
{/* Product Image */}
<div className="relative aspect-video bg-secondary-100 rounded-xl overflow-hidden">
<Image
src={product.image}
alt={`${product.name} - ${product.nameEn}`}
fill
className="object-cover"
priority
/>
</div>
{/* Product Info */}
<div>
<span className="text-primary-600 font-semibold">{product.nameEn}</span>
<h1 className="text-3xl md:text-4xl font-bold text-secondary-900 mt-2 mb-4">
{product.name}
</h1>
<p className="text-secondary-600 text-lg mb-6 leading-relaxed">
{product.description}
</p>
{/* CTA */}
<div className="flex flex-wrap gap-4 mb-6">
<Link href="/contact-us" className="btn-primary">
</Link>
<a
href="tel:090-555-1415"
className="btn-outline"
>
โทรสอบถาม: 090-555-1415
</a>
</div>
{/* Quick Contact Info */}
<div className="bg-secondary-50 rounded-lg p-4 space-y-2">
<div className="flex items-center gap-2 text-secondary-600">
<svg className="w-5 h-5 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<span>: <strong>090-555-1415</strong></span>
</div>
<div className="flex items-center gap-2 text-secondary-600">
<svg className="w-5 h-5 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span>Email: <strong>info@dealplustech.co.th</strong></span>
</div>
<div className="flex items-center gap-2 text-secondary-600">
<svg className="w-5 h-5 text-primary-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314"/>
</svg>
<span>LINE: <strong>@dealplustech</strong></span>
</div>
</div>
</div>
</div>
)}
{/* Specifications Section */}
{product.specifications && product.specifications.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold text-secondary-900 mb-6 flex items-center gap-2">
<svg className="w-6 h-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</h2>
<div className="bg-white rounded-xl border border-secondary-200 overflow-hidden">
<table className="w-full">
<tbody>
{product.specifications.map((spec, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-secondary-50' : 'bg-white'}>
<td className="px-6 py-4 font-medium text-secondary-700 w-1/3">{spec.label}</td>
<td className="px-6 py-4 text-secondary-900">
{spec.value}{spec.unit ? ` ${spec.unit}` : ''}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)}
{/* Features Section */}
{product.features && product.features.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold text-secondary-900 mb-6 flex items-center gap-2">
<svg className="w-6 h-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{product.features.map((feature, index) => (
<div key={index} className="flex items-start gap-3 p-4 bg-primary-50 rounded-lg">
<svg className="w-5 h-5 text-primary-600 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-secondary-700">{feature}</span>
</div>
))}
</div>
</section>
)}
{/* Applications Section */}
{product.applications && product.applications.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold text-secondary-900 mb-6 flex items-center gap-2">
<svg className="w-6 h-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</h2>
<div className="flex flex-wrap gap-3">
{product.applications.map((app, index) => (
<span key={index} className="inline-flex items-center px-4 py-2 bg-secondary-100 text-secondary-700 rounded-full font-medium">
{app}
</span>
))}
</div>
</section>
)}
{/* Certifications Section */}
{product.certifications && product.certifications.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold text-secondary-900 mb-6 flex items-center gap-2">
<svg className="w-6 h-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
</h2>
<div className="flex flex-wrap gap-3">
{product.certifications.map((cert, index) => (
<span key={index} className="inline-flex items-center px-4 py-2 bg-primary-100 text-primary-700 rounded-lg font-semibold">
{cert}
</span>
))}
</div>
</section>
)}
{/* SEO Content Section */}
{product.seoContent && (
<section className="mb-12">
<div className="prose prose-lg max-w-none">
<div dangerouslySetInnerHTML={{ __html: product.seoContent.replace(/\n/g, '<br/>') }} />
</div>
</section>
)}
{/* FAQ Section */}
{product.faq && product.faq.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold text-secondary-900 mb-6 flex items-center gap-2">
<svg className="w-6 h-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</h2>
<div className="space-y-4">
{product.faq.map((item, index) => (
<details key={index} className="group bg-white rounded-lg border border-secondary-200 overflow-hidden">
<summary className="flex items-center justify-between p-5 cursor-pointer font-medium text-secondary-900 hover:bg-secondary-50">
<span>{item.question}</span>
<svg className="w-5 h-5 text-secondary-500 group-open:rotate-180 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</summary>
<div className="px-5 pb-5 text-secondary-600 border-t border-secondary-100 pt-4">
{item.answer}
</div>
</details>
))}
</div>
</section>
)}
{/* Related Products */}
{relatedProducts.length > 0 && (
<section className="mt-16">
<h2 className="text-2xl font-bold text-secondary-900 mb-6">
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{relatedProducts.map((related) => (
<Link
key={related.id}
href={related.href}
className="card group"
>
<div className="relative aspect-video bg-secondary-100">
<Image
src={related.image}
alt={related.name}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div className="p-4">
<span className="text-xs text-primary-600 font-semibold">
{related.nameEn}
</span>
<h3 className="text-lg font-bold text-secondary-900 mt-1 group-hover:text-primary-600 transition-colors">
{related.name}
</h3>
</div>
</Link>
))}
</div>
</section>
)}
</div>
</div>
</div>
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,40 @@ export interface ProductCategory {
description: string;
shortDescription?: string;
keywords?: string[];
// Enhanced SEO Content
seoContent?: string;
// Product Specifications
specifications?: ProductSpecification[];
// Key Features
features?: string[];
// Applications/Uses
applications?: string[];
// Standards & Certifications
certifications?: string[];
// FAQ Section
faq?: FAQItem[];
// Schema.org data
schemaData?: {
brand?: string;
manufacturer?: string;
sku?: string;
mpn?: string;
material?: string;
category?: string;
};
// Related product IDs
relatedProductIds?: string[];
}
export interface ProductSpecification {
label: string;
value: string;
unit?: string;
}
export interface FAQItem {
question: string;
answer: string;
}
export interface NavItem {