refactor: move blog from EmDash to Astro content collections

EmDash CMS integration is being removed. The blog content is
moved to native Astro content collections (markdown files in
src/content/blog/) which works with the static output config.

Changes:
- Remove EmDash from astro.config.mjs (revert to static output)
- Remove emdash packages from package.json/package-lock.json
- Remove seed/seed.json (was EmDash-only)
- Remove src/live.config.ts (EmDash Astro loader)
- Add src/content.config.ts (Astro content collection for blog)
- Move 3 blog posts to src/content/blog/*.md
- Update src/pages/index.astro to use getCollection('blog')
- Update src/pages/บทความ/[slug].astro to use render() from astro:content
  (Astro 6 API: render(article), not article.render())
- Update src/pages/บทความ/index.astro (blog list)
- Add .hermes/ to .gitignore

Verified:
- npm run build: 35 pages, complete in 2.50s
- / , /aeroflex, /about-us, /บทความ, /บทความ/welcome-post: all 200
This commit is contained in:
Kunthawat Greethong
2026-06-03 14:02:41 +07:00
parent c8cf03a725
commit ef4b0f2e89
13 changed files with 296 additions and 4800 deletions

1
.gitignore vendored
View File

@@ -25,3 +25,4 @@ uploads/
# Generated
emdash-env.d.ts
.hermes/

View File

@@ -1,10 +1,6 @@
import { defineConfig } from 'astro/config'
import tailwindcss from '@tailwindcss/vite'
import node from '@astrojs/node'
import react from '@astrojs/react'
import emdash, { local } from 'emdash/astro'
import { sqlite } from 'emdash/db'
import { google } from 'emdash/auth/providers/google'
import { fileURLToPath } from 'url'
import path from 'path'
@@ -12,10 +8,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default defineConfig({
site: 'https://dealplustech.com',
output: 'server',
adapter: node({
mode: 'standalone',
}),
output: 'static',
vite: {
plugins: [tailwindcss()],
resolve: {
@@ -29,14 +22,6 @@ export default defineConfig({
},
integrations: [
react(),
emdash({
database: sqlite({ url: 'file:./data.db' }),
storage: local({
directory: './uploads',
baseUrl: '/_emdash/api/media/file',
}),
authProviders: [google()],
}),
],
build: {
assets: '_assets',

4480
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,17 +7,14 @@
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"start": "node ./dist/server/entry.mjs"
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/node": "^10.1.1",
"@astrojs/react": "^5.0.5",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
"astro": "^6.1.7",
"emdash": "^0.14.0",
"marked": "^18.0.3",
"react": "^19.2.6",
"react-dom": "^19.2.6",

View File

@@ -1,137 +0,0 @@
{
"$schema": "https://emdashcms.com/seed.schema.json",
"version": "1",
"meta": {
"name": "ดีล พลัส เทค",
"description": "ระบบน้ำคุณภาพสูง ราคาโรงงาน",
"author": "ดีล พลัส เทค"
},
"settings": {
"title": "ดีล พลัส เทค",
"tagline": "ระบบน้ำคุณภาพสูง ราคาโรงงาน"
},
"collections": [
{
"slug": "blog",
"label": "บทความ",
"labelSingular": "บทความ",
"description": "บทความและข่าวสาร",
"supports": ["drafts", "revisions", "search"],
"fields": [
{ "slug": "title", "label": "หัวข้อ", "type": "string", "required": true, "searchable": true },
{ "slug": "excerpt", "label": "คำอธิบายสั้น", "type": "text", "required": true, "searchable": true },
{ "slug": "body", "label": "เนื้อหา", "type": "portableText", "required": true, "searchable": true },
{ "slug": "featured_image", "label": "รูปภาพหลัก", "type": "image" },
{ "slug": "tags", "label": "แท็ก", "type": "string", "searchable": true }
]
}
],
"menus": [
{
"name": "primary",
"label": "เมนูหลัก",
"items": [
{ "type": "custom", "label": "หน้าแรก", "url": "/" },
{ "type": "custom", "label": "สินค้าทั้งหมด", "url": "/all-products" },
{ "type": "custom", "label": "บทความ", "url": "/บทความ" },
{ "type": "custom", "label": "เกี่ยวกับเรา", "url": "/about-us" },
{ "type": "custom", "label": "ติดต่อเรา", "url": "/contact-us" }
]
}
],
"content": {
"blog": [
{
"id": "welcome-post",
"slug": "ยินดีต้อนรับ",
"status": "published",
"data": {
"title": "ยินดีต้อนรับสู่บล็อกดีล พลัส เทค",
"excerpt": "บทความแรกของดีล พลัส เทค พบกับความรู้และข้อมูลที่เป็นประโยชน์เกี่ยวกับระบบน้ำ ท่อ และอุปกรณ์ต่างๆ",
"featured_image": { "src": "/images/logo.png", "alt": "ดีล พลัส เทค" },
"body": [
{
"_type": "block",
"style": "normal",
"children": [
{ "_type": "span", "text": "ยินดีต้อนรับสู่บล็อกของดีล พลัส เทค! เราจะนำเสนอความรู้และข้อมูลที่เป็นประโยชน์เกี่ยวกับระบบน้ำ ท่อ และอุปกรณ์ต่างๆ ให้กับคุณ" }
]
},
{
"_type": "block",
"style": "normal",
"children": [
{ "_type": "span", "text": "ติดตามบทความใหม่ๆ ได้ที่นี่ หรือแวะมาพูดคุยกันที่ Line: @JPPSELECTION" }
]
}
],
"published_at": "2025-05-25T00:00:00Z",
"tags": "ข่าวสาร"
}
},
{
"id": "pipe-knowledge",
"slug": "ความรู้เรื่องท่อ-ppr",
"status": "published",
"data": {
"title": "ความรู้เกี่ยวกับท่อ PPR สำหรับระบบน้ำ",
"excerpt": "ท่อ PPR เป็นวัสดุที่นิยมใช้ในระบบประปาและการเดินท่อน้ำร้อน-น้ำเย็น เนื่องจากมีคุณสมบัติเด่นหลายประการ",
"featured_image": { "src": "/images/hdpe001-page1.jpg", "alt": "ท่อ PPR" },
"body": [
{
"_type": "block",
"style": "normal",
"children": [
{ "_type": "span", "text": "ท่อ PPR (Polypropylene Random Copolymer) เป็นท่อพลาสติกชนิดหนึ่งที่ได้รับความนิยมอย่างสูงในระบบประปา ท่อน้ำร้อน-น้ำเย็น และระบบทำความร้อนใต้พื้น เนื่องจากมีความทนทานต่อความร้อนและสารเคมีได้ดี" }
]
},
{
"_type": "block",
"style": "normal",
"children": [
{ "_type": "span", "text": "ท่อ PPR มีอายุการใช้งานยาวนานกว่า 50 ปี สามารถทนอุณหภูมิได้สูงถึง 95°C และทนแรงดันได้ดี นอกจากนี้ยังมีน้ำหนักเบา ติดตั้งง่าย และไม่เป็นสนิม ทำให้เป็นทางเลือกที่ดีสำหรับระบบท่อ Modern Plumbing." }
]
}
],
"published_at": "2025-05-24T00:00:00Z",
"tags": "ท่อ-ppr"
}
},
{
"id": "valve-guide",
"slug": "ประเภทของวาล์ว",
"status": "published",
"data": {
"title": "ประเภทของวาล์วที่ใช้ในระบบน้ำ",
"excerpt": "วาล์วเป็นอุปกรณ์สำคัญในระบบท่อที่ใช้ควบคุมการไหลของน้ำ เลือกใช้วาล์วให้ถูกประเภทช่วยให้ระบบทำงานได้อย่างมีประสิทธิภาพ",
"featured_image": { "src": "/images/valve-In01.jpg", "alt": "วาล์วน้ำ" },
"body": [
{
"_type": "block",
"style": "normal",
"children": [
{ "_type": "span", "text": "วาล์ว (Valve) เป็นอุปกรณ์ควบคุมการไหลของของเหลวและก๊าซในระบบท่อ มีหลายประเภทตามลักษณะการใช้งาน วาล์วแต่ละประเภทมีข้อดีและข้อเสียแตกต่างกัน" }
]
},
{
"_type": "block",
"style": "normal",
"children": [
{ "_type": "span", "text": "วาล์วประตู (Gate Valve) ใช้นิ้วเปิด-ปิดการไหลแบบเต็มที่ วาล์วปีกผีเสื้อ (Butterfly Valve) เหมาะกับระบบขนาดใหญ่ที่ต้องการควบคุมการไหลอย่างรวดเร็ว และวาล์วกันกลับ (Check Valve) ป้องกันน้ำไหลย้อนกลับในระบบ." }
]
},
{
"_type": "block",
"style": "normal",
"children": [
{ "_type": "span", "text": "ที่ดีล พลัส เทค เรามีวาล์วคุณภาพสูงให้เลือกหลากหลายประเภท พร้อมให้คำปรึกษาในการเลือกใช้วาล์วให้เหมาะสมกับระบบของคุณ" }
]
}
],
"published_at": "2025-05-23T00:00:00Z",
"tags": "วาล์ว"
}
}
]
}
}

15
src/content.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
excerpt: z.string(),
featured_image: z.string().optional(),
published_at: z.coerce.date(),
tags: z.array(z.string()).optional(),
}),
});
export const collections = { blog };

View File

@@ -0,0 +1,50 @@
---
title: "ความรู้เกี่ยวกับท่อ PPR สำหรับระบบน้ำ"
excerpt: "ท่อ PPR เป็นวัสดุที่นิยมใช้ในระบบประปาและการเดินท่อน้ำร้อน-น้ำเย็น เนื่องจากมีคุณสมบัติที่เหมาะสมและอายุการใช้งานยาวนาน"
featured_image: "/images/hdpe001-page1.jpg"
published_at: 2025-02-01
tags: ["ท่อ PPR", "ระบบน้ำ", "ความรู้"]
---
# ความรู้เกี่ยวกับท่อ PPR
**ท่อ PPR (Polypropylene Random Copolymer)** เป็นท่อพลาสติกคุณภาพสูงที่ได้รับความนิยมอย่างแพร่หลายในงานระบบประปา เนื่องจากมีคุณสมบัติที่เหมาะสมกับการใช้งานทั้งน้ำร้อนและน้ำเย็น
## คุณสมบัติเด่นของท่อ PPR
### 1. ทนความร้อนสูง
- ทนอุณหภูมิได้ถึง **95°C**
- เหมาะสำหรับงานน้ำร้อนและระบบทำความร้อน
### 2. อายุการใช้งานยาวนาน
- มากกว่า **50 ปี** เมื่อใช้งานตามมาตรฐาน
- ทนต่อการกัดกร่อนจากสารเคมี
### 3. Food Grade ปลอดภัย
- ไม่มีสารโลหะหนักปนเปื้อน
- เหมาะสำหรับระบบน้ำดื่ม
### 4. น้ำหนักเบา ติดตั้งง่าย
- เบากว่าท่อเหล็กหลายเท่า
- ประหยัดค่าแรงและเวลา
## มาตรฐานการผลิต
ท่อ PPR ที่มีคุณภาพต้องผ่านมาตรฐาน:
- **DIN 8077 / DIN 8078** (มาตรฐานเยอรมัน)
- **ISO 15874** (มาตรฐานสากล)
- **มอก.** สำหรับผลิตภัณฑ์ในประเทศไทย
## การเลือกขนาดท่อ PPR
| ขนาดท่อ (mm) | ขนาด (นิ้ว) | การใช้งาน |
|---|---|---|
| 20 | 1/2" | ท่อน้ำดื่มในบ้าน |
| 25 | 3/4" | ท่อน้ำทั่วไป |
| 32 | 1" | ท่อเมนเข้าบ้าน |
| 40-50 | 1.5"-2" | ท่อเมนอาคาร |
| 63+ | 2.5"+ | ท่อเมนโรงงาน |
## สรุป
ท่อ PPR เป็นตัวเลือกที่คุ้มค่าสำหรับระบบน้ำทั้งในบ้านและอาคาร ด้วยคุณสมบัติที่เหนือกว่าท่อโลหะในหลายด้าน และอายุการใช้งานที่ยาวนาน

View File

@@ -0,0 +1,60 @@
---
title: "ประเภทของวาล์วที่ใช้ในระบบน้ำ"
excerpt: "วาล์วเป็นอุปกรณ์สำคัญในระบบท่อที่ใช้ควบคุมการไหลของน้ำ เลือกใช้วาล์วให้ถูกประเภทตามการใช้งานจะช่วยยืดอายุและลดค่าใช้จ่ายในการบำรุงรักษา"
featured_image: "/images/valve-In01.jpg"
published_at: 2025-02-15
tags: ["วาล์ว", "ระบบน้ำ", "ความรู้"]
---
# ประเภทของวาล์วที่ใช้ในระบบน้ำ
**วาล์ว (Valve)** เป็นอุปกรณ์สำคัญที่สุดในระบบท่อ ใช้สำหรับควบคุมการไหล การเปิด-ปิด และการปรับแรงดันของน้ำ การเลือกวาล์วที่เหมาะสมกับการใช้งานเป็นสิ่งสำคัญมาก
## 1. Ball Valve (วาล์วบอล)
วาล์วที่ใช้ลูกบอลเจาะรูเปิด-ปิด เหมาะสำหรับ:
- ใช้งานทั่วไปในระบบประปา
- เปิด-ปิดเร็ว
- ปิดสนิท ไม่รั่วซึม
## 2. Gate Valve (วาล์วประตูน้ำ)
วาล์วที่ใช้ประตูเลื่อนขึ้น-ลง:
- เหมาะกับงานที่ต้องเปิด-ปิดเต็มที่
- แรงดันตกต่ำ
- ใช้ในท่อเมนขนาดใหญ่
## 3. Check Valve (วาล์วกันกลับ)
ป้องกันน้ำไหลย้อนกลับ:
- ใช้ในระบบปั๊มน้ำ
- ป้องกันน้ำท่วม
- ป้องกันการปนเปื้อน
## 4. Butterfly Valve (วาล์วผีเสื้อ)
วาล์วที่ใช้แผ่นโลหะหมุน:
- เหมาะกับท่อขนาดใหญ่
- น้ำหนักเบา
- ติดตั้งง่าย
## 5. Globe Valve (วาล์วโกลบ์)
วาล์วสำหรับปรับค่าการไหล:
- ควบคุมอัตราการไหลได้แม่นยำ
- ใช้ในระบบที่ต้องการความเที่ยงตรง
- แรงดันตกสูง
## การเลือกวาล์วที่เหมาะสม
| การใช้งาน | วาล์วแนะนำ |
|---|---|
| ท่อน้ำดื่มในบ้าน | Ball Valve |
| ท่อเมนอาคาร | Gate Valve |
| ระบบปั๊มน้ำ | Check Valve |
| ท่อขนาดใหญ่ | Butterfly Valve |
| ระบบควบคุมอัตราการไหล | Globe Valve |
## สรุป
การเลือกวาล์วที่ถูกต้องตามการใช้งานจะช่วยให้ระบบทำงานได้อย่างมีประสิทธิภาพ ลดปัญหาการรั่วซึม และยืดอายุการใช้งานของระบบทั้งหมด

View File

@@ -0,0 +1,36 @@
---
title: "ยินดีต้อนรับสู่บล็อกดีล พลัส เทค"
excerpt: "บทความแรกของดีล พลัส เทค พบกับความรู้และข้อมูลที่เป็นประโยชน์เกี่ยวกับระบบน้ำ ท่อ และอุปกรณ์ต่างๆ"
featured_image: "/images/logo.png"
published_at: 2025-01-15
tags: ["ทั่วไป", "แนะนำ"]
---
ยินดีต้อนรับสู่บล็อกของ **ดีล พลัส เทค** ครับ
ที่นี่คุณจะได้พบกับความรู้ คำแนะนำ และข้อมูลที่เป็นประโยชน์เกี่ยวกับ:
- ระบบน้ำประปา
- ท่อ PPR, HDPE, UPVC และท่อเหล็ก
- ปั๊มน้ำและอุปกรณ์
- วาล์วและข้อต่อ
- ฉนวนกันความร้อน
- ระบบดับเพลิง
## เกี่ยวกับดีล พลัส เทค
**บริษัท ดีล พลัส เทค จำกัด** เป็นผู้นำเข้าและจัดจำหน่ายสินค้าระบบน้ำคุณภาพสูงจากโรงงาน ด้วยประสบการณ์กว่า 10 ปี เราพร้อมให้คำปรึกษาและจัดส่งสินค้าถึงมือคุณ
### บริการของเรา
- **สินค้าระบบน้ำ** — ท่อ PPR ท่อ HDPE อุปกรณ์วาล์ว
- **อุปกรณ์ปรับอากาศ** — กริลแอร์ หัวจ่ายแอร์
- **ระบบรั้ว** — รั้วเทวดา ระบบรั้วไวน์แมน
### ติดต่อเรา
- **โทร:** 090-555-1415
- **LINE:** @JPPSELECTION
- **อีเมล:** dealplustech@gmail.com
หวังว่าบทความของเราจะเป็นประโยชน์กับท่านครับ

View File

@@ -1,6 +0,0 @@
import { defineLiveCollection } from 'astro:content'
import { emdashLoader } from 'emdash/runtime'
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
}

View File

@@ -1,13 +1,10 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import { getEmDashCollection } from 'emdash';
import { getCollection } from 'astro:content';
const { entries: articles, cacheHint } = await getEmDashCollection('blog', {
limit: 3,
orderBy: { published_at: 'desc' },
status: 'published',
});
Astro.cache.set(cacheHint);
const articles = (await getCollection('blog')).sort(
(a, b) => b.data.published_at.getTime() - a.data.published_at.getTime()
).slice(0, 3);
---
<BaseLayout title="ดีล พลัส เทค - ระบบน้ำคุณภาพสูง ราคาโรงงาน" description="ดีล พลัส เทค จำกัด ผู้นำด้านระบบน้ำคุณภาพสูง ราคาโรงงาน ท่อ PPR ท่อ HDPE อุปกรณ์วาล์ว ปั๊มน้ำ เครื่องเชื่อมท่อ และอุปกรณ์โรงงานคุณภาพ">
@@ -330,49 +327,43 @@ Astro.cache.set(cacheHint);
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{
articles.length > 0 ? articles.map(article => (
<a href={`/${encodeURI('บทความ')}/${encodeURIComponent(article.data.slug || article.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">
{
(() => {
const img = article.data.featured_image;
const imgSrc = typeof img === 'string' ? img : img?.src || (img?.provider === 'local' && (img?.meta?.storageKey || img?.id) ? `/_emdash/api/media/file/${img.meta?.storageKey || img.id}` : null);
const imgAlt = typeof img === 'object' && img?.alt ? img.alt : article.data.title;
return imgSrc ? (
<img src={imgSrc} alt={imgAlt} 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-16 h-16" 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-6">
<div class="flex items-center gap-3 text-sm text-slate-500 mb-3">
<time datetime={article.data.publishedAt}>{new Date(article.data.publishedAt).toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}</time>
{article.data.tags && (
<span class="px-2.5 py-0.5 bg-primary-50 text-primary-600 rounded-full text-xs font-medium">{article.data.tags}</span>
)}
{articles.length > 0 && articles.map(article => (
<a href={`/${encodeURI('บทความ')}/${encodeURIComponent(article.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">
{article.data.featured_image ? (
<img src={article.data.featured_image} alt={article.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-16 h-16" 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>
<h3 class="text-lg font-bold text-slate-900 group-hover:text-primary-600 transition-colors mb-2 line-clamp-2">{article.data.title}</h3>
<p class="text-sm text-slate-600 line-clamp-2">{article.data.excerpt}</p>
</div>
</a>
)) : (
<div class="md:col-span-2 lg:col-span-3 text-center py-16">
<div class="text-slate-300 mb-4">
<svg class="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/>
</svg>
</div>
<p class="text-lg text-slate-400">ยังไม่มีบทความในขณะนี้</p>
)}
</div>
)
}
<div class="p-6">
<div class="flex items-center gap-3 text-sm text-slate-500 mb-3">
<time datetime={article.data.published_at.toISOString().slice(0, 10)}>
{article.data.published_at.toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}
</time>
{article.data.tags?.[0] && (
<span class="px-2.5 py-0.5 bg-primary-50 text-primary-600 rounded-full text-xs font-medium">{article.data.tags[0]}</span>
)}
</div>
<h3 class="text-lg font-bold text-slate-900 group-hover:text-primary-600 transition-colors mb-2 line-clamp-2">{article.data.title}</h3>
<p class="text-sm text-slate-600 line-clamp-2">{article.data.excerpt}</p>
</div>
</a>
))}
{articles.length === 0 && (
<div class="md:col-span-2 lg:col-span-3 text-center py-16">
<div class="text-slate-300 mb-4">
<svg class="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/>
</svg>
</div>
<p class="text-lg text-slate-400">ยังไม่มีบทความในขณะนี้</p>
</div>
)}
</div>
<div class="text-center mt-10 sm:hidden">

View File

@@ -1,23 +1,31 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import { getEmDashEntry, getEmDashCollection } from 'emdash';
import { PortableText, Image } from 'emdash/ui';
import { getCollection, render } from 'astro:content';
const { slug } = Astro.params;
const { entry: article, cacheHint } = await getEmDashEntry('blog', slug);
Astro.cache.set(cacheHint);
if (!article) {
return Astro.redirect('/404');
export async function getStaticPaths() {
const articles = await getCollection('blog');
return articles.map(article => ({
params: { slug: article.id },
props: { article },
}));
}
// Get related articles (same tags, excluding current)
const { entries: relatedArticles } = await getEmDashCollection('blog', {
limit: 3,
orderBy: { published_at: 'desc' },
status: 'published',
});
const related = relatedArticles.filter(a => a.id !== article.id).slice(0, 3);
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] ?? '';
---
<BaseLayout title={`${article.data.title} - ดีล พลัส เทค`} description={article.data.excerpt || `บทความ ${article.data.title}`}>
@@ -39,49 +47,38 @@ const related = relatedArticles.filter(a => a.id !== article.id).slice(0, 3);
<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">
{article.data.tags && (
<span class="inline-block px-3 py-1 bg-primary-50 text-primary-600 rounded-full text-sm font-medium mb-4">{article.data.tags}</span>
{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 class="flex items-center gap-4 text-slate-500">
<time datetime={article.data.publishedAt} class="text-lg">
{new Date(article.data.publishedAt).toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}
<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 */}
{
(() => {
const img = article.data.featured_image;
const imgSrc = typeof img === 'string' ? img : img?.src || (img?.provider === 'local' && (img?.meta?.storageKey || img?.id) ? `/_emdash/api/media/file/${img.meta?.storageKey || img.id}` : null);
const imgAlt = typeof img === 'object' && img?.alt ? img.alt : article.data.title;
return imgSrc ? (
<div class="rounded-3xl overflow-hidden mb-12 shadow-lg">
<img src={imgSrc} alt={imgAlt} class="w-full h-auto" />
</div>
) : null;
})()
}
{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" />
</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.data.body ? (
<PortableText value={article.data.body} />
) : (
<p class="text-slate-500 italic">ไม่มีเนื้อหา</p>
)}
<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.data.slug || 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 24 10.314"/></svg>
<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.data.slug || 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">
<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>
@@ -97,24 +94,17 @@ const related = relatedArticles.filter(a => a.id !== article.id).slice(0, 3);
<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.data.slug || 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">
<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">
{
(() => {
const img = rel.data.featured_image;
const imgSrc = typeof img === 'string' ? img : img?.src || (img?.provider === 'local' && (img?.meta?.storageKey || img?.id) ? `/_emdash/api/media/file/${img.meta?.storageKey || img.id}` : null);
const imgAlt = typeof img === 'object' && img?.alt ? img.alt : rel.data.title;
return imgSrc ? (
<img src={imgSrc} alt={imgAlt} 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>
);
})()
}
{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>

View File

@@ -1,19 +1,10 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import { getEmDashCollection } from 'emdash';
import { getCollection } from 'astro:content';
const page = Astro.url.searchParams.get('page') || '1';
const limit = 9;
const offset = (parseInt(page) - 1) * limit;
const { entries: articles, nextCursor, cacheHint } = await getEmDashCollection('blog', {
limit,
orderBy: { published_at: 'desc' },
status: 'published',
});
Astro.cache.set(cacheHint);
const totalPages = nextCursor ? null : 1; // Simple pagination
const articles = (await getCollection('blog')).sort(
(a, b) => b.data.published_at.getTime() - a.data.published_at.getTime()
);
---
<BaseLayout title="บทความทั้งหมด - ดีล พลัส เทค" description="รวมบทความและความรู้เกี่ยวกับระบบน้ำ ท่อ และอุปกรณ์ต่างๆ จากดีล พลัส เทค">
@@ -32,54 +23,47 @@ const totalPages = nextCursor ? null : 1; // Simple pagination
<!-- Articles Grid -->
<section class="py-16 lg:py-24">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{
articles.length > 0 ? (
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{articles.map(article => (
<a href={`/${encodeURI('บทความ')}/${encodeURIComponent(article.data.slug || article.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">
{
(() => {
const img = article.data.featured_image;
const imgSrc = typeof img === 'string' ? img : img?.src || (img?.provider === 'local' && (img?.meta?.storageKey || img?.id) ? `/_emdash/api/media/file/${img.meta?.storageKey || img.id}` : null);
const imgAlt = typeof img === 'object' && img?.alt ? img.alt : article.data.title;
return imgSrc ? (
<img src={imgSrc} alt={imgAlt} 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-16 h-16" 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-6">
<div class="flex items-center gap-3 text-sm text-slate-500 mb-3">
<time datetime={article.data.publishedAt}>{new Date(article.data.publishedAt).toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}</time>
{article.data.tags && (
<span class="px-2.5 py-0.5 bg-primary-50 text-primary-600 rounded-full text-xs font-medium">{article.data.tags}</span>
)}
{articles.length > 0 ? (
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{articles.map(article => (
<a href={`/${encodeURI('บทความ')}/${encodeURIComponent(article.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">
{article.data.featured_image ? (
<img src={article.data.featured_image} alt={article.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-16 h-16" 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>
<h2 class="text-lg font-bold text-slate-900 group-hover:text-primary-600 transition-colors mb-2 line-clamp-2">{article.data.title}</h2>
<p class="text-sm text-slate-600 line-clamp-2">{article.data.excerpt}</p>
)}
</div>
<div class="p-6">
<div class="flex items-center gap-3 text-sm text-slate-500 mb-3">
<time datetime={article.data.published_at.toISOString().slice(0, 10)}>
{article.data.published_at.toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}
</time>
{article.data.tags?.[0] && (
<span class="px-2.5 py-0.5 bg-primary-50 text-primary-600 rounded-full text-xs font-medium">{article.data.tags[0]}</span>
)}
</div>
</a>
))}
<h2 class="text-lg font-bold text-slate-900 group-hover:text-primary-600 transition-colors mb-2 line-clamp-2">{article.data.title}</h2>
<p class="text-sm text-slate-600 line-clamp-2">{article.data.excerpt}</p>
</div>
</a>
))}
</div>
) : (
<div class="text-center py-16">
<div class="text-slate-300 mb-4">
<svg class="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/>
</svg>
</div>
) : (
<div class="text-center py-16">
<div class="text-slate-300 mb-4">
<svg class="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/>
</svg>
</div>
<p class="text-xl text-slate-400 mb-2">ยังไม่มีบทความ</p>
<p class="text-slate-400">กลับมาตรวจสอบอีกครั้งในภายหลัง</p>
</div>
)
}
<p class="text-xl text-slate-400 mb-2">ยังไม่มีบทความ</p>
<p class="text-slate-400">กลับมาตรวจสอบอีกครั้งในภายหลัง</p>
</div>
)}
</div>
</section>
</main>