--- import '@/styles/global.css'; import Layout from './Layout.astro'; import JsonLd from '@/components/seo/JsonLd.astro'; const companyInfo = { name: "ดีล พลัส เทค จำกัด", phone: "090-555-1415", email: "dealplustech@gmail.com", line: "@JPPSELECTION", address: "9/70 ซอยนครลุง 17 แขวงบางไผ่ เขตบางแค กรุงเทพมหานคร 10160", hours: "จันทร์-ศุกร์ 08:00-18:00 เสาร์ 08:00-17:00" }; interface Props { title: string; description?: string; ogImage?: string; canonical?: string; ogType?: 'website' | 'article' | 'product'; publishedTime?: string; author?: string; robots?: string; product?: { name: string; image: string; brand?: string; description?: string; sku?: string }; faq?: { question: string; answer: string }[]; jsonLd?: Record | Record[]; /** HowTo schema for installation/selection steps. * Provide a name, optional description, totalTime (ISO 8601 e.g. PT30M), * and an array of step strings. */ howTo?: { name: string; description?: string; totalTime?: string; steps: string[] }; product?: { name?: string; description?: string; image: string; sku?: string; brand?: string; priceCurrency?: string; price?: number; availability?: 'InStock' | 'OutOfStock' | 'PreOrder'; url?: string; }; /** * FAQ schema. Pass an array of {question, answer} pairs to render * a FAQPage JSON-LD. Answer may be plain text or HTML. */ faq?: Array<{ question: string; answer: string }>; /** * Override the auto-generated breadcrumb. If omitted, BaseLayout * builds a breadcrumb automatically from the categories tree. */ breadcrumb?: Array<{ name: string; url: string }>; } const { title, description = "ดีล พลัส เทค - ระบบน้ำคุณภาพสูง ราคาโรงงาน", ogImage, canonical, ogType = 'website', publishedTime, author, robots, jsonLd, product: productProp, faq: faqProp, breadcrumb: breadcrumbProp, howTo: howToProp, } = Astro.props; // Auto-standardize title suffix: "Foo" -> "Foo | ดีล พลัส เทค" // Skip when the title already ends with the brand name (case-insensitive). const BRAND = "ดีล พลัส เทค"; const trimmed = title.trim(); const lowerTitle = trimmed.toLowerCase(); const lowerBrand = BRAND.toLowerCase(); const hasBrandSuffix = lowerTitle.endsWith(lowerBrand) || lowerTitle.endsWith(lowerBrand + ' - ' + lowerBrand) || lowerTitle === lowerBrand; const fullTitle = hasBrandSuffix ? trimmed : `${trimmed} | ${BRAND}`; // Organization JSON-LD (rendered on every page so Google can verify the // publisher of the site). LocalBusiness subtype for Maps/3-pack SEO. const siteUrl = 'https://dealplustech.com'; const organizationSchema = { '@context': 'https://schema.org', '@type': 'LocalBusiness', '@id': `${siteUrl}/#organization`, name: companyInfo.name, url: siteUrl, logo: `${siteUrl}/images/logo/dealplustech-logo.png`, image: `${siteUrl}/images/og/default-og.jpg`, description: 'ผู้นำเข้าและจัดจำหน่ายระบบน้ำคุณภาพสูง ราคาโรงงาน', telephone: companyInfo.phone, email: companyInfo.email, priceRange: '฿฿', address: { '@type': 'PostalAddress', streetAddress: '9/70 ซอยนครลุง 17', addressLocality: 'แขวงบางไผ่', addressRegion: 'เขตบางแค', addressCountry: 'TH', postalCode: '10160', }, openingHoursSpecification: [ { '@type': 'OpeningHoursSpecification', dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], opens: '08:00', closes: '18:00', }, { '@type': 'OpeningHoursSpecification', dayOfWeek: 'Saturday', opens: '08:00', closes: '17:00', }, ], sameAs: [ `https://line.me/ti/p/~${companyInfo.line.replace('@', '')}`, ], }; const categories = [ { name: 'ท่อพีพีอาร์', slug: '/ท่อ-ppr-thai-ppr', subcategories: [ { name: 'ไทยพีพีอาร์', slug: '/ท่อ-ppr-thai-ppr' }, { name: 'ท่อ PPR ตราช้าง', slug: '/ท่อ-ppr-scg' }, { name: 'ท่อ HDPE', slug: '/ท่อ-hdpe' }, { name: 'ท่อ UPVC', slug: '/ท่อ-upvc' }, { name: 'ท่อ Syler', slug: '/ท่อ-syler' }, { name: 'ท่อ XYLENT', slug: '/ท่อ-xy-lent' }, ] }, { name: 'เครื่องเชื่อมท่อ', slug: '/เครื่องเชื่อม-hdpe', subcategories: [ { name: 'เครื่องเชื่อม HDPE', slug: '/เครื่องเชื่อม-hdpe' }, { name: 'เครื่องเชื่อม PPR', slug: '/เครื่องเชื่อม-ppr' }, { name: 'Pipe Coupling', slug: '/pipe-coupling' }, { name: 'เม็ดกรู๊ฟ คับปลิ้ง', slug: '/เม็กกรู๊ฟ-คับปลิ้ง' }, ] }, { name: 'ระบบน้ำ', slug: '/water-pump', subcategories: [ { name: 'วาล์ว', slug: '/วาล์ว-valve' }, { name: 'ปั๊มน้ำ', slug: '/water-pump' }, { name: 'ระบบกรองน้ำ', slug: '/water-treatment' }, ] }, { name: 'อุปกรณ์ปรับอากาศ', slug: '/grilles', subcategories: [ { name: 'กริลแอร์', slug: '/grilles' }, { name: 'DURGO วาล์วเติมอากาศ', slug: '/durgo-avvs' }, { name: 'หัวจ่ายแอร์ Ball Jet', slug: '/หัวจ่าย-ball-jet' }, ] }, { name: 'อุปกรณ์ดับเพลิง', slug: '/ตู้ดับเพลิง', subcategories: [ { name: 'ตู้ดับเพลิง', slug: '/ตู้ดับเพลิง' }, { name: 'Realflex', slug: '/realflex' }, ] }, { name: 'ฉนวนหุ้มท่อ', slug: '/armflex', subcategories: [ { name: 'Armaflex', slug: '/armflex' }, { name: 'Aeroflex', slug: '/aeroflex' }, { name: 'Maxflex', slug: '/maxflex' }, { name: 'เทอร์โมเบรค', slug: '/เทอร์โมเบรค-thermobreak' }, ] }, { name: 'ระบบรั้ว', slug: '/รั้วเทวดา', subcategories: [ { name: 'รั้วเทวดา', slug: '/รั้วเทวดา' }, { name: 'ระบบรั้วไวน์แมน', slug: '/ระบบรั้วไวน์แมน' }, ] }, ]; // --------------------------------------------------------------------------- // Schema builders // --------------------------------------------------------------------------- /** * Build a BreadcrumbList from the current URL by walking the categories tree. * Pages outside the product tree (e.g. /about-us, /contact-us) get a 1-item * breadcrumb (Home). Pages in the product tree get: Home > Category > Page. */ function buildBreadcrumb() { if (breadcrumbProp) return breadcrumbProp; // Normalise pathname: URL-decode (Astro returns encoded in static build), // then strip trailing slash so it matches the slugs in the categories // tree (which are stored decoded and have no trailing slash). const rawPath = Astro.url.pathname; const decoded = (() => { try { return decodeURIComponent(rawPath); } catch { return rawPath; } })(); const path = decoded.replace(/\/+$/, '') || '/'; if (path === '/') return []; // Find the subcategory whose slug matches the current path for (const cat of categories) { const sub = cat.subcategories.find(s => s.slug === path); if (sub) { return [ { name: 'หน้าแรก', url: '/' }, { name: cat.name, url: cat.slug }, { name: sub.name, url: sub.slug }, ]; } } // Page not in product tree → just Home return [{ name: 'หน้าแรก', url: '/' }]; } const breadcrumbItems = buildBreadcrumb(); const breadcrumbSchema = breadcrumbItems.length > 0 ? { '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: breadcrumbItems.map((item, idx) => ({ '@type': 'ListItem', position: idx + 1, name: item.name, item: `${siteUrl}${item.url}`, })), } : null; /** * Build a Product JSON-LD from the `product` prop. Uses page title/description * as defaults so callers only need to pass `image`. */ const productSchema = productProp ? { '@context': 'https://schema.org', '@type': 'Product', name: productProp.name ?? trimmed, description: productProp.description ?? description, image: /^https?:\/\//.test(productProp.image) ? productProp.image : `${siteUrl}${productProp.image}`, ...(productProp.sku && { sku: productProp.sku }), ...(productProp.brand && { brand: { '@type': 'Brand', name: productProp.brand }, }), offers: { '@type': 'Offer', url: productProp.url ?? `${siteUrl}${Astro.url.pathname}`, priceCurrency: productProp.priceCurrency ?? 'THB', ...(productProp.price !== undefined && { price: productProp.price }), availability: `https://schema.org/${productProp.availability ?? 'InStock'}`, seller: { '@id': `${siteUrl}/#organization` }, }, } : null; /** * Build a FAQPage JSON-LD from the `faq` prop. Strips HTML from answers * for the schema text field (Google prefers plain text). */ const faqSchema = faqProp && faqProp.length > 0 ? { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faqProp.map(item => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer.replace(/<[^>]+>/g, '').trim(), }, })), } : null; // HowTo schema — installation/selection steps as ordered list const howToSchema = howToProp && howToProp.steps.length > 0 ? { '@context': 'https://schema.org', '@type': 'HowTo', name: howToProp.name, ...(howToProp.description && { description: howToProp.description }), ...(howToProp.totalTime && { totalTime: howToProp.totalTime }), step: howToProp.steps.map((text, i) => ({ '@type': 'HowToStep', position: i + 1, name: `ขั้นตอนที่ ${i + 1}`, text: text, })), } : null; /** * Collect all JSON-LD payloads to render on this page. * Always includes the Organization schema. Plus: breadcrumb, product, faq, * and any caller-supplied custom JSON-LD. */ const jsonLdPayloads: Record[] = [organizationSchema]; if (breadcrumbSchema) jsonLdPayloads.push(breadcrumbSchema); if (productSchema) jsonLdPayloads.push(productSchema); if (faqSchema) jsonLdPayloads.push(faqSchema); if (howToSchema) jsonLdPayloads.push(howToSchema); if (jsonLd) { if (Array.isArray(jsonLd)) { jsonLdPayloads.push(...jsonLd); } else { jsonLdPayloads.push(jsonLd); } } const productLinks = [ { title: "ไทยพีพีอาร์", href: "/ท่อ-ppr-thai-ppr" }, { title: "เทอร์โมเบรค", href: "/เทอร์โมเบรค-thermobreak" }, { title: "กริลแอร์", href: "/grilles" }, { title: "หัวจ่ายแอร์ Ball Jet", href: "/หัวจ่าย-ball-jet" }, { title: "ท่อ HDPE", href: "/ท่อ-hdpe" }, { title: "ท่อ Syler", href: "/ท่อ-syler" }, ]; --- {jsonLdPayloads.map((data, idx) => ( ))}