diff --git a/astro.config.mjs b/astro.config.mjs index e1ca4bc..b7ee0e9 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,13 +1,14 @@ import { defineConfig } from 'astro/config' import tailwindcss from '@tailwindcss/vite' import react from '@astrojs/react' +import sitemap from '@astrojs/sitemap' import { fileURLToPath } from 'url' import path from 'path' const __dirname = path.dirname(fileURLToPath(import.meta.url)) export default defineConfig({ - site: 'https://dealplustech.com', + site: 'https://dealplustech.co.th', output: 'static', vite: { plugins: [tailwindcss()], @@ -22,6 +23,7 @@ export default defineConfig({ }, integrations: [ react(), + sitemap(), ], build: { assets: '_assets', diff --git a/package-lock.json b/package-lock.json index b27c160..6de4109 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { - "name": "dealplustech-emdash", + "name": "dealplustech-astroreal", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "dealplustech-emdash", + "name": "dealplustech-astroreal", "version": "1.0.0", "dependencies": { "@astrojs/check": "^0.9.4", "@astrojs/react": "^5.0.5", + "@astrojs/sitemap": "^3.7.3", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.0.0", "astro": "^6.1.7", @@ -160,6 +161,17 @@ "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.3.tgz", + "integrity": "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA==", + "license": "MIT", + "dependencies": { + "sitemap": "^9.0.0", + "stream-replace-string": "^2.0.0", + "zod": "^4.3.6" + } + }, "node_modules/@astrojs/telemetry": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", @@ -2329,8 +2341,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -2355,6 +2365,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2562,6 +2581,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5748,6 +5773,40 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, + "node_modules/sitemap": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz", + "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.9.2", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + }, + "bin": { + "sitemap": "dist/esm/cli.js" + }, + "engines": { + "node": ">=20.19.5", + "npm": ">=10.8.2" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/sitemap/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, "node_modules/smol-toml": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", @@ -5779,6 +5838,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stream-replace-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", + "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5980,9 +6045,7 @@ "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/unified": { "version": "11.0.5", diff --git a/package.json b/package.json index 8a9db40..9f0c629 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@astrojs/check": "^0.9.4", "@astrojs/react": "^5.0.5", + "@astrojs/sitemap": "^3.7.3", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.0.0", "astro": "^6.1.7", diff --git a/src/pages/sitemap.xml.ts b/src/pages/sitemap.xml.ts deleted file mode 100644 index b87769b..0000000 --- a/src/pages/sitemap.xml.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * /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.co.th'; - -// --------------------------------------------------------------------------- -// 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 { - // 1. Static pages — discovered via Vite's import.meta.glob - const modules = import.meta.glob('./*.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, '''); -} - -function toXml(entries: Entry[]): string { - const urls = entries.map(e => { - const lastmod = e.lastmod ? `\n ${e.lastmod}` : ''; - return ` - ${escapeXml(e.loc)}${lastmod} - ${e.changefreq} - ${e.priority.toFixed(1)} - `; - }).join('\n'); - - return ` - -${urls} - -`; -} - -// --------------------------------------------------------------------------- -// 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', - }, - }); -};