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 Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { productCategories, portfolioProjects } from '@/data/site-config'; import { productCategories, portfolioProjects } from '@/data/site-config';
import type { ProductCategory, FAQItem } from '@/types';
interface Props { interface Props {
params: { slug: string[] }; params: { slug: string[] };
@@ -30,7 +30,7 @@ export async function generateStaticParams() {
type ContentType = 'product' | 'portfolio'; 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 // Decode URL-encoded slug parts
const decodedSlug = slug.map(part => decodeURIComponent(part)); const decodedSlug = slug.map(part => decodeURIComponent(part));
const fullPath = '/' + decodedSlug.join('/') + '/'; const fullPath = '/' + decodedSlug.join('/') + '/';
@@ -60,11 +60,21 @@ export async function generateMetadata({ params }: Props) {
const { type, data } = content; const { type, data } = content;
if (type === 'product') { 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 { return {
title: `${product.name} - ${product.nameEn}`, title,
description: product.description, 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) { export default function DynamicPage({ params }: Props) {
const content = findContentBySlug(params.slug); const content = findContentBySlug(params.slug);
@@ -89,22 +161,29 @@ export default function DynamicPage({ params }: Props) {
} }
// Render product page // Render product page
return <ProductPage product={data as typeof productCategories[0]} />; return <ProductPage product={data as ProductCategory} />;
} }
// Product Page Component // Product Page Component
function ProductPage({ product }: { product: typeof productCategories[0] }) { function ProductPage({ product }: { product: ProductCategory }) {
// Find related products in same category // Find related products - either by explicit IDs or same category
const relatedProducts = productCategories const relatedProducts = product.relatedProductIds
? productCategories.filter((p) => product.relatedProductIds?.includes(p.id))
: productCategories
.filter((p) => p.slug === product.slug && p.id !== product.id) .filter((p) => p.slug === product.slug && p.id !== product.id)
.slice(0, 4); .slice(0, 4);
return ( return (
<div className="pt-32 pb-16"> <>
{/* 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"> <div className="container mx-auto px-4">
{/* Breadcrumb */} {/* Breadcrumb */}
<nav className="mb-6"> <nav className="mb-6">
<ol className="flex items-center gap-2 text-sm"> <ol className="flex items-center gap-2 text-sm flex-wrap">
<li> <li>
<Link href="/" className="text-secondary-500 hover:text-primary-600"> <Link href="/" className="text-secondary-500 hover:text-primary-600">
@@ -121,12 +200,13 @@ function ProductPage({ product }: { product: typeof productCategories[0] }) {
</ol> </ol>
</nav> </nav>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12"> {/* Product Header */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-12">
{/* Product Image */} {/* Product Image */}
<div className="relative aspect-video bg-secondary-100 rounded-xl overflow-hidden"> <div className="relative aspect-video bg-secondary-100 rounded-xl overflow-hidden">
<Image <Image
src={product.image} src={product.image}
alt={product.name} alt={`${product.name} - ${product.nameEn}`}
fill fill
className="object-cover" className="object-cover"
priority priority
@@ -139,12 +219,12 @@ function ProductPage({ product }: { product: typeof productCategories[0] }) {
<h1 className="text-3xl md:text-4xl font-bold text-secondary-900 mt-2 mb-4"> <h1 className="text-3xl md:text-4xl font-bold text-secondary-900 mt-2 mb-4">
{product.name} {product.name}
</h1> </h1>
<p className="text-secondary-600 text-lg mb-6"> <p className="text-secondary-600 text-lg mb-6 leading-relaxed">
{product.description} {product.description}
</p> </p>
{/* CTA */} {/* CTA */}
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4 mb-6">
<Link href="/contact-us" className="btn-primary"> <Link href="/contact-us" className="btn-primary">
</Link> </Link>
@@ -152,15 +232,159 @@ function ProductPage({ product }: { product: typeof productCategories[0] }) {
href="tel:090-555-1415" href="tel:090-555-1415"
className="btn-outline" className="btn-outline"
> >
โทรสอบถาม: 090-555-1415
</a> </a>
</div> </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>
</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 */} {/* Related Products */}
{relatedProducts.length > 0 && ( {relatedProducts.length > 0 && (
<div className="mt-16"> <section className="mt-16">
<h2 className="text-2xl font-bold text-secondary-900 mb-6"> <h2 className="text-2xl font-bold text-secondary-900 mb-6">
</h2> </h2>
@@ -190,10 +414,11 @@ function ProductPage({ product }: { product: typeof productCategories[0] }) {
</Link> </Link>
))} ))}
</div> </div>
</div> </section>
)} )}
</div> </div>
</div> </div>
</>
); );
} }

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,40 @@ export interface ProductCategory {
description: string; description: string;
shortDescription?: string; shortDescription?: string;
keywords?: string[]; keywords?: string[];
// Enhanced SEO Content
seoContent?: string; 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 { export interface NavItem {