# ออกแบบ Blog Platform ด้วย React Router v7 + Cloudflare Stack
## บทนำ
บทความนี้บันทึกกระบวนการออกแบบและตัดสินใจเลือก technology สำหรับ blog ส่วนตัวที่ thana.in.th ตั้งแต่ต้นจนสามารถ deploy ขึ้น production ได้จริง โดยเน้นที่ความเรียบง่าย ต้นทุนต่ำ และประสิทธิภาพสูง
---
## โจทย์และข้อกำหนด
เริ่มต้นจากโจทย์ง่ายๆ คือต้องการ blog ส่วนตัวที่ใช้งานได้จริงในระยะยาว โดยมีข้อกำหนดหลักดังนี้
- **Single author** — เขียนคนเดียว ไม่ต้องการระบบ user หลายคน
- **Rich text editor** — เขียนบทความได้สะดวก มี formatting ครบ
- **Image upload** — อัปโหลดรูปได้ หรือจะใส่ URL จากแหล่งอื่นก็ได้
- **Full-text search** — ค้นหาบทความได้จากเนื้อหาจริง
- **Dark mode** — Design สวยงาม อ่านสบายตา
- **ต้นทุนต่ำ** — ใช้ Cloudflare free tier ให้มากที่สุด
---
## การเลือก Tech Stack
### Framework: React Router v7
เดิมวางแผนจะใช้ **Remix** แต่พบว่า Remix v2 ถูก upstream เข้าไปใน React Router v7 แล้ว จึงเลือก React Router v7 แทน ซึ่งยังคง architecture เดิมของ Remix ไว้ครบถ้วน ได้แก่
- **Loader/Action pattern** — โหลดข้อมูลและจัดการ form ฝั่ง server ได้โดยตรง
- **Nested routes** — จัดการ layout ซ้อนกันได้ดี เหมาะกับ admin section
- **SSR by default** — render ฝั่ง server ทำให้ SEO ดีและโหลดเร็ว
### Database: Cloudflare D1
D1 คือ SQLite ที่รันบน Cloudflare edge network ข้อดีหลักคือ
- **ไม่มีค่าใช้จ่าย** สำหรับ blog ส่วนตัวที่มี traffic ปกติ (free tier: 5M rows read/day)
- **Query ง่าย** — ใช้ SQL ตรงๆ ไม่ต้องเรียน ORM ใหม่
- **FTS5** — SQLite รองรับ Full-text search ในตัว ไม่ต้องใช้ service แยก
```sql
-- Full-text search ด้วย SQLite FTS5
CREATE VIRTUAL TABLE posts_fts USING fts5(
title, excerpt, content,
content=posts, content_rowid=id
);
```
สิ่งที่น่าสนใจคือการใช้ **trigger** เพื่อ sync ข้อมูลระหว่าง `posts` table กับ `posts_fts` virtual table อัตโนมัติ ทำให้ไม่ต้องจัดการ index ด้วยตัวเอง
### Storage: Cloudflare R2
สำหรับเก็บรูปภาพ เลือก R2 เพราะ
- **ไม่มีค่า egress** — ไม่เสียค่าดึงรูปออกมา ต่างจาก S3
- **Free tier** 10GB ต่อเดือน เพียงพอมากสำหรับ blog
- Integrate กับ Cloudflare Workers/Pages ได้โดยตรงผ่าน binding
### Authentication: Google OAuth
เลือก Google OAuth แบบ custom implementation (ไม่ใช้ library) เพราะ
- มี user คนเดียว ไม่ต้องการ library ขนาดใหญ่
- Flow ง่ายมาก: `redirect → callback → get profile → set cookie`
- ล็อก email ที่อนุญาตไว้ใน code โดยตรง
```
GET /auth/google → redirect ไป Google
GET /auth/google/callback → รับ code, แลก token, ดึง profile, เซ็ต cookie
POST /auth/logout → ลบ session
```
Session เก็บใน **signed cookie** ผ่าน `createCookieSessionStorage` ของ React Router ซึ่งปลอดภัยและไม่ต้องการ database เพิ่ม
---
## Database Schema
Schema ออกแบบให้เรียบง่าย normalize พอดี
```
categories (id, name, slug)
↓ 1:N
posts (id, slug, title, excerpt, content, featured_image, category_id, status, published_at)
↓ N:M
post_tags → tags (id, name, slug)
```
จุดที่ต้องระวังคือ `status` field ใช้ `CHECK constraint` เพื่อป้องกัน invalid value
```sql
status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft', 'published'))
```
---
## Rich Text Editor: TipTap
TipTap ถูกเลือกเพราะ
- **Extension-based** — เพิ่ม/ลด feature ได้ตามต้องการ
- **Output เป็น HTML** — เก็บใน D1 ง่าย render ในหน้าอ่านได้โดยตรง
- **React integration** ดีมาก ใช้ `useEditor` hook ได้สะดวก
สิ่งที่ต้องระวังคือ TipTap เป็น client-side only จึงต้อง lazy load ด้วย `React.lazy` เพื่อป้องกัน SSR error
```tsx
const RichEditor = lazy(() => import("./RichEditor"));
// ใน PostForm
<Suspense fallback={<div>Loading editor...</div>}>
<RichEditor content={content} onChange={setContent} />
</Suspense>
```
---
## Design System
ใช้ **Academic Minimalist Dark** theme ที่ออกแบบมาเพื่อการอ่านในที่มืด ระบบสีหลักมี 3 กลุ่ม
| กลุ่ม | สี | ใช้งาน |
|-------|-----|--------|
| Primary | `#adc7ff` (blue) | CTA, link, active state |
| Secondary | `#ffb68a` (orange) | Badge, notification |
| Surface | `#10131a` → `#32353c` | Background layers |
Tailwind CSS v4 รองรับ CSS custom properties ตรงๆ ผ่าน `@theme` directive ทำให้ define design token ได้สะอาดกว่าเดิม
```css
@theme {
--color-primary: #adc7ff;
--color-background: #10131a;
--color-surface-container: #1d2027;
}
```
---
## Deployment Architecture
```
Browser → Cloudflare Edge
↓
Pages Function ([[path]].ts)
↓
React Router Handler
↓
D1 Database / R2 Bucket
```
Cloudflare Pages ใช้ **Pages Functions** เป็นตัวรับ request ทุก path (`[[path]].ts`) แล้วส่งต่อให้ React Router จัดการ การออกแบบแบบนี้ทำให้ทุก route รัน on-demand ใกล้ผู้ใช้ที่สุด (edge computing)
---
## บทเรียนที่ได้
1. **Remix กลายเป็น React Router v7** — ต้องระวังเรื่อง template และ adapter ที่เปลี่ยนไป `cloudflareDevProxyVitePlugin` ไม่มีใน version ใหม่แล้ว
2. **D1 FTS5 ทรงพลังมาก** — ไม่จำเป็นต้องใช้ Algolia หรือ Elasticsearch สำหรับ blog ขนาดเล็ก-กลาง
3. **Cookie session เพียงพอ** — สำหรับ single-author blog ไม่จำเป็นต้องเก็บ session ใน database
4. **Lazy load editor** — TipTap และ library ขนาดใหญ่อื่นๆ ควร lazy load เสมอ ลด initial bundle size
---
## สรุป
Stack นี้เหมาะมากสำหรับ personal blog หรือ small team ที่ต้องการ
- ✅ ต้นทุนต่ำมาก (Cloudflare free tier ครอบคลุมแทบทั้งหมด)
- ✅ Performance ดี (edge computing, SQLite fast read)
- ✅ Developer experience ดี (TypeScript ตลอด, hot reload)
- ✅ Deploy ง่าย (push code → auto deploy)
โค้ดทั้งหมดอยู่ที่ `thana-blog/` และสามารถ deploy ด้วย `npm run deploy` คำสั่งเดียว
# Tech Stack (recap)
| Layer | Technology |
|-------|-----------|
| Framework | React Router v7 (Cloudflare Pages adapter) |
| Database | Cloudflare D1 (SQLite) |
| Storage | Cloudflare R2 (image uploads) |
| Auth | Google OAuth (custom implementation) |
| Editor | TipTap (WYSIWYG rich text) |
| Styling | Tailwind CSS |
| Deployment | Cloudflare Pages |
| Domain | thana.in.th (DNS on Cloudflare) |Technology24 เมษายน 2569
สร้างเว็บ thana.in.th ด้วย Claude Code ภายใน 10 นาที
