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__/
154 lines
5.1 KiB
TypeScript
154 lines
5.1 KiB
TypeScript
/**
|
|
* /sitemap.xml.ts — Custom sitemap with per-type priority/changefreq.
|
|
*
|
|
* Generates a sitemap by:
|
|
* 1. Listing all static .astro pages (excluding 404 + dynamic [slug])
|
|
* 2. Walking content collections (blog) for dynamic routes
|
|
* 3. Classifying each URL into a category to assign priority + changefreq
|
|
*
|
|
* Per Astro 6, the file lives in src/pages/ and exports a GET handler.
|
|
*/
|
|
import type { APIRoute } from 'astro';
|
|
import { getCollection } from 'astro:content';
|
|
|
|
const SITE = 'https://dealplustech.com';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// URL classification
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const NOINDEX_PAGES = new Set([
|
|
'privacy-policy',
|
|
'terms-and-conditions',
|
|
'404',
|
|
]);
|
|
|
|
const PRODUCT_SLUGS = new Set([
|
|
'aeroflex', 'armflex', 'durgo-avvs', 'grilles', 'maxflex',
|
|
'pipe-coupling', 'realflex', 'water-pump', 'water-treatment',
|
|
'ตู้ดับเพลิง',
|
|
'ท่อ-hdpe', 'ท่อ-ppr-scg', 'ท่อ-ppr-thai-ppr', 'ท่อ-syler',
|
|
'ท่อ-upvc', 'ท่อ-xy-lent',
|
|
'ระบบรั้วไวน์แมน', 'รั้วเทวดา',
|
|
'วาล์ว-valve', 'หัวจ่าย-ball-jet',
|
|
'เครื่องเชื่อม-hdpe', 'เครื่องเชื่อม-ppr',
|
|
'เทอร์โมเบรค-thermobreak', 'เม็กกรู๊ฟ-คับปลิ้ง',
|
|
]);
|
|
|
|
const HOMEPAGE_SLUGS = new Set(['index', 'all-products']);
|
|
const CATEGORY_SLUGS = new Set(['ระบบน้ำ']);
|
|
const STATIC_SLUGS = new Set(['about-us', 'contact-us', 'portfolio']);
|
|
|
|
type Entry = {
|
|
loc: string;
|
|
priority: number;
|
|
changefreq: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
|
lastmod?: string;
|
|
};
|
|
|
|
function classify(slug: string): { priority: number; changefreq: Entry['changefreq'] } {
|
|
if (slug === 'index') return { priority: 1.0, changefreq: 'weekly' };
|
|
if (slug === 'all-products') return { priority: 0.9, changefreq: 'weekly' };
|
|
if (PRODUCT_SLUGS.has(slug)) return { priority: 0.9, changefreq: 'monthly' };
|
|
if (CATEGORY_SLUGS.has(slug)) return { priority: 0.8, changefreq: 'monthly' };
|
|
if (STATIC_SLUGS.has(slug)) return { priority: 0.6, changefreq: 'yearly' };
|
|
return { priority: 0.5, changefreq: 'monthly' };
|
|
}
|
|
|
|
function urlFromSlug(slug: string): string {
|
|
if (slug === 'index') return `${SITE}/`;
|
|
return `${SITE}/${slug}`;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Build entries
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function buildEntries(): Promise<Entry[]> {
|
|
// 1. Static pages — discovered via Vite's import.meta.glob
|
|
const modules = import.meta.glob<true>('./*.astro');
|
|
const pageFiles = Object.keys(modules)
|
|
.map(p => p.replace(/^\.\//, '').replace(/\.astro$/, ''))
|
|
.filter(slug => !slug.startsWith('[') && !NOINDEX_PAGES.has(slug));
|
|
|
|
const entries: Entry[] = pageFiles.map(slug => {
|
|
const { priority, changefreq } = classify(slug);
|
|
return { loc: urlFromSlug(slug), priority, changefreq };
|
|
});
|
|
|
|
// 2. Blog posts (dynamic [slug] route)
|
|
const articles = await getCollection('blog');
|
|
for (const article of articles) {
|
|
entries.push({
|
|
loc: `${SITE}/${encodeURI('บทความ')}/${encodeURIComponent(article.id)}`,
|
|
priority: 0.7,
|
|
changefreq: 'yearly',
|
|
lastmod: article.data.published_at.toISOString(),
|
|
});
|
|
}
|
|
|
|
// 3. Blog index
|
|
entries.push({
|
|
loc: `${SITE}/${encodeURI('บทความ')}`,
|
|
priority: 0.7,
|
|
changefreq: 'weekly',
|
|
});
|
|
|
|
// Sort: homepage first, then by priority desc, then by URL asc
|
|
entries.sort((a, b) => {
|
|
if (a.loc === `${SITE}/`) return -1;
|
|
if (b.loc === `${SITE}/`) return 1;
|
|
if (a.priority !== b.priority) return b.priority - a.priority;
|
|
return a.loc.localeCompare(b.loc);
|
|
});
|
|
|
|
return entries;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// XML serialiser
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function escapeXml(s: string): string {
|
|
return s
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function toXml(entries: Entry[]): string {
|
|
const urls = entries.map(e => {
|
|
const lastmod = e.lastmod ? `\n <lastmod>${e.lastmod}</lastmod>` : '';
|
|
return ` <url>
|
|
<loc>${escapeXml(e.loc)}</loc>${lastmod}
|
|
<changefreq>${e.changefreq}</changefreq>
|
|
<priority>${e.priority.toFixed(1)}</priority>
|
|
</url>`;
|
|
}).join('\n');
|
|
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
${urls}
|
|
</urlset>
|
|
`;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Handler
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const GET: APIRoute = async () => {
|
|
const entries = await buildEntries();
|
|
const xml = toXml(entries);
|
|
|
|
return new Response(xml, {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/xml; charset=utf-8',
|
|
'Cache-Control': 'public, max-age=3600',
|
|
},
|
|
});
|
|
};
|