Files
dealplustech-astroreal/src/pages/บทความ/[slug].astro
hermes b34f8fc2fb feat(blog): Phase 5 SEO/GEO content with 5 new blog posts
Add 5 long-form Thai blog posts (1,200-2,500 words each) with SEO + GEO
optimization for the dealplustech water-systems site. Each post targets
a specific audience (contractors, engineers, project managers) and
follows a content-quality workflow: source real product specs, verify
Thai text, dedupe images, link back to product pages.

## New blog posts (src/content/blog/)
- thermobreak-guide.md (Thermobreak closed-cell insulation overview)
- plastic-grilles-guide.md (ABS plastic grilles for HVAC)
- ppr-pipe-guide.md (PPR pipe properties + heat-fusion welding)
- ppr-vs-hdpe-vs-upvc.md (3-way pipe comparison with PE80/PE100)
- thermobreak-series-guide.md (Thermobreak LS vs Solar series)
- 10-things-checklist-pipe-ordering.md (10-point pre-order checklist)

## Removed legacy posts
- pipe-knowledge.md, valve-guide.md, welcome-post.md (orphans)

## Hero images (public/images/blog/)
~20 product photos sourced from manufacturers (Thermobreak, Thai PPR,
thaiconsupply) plus Nano Banana Pro infographics. All resized to
3:2 aspect ratio per user preference. Source folder preserved for
re-derivation.

## Astro layout/SEO work
- src/components/seo/SEO.astro, JsonLd.astro (new SEO components)
- src/layouts/BaseLayout.astro, Layout.astro (OG/Twitter/JSON-LD wiring)
- src/pages/404.astro
- Product pages (8): added #pricelist anchors + schema work
- src/styles/global.css: scroll-padding for sticky-header anchors

## Automation scripts (scripts/)
- build_og_image.py (OG image builder)
- inject_faq_schema.py, inject_product_schema.py (JSON-LD injection)

## Misc
- public/robots.txt, public/images/og/default-og.jpg
- .gitignore: exclude scripts/__pycache__/
2026-06-08 12:45:32 +07:00

196 lines
10 KiB
Plaintext

---
import BaseLayout from '@/layouts/BaseLayout.astro';
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const articles = await getCollection('blog');
return articles.map(article => ({
params: { slug: article.id },
props: { article },
}));
}
const { article } = Astro.props;
const { Content } = await render(article);
// Get related articles (same tags first, then by date, excluding current)
const allArticles = await getCollection('blog');
const related = allArticles
.filter(a => a.id !== article.id)
.sort((a, b) => {
const aTagMatch = a.data.tags?.some(t => article.data.tags?.includes(t)) ? 1 : 0;
const bTagMatch = b.data.tags?.some(t => article.data.tags?.includes(t)) ? 1 : 0;
if (aTagMatch !== bTagMatch) return bTagMatch - aTagMatch;
return b.data.published_at.getTime() - a.data.published_at.getTime();
})
.slice(0, 3);
const tag = article.data.tags?.[0] ?? '';
// BlogPosting JSON-LD — provides rich result eligibility in Google
// (article cards, author attribution, publish date).
const articleUrl = `https://dealplustech.com/${encodeURI('บทความ')}/${article.id}`;
const blogPostingSchema = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
'@id': `${articleUrl}#article`,
headline: article.data.title,
description: article.data.excerpt || `บทความ ${article.data.title}`,
...(article.data.featured_image && {
image: /^https?:\/\//.test(article.data.featured_image)
? article.data.featured_image
: `https://dealplustech.com${article.data.featured_image}`,
}),
datePublished: article.data.published_at.toISOString(),
dateModified: (article.data.updated_at ?? article.data.published_at).toISOString(),
author: {
'@type': 'Organization',
name: article.data.author || 'ดีล พลัส เทค',
url: 'https://dealplustech.com',
},
publisher: {
'@type': 'Organization',
name: 'ดีล พลัส เทค',
logo: {
'@type': 'ImageObject',
url: 'https://dealplustech.com/images/logo/dealplustech-logo.png',
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': articleUrl,
},
...(article.data.tags && article.data.tags.length > 0 && {
keywords: article.data.tags.join(', '),
}),
...(article.data.reviewer && {
reviewedBy: {
'@type': 'Organization',
name: article.data.reviewer,
},
}),
};
// Extract FAQ pairs from article body — scoped to the FAQ section only.
// Looks for `## คำถาม...` or `## FAQ` heading then captures all H3+paragraph
// pairs until the next H2.
const faqPairs: { question: string; answer: string }[] = [];
const body = article.body ?? '';
const faqSection = body.match(/##\s+(?:คำถาม[^\n]*|FAQ[^\n]*|Common Questions[^\n]*)\s*\n([\s\S]*?)(?=\n##\s|\n---\s*$|$)/i);
if (faqSection) {
const sectionBody = faqSection[1];
const pairRegex = /###\s+(.+?)\n\n([\s\S]*?)(?=\n###|\n##\s|$)/g;
for (const m of sectionBody.matchAll(pairRegex)) {
const question = m[1].trim();
const answer = m[2].trim().replace(/\n+/g, ' ');
if (question && answer && !question.startsWith('##')) {
faqPairs.push({ question, answer });
}
}
}
const faqSchema = faqPairs.length >= 2 ? {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqPairs.map(p => ({
'@type': 'Question',
name: p.question,
acceptedAnswer: { '@type': 'Answer', text: p.answer },
})),
} : null;
---
<BaseLayout
title={`${article.data.title} - ดีล พลัส เทค`}
description={article.data.excerpt || `บทความ ${article.data.title}`}
ogImage={article.data.og_image ?? article.data.featured_image}
ogType="article"
publishedTime={article.data.published_at.toISOString()}
author="ดีล พลัส เทค"
jsonLd={blogPostingSchema}
faq={faqPairs}
>
<main class="bg-white min-h-screen">
{/* Breadcrumb */}
<section class="bg-slate-50 border-b border-slate-100">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<nav class="text-sm text-slate-500">
<a href="/" class="hover:text-primary-600 transition-colors">หน้าแรก</a>
<span class="mx-2">/</span>
<a href={`/${encodeURI('บทความ')}`} class="hover:text-primary-600 transition-colors">บทความ</a>
<span class="mx-2">/</span>
<span class="text-slate-800">{article.data.title}</span>
</nav>
</div>
</section>
{/* Article Header */}
<section class="py-12 lg:py-16">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="mb-8">
{tag && (
<span class="inline-block px-3 py-1 bg-primary-50 text-primary-600 rounded-full text-sm font-medium mb-4">{tag}</span>
)}
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-slate-900 mb-4 leading-tight">{article.data.title}</h1>
</div>
{/* Featured Image */}
{article.data.featured_image && (
<div class="rounded-3xl overflow-hidden mb-12 shadow-lg">
<img src={article.data.featured_image} alt={article.data.title} class="w-full h-auto" loading="lazy" />
</div>
)}
{/* Article Body */}
<article class="prose prose-lg max-w-none prose-headings:text-slate-900 prose-headings:font-bold prose-h2:text-2xl prose-h2:mt-12 prose-h2:mb-4 prose-h2:pb-3 prose-h2:border-b-2 prose-h2:border-primary-200 prose-h2:text-primary-800 prose-h3:text-xl prose-h3:mt-8 prose-h3:mb-3 prose-h3:text-primary-700 prose-h3:font-semibold prose-h4:text-base prose-h4:font-semibold prose-a:text-primary-600 prose-a:no-underline hover:prose-a:underline prose-img:rounded-2xl prose-img:shadow-md prose-strong:text-neutral-900 prose-strong:font-semibold prose-table:my-0 prose-th:bg-primary-50 prose-th:text-primary-800 prose-th:font-bold prose-td:border-neutral-200 mb-16">
<Content />
</article>
{/* Share Buttons */}
<div class="border-t border-slate-100 pt-8 mb-8">
<div class="flex items-center gap-4">
<span class="text-sm font-medium text-slate-700">แชร์บทความ:</span>
<a href={`https://line.me/R/msg/text/?${encodeURIComponent(`${article.data.title} - https://dealplustech.com/${encodeURI('บทความ')}/${encodeURIComponent(article.id)}`)}`} target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-4 py-2 bg-green-50 text-green-600 rounded-xl hover:bg-green-100 transition-colors text-sm font-medium">
<svg class="w-5 h-5" 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-.344.282-.629.627-.629h2.386c.349 0 .63.285.63.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-.629.63-.629.345 0 .63.284.63.629v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.629.627-.629.349 0 .631.284.631.629v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.629.63-.629.348 0 .63.284.63.629v4.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 18.062 24 10.314\"/></svg>
Line
</a>
<a href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(`https://dealplustech.com/${encodeURI('บทความ')}/${encodeURIComponent(article.id)}`)}`} target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-4 py-2 bg-blue-50 text-blue-600 rounded-xl hover:bg-blue-100 transition-colors text-sm font-medium">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
Facebook
</a>
</div>
</div>
</div>
</section>
{/* Related Articles */}
{related.length > 0 && (
<section class="py-12 lg:py-16 bg-slate-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-2xl font-bold text-slate-900 mb-8">บทความที่เกี่ยวข้อง</h2>
<div class="grid md:grid-cols-3 gap-8">
{related.map(rel => (
<a href={`/${encodeURI('บทความ')}/${encodeURIComponent(rel.id)}`} class="group block bg-white rounded-3xl overflow-hidden border border-slate-100 hover:border-primary-200 hover:shadow-xl transition-all duration-300">
<div class="aspect-[16/9] bg-slate-100 overflow-hidden">
{rel.data.featured_image ? (
<img src={rel.data.featured_image} alt={rel.data.title} class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" loading="lazy" />
) : (
<div class="w-full h-full flex items-center justify-center text-slate-300">
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
)}
</div>
<div class="p-5">
<h3 class="text-base font-bold text-slate-900 group-hover:text-primary-600 transition-colors mb-2 line-clamp-2">{rel.data.title}</h3>
<p class="text-sm text-slate-600 line-clamp-2">{rel.data.excerpt}</p>
</div>
</a>
))}
</div>
</div>
</section>
)}
</main>
</BaseLayout>