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__/
This commit is contained in:
hermes
2026-06-08 12:45:32 +07:00
parent 7c905bdb00
commit b34f8fc2fb
81 changed files with 4031 additions and 282 deletions

View File

@@ -26,9 +26,89 @@ const related = allArticles
.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}`}>
<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">
@@ -51,22 +131,17 @@ const tag = article.data.tags?.[0] ?? '';
<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 class="flex items-center gap-4 text-slate-500">
<time datetime={article.data.published_at.toISOString().slice(0, 10)} class="text-lg">
{article.data.published_at.toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}
</time>
</div>
</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" />
<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-a:text-primary-600 prose-img:rounded-2xl prose-img:shadow-md mb-16">
<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>