สร้างเว็บ thana.in.th ด้วย Claude Code ภายใน 10 นาที


บทความนี้บันทึกกระบวนการออกแบบและตัดสินใจเลือก technology สำหรับ blog ส่วนตัวที่ thana.in.th ตั้งแต่ต้นจนสามารถ deploy ขึ้น production ได้จริง โดยเน้นที่ความเรียบง่าย ต้นทุนต่ำ และประสิทธิภาพสูง
เริ่มต้นจากโจทย์ง่ายๆ คือต้องการ blog ส่วนตัวที่ใช้งานได้จริงในระยะยาว โดยมีข้อกำหนดหลักดังนี้
เดิมวางแผนจะใช้ Remix แต่พบว่า Remix v2 ถูก upstream เข้าไปใน React Router v7 แล้ว จึงเลือก React Router v7 แทน ซึ่งยังคง architecture เดิมของ Remix ไว้ครบถ้วน ได้แก่
D1 คือ SQLite ที่รันบน Cloudflare edge network ข้อดีหลักคือ
-- 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 ด้วยตัวเอง
สำหรับเก็บรูปภาพ เลือก R2 เพราะ
เลือก Google OAuth แบบ custom implementation (ไม่ใช้ library) เพราะ
redirect → callback → get profile → set cookieGET /auth/google → redirect ไป Google
GET /auth/google/callback → รับ code, แลก token, ดึง profile, เซ็ต cookie
POST /auth/logout → ลบ session
Session เก็บใน signed cookie ผ่าน createCookieSessionStorage ของ React Router ซึ่งปลอดภัยและไม่ต้องการ database เพิ่ม
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
status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft', 'published'))
เริ่มต้นเลือก TipTap (WYSIWYG) แต่ภายหลังเปลี่ยนมาใช้ Markdown editor แทน เพราะ
data-color-mode="dark" ได้ทันทีconst RichEditor = lazy(() => import("./RichEditor"));
// ใน PostForm
<Suspense fallback={<div>Loading editor...</div>}>
<RichEditor content={content} onChange={setContent} />
</Suspense>
ใช้ marked library ไม่ได้ใน Cloudflare Workers environment เพราะ v18 มีปัญหา async/sync — marked.parse() อาจ return Promise แทน string บน edge runtime
วิธีแก้ — ใช้ @uiw/react-markdown-preview render ฝั่ง client แทน
const MarkdownPreview = lazy(() => import("@uiw/react-markdown-preview"));
<Suspense fallback={<div>Loading...</div>}>
<MarkdownPreview
source={post.content ?? ""}
data-color-mode="dark"
/>
</Suspense>
library นี้ install มาพร้อมกับ @uiw/react-md-editor อยู่แล้ว ไม่ต้องติดตั้งเพิ่มค่ะ
ใช้ 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 ได้สะอาดกว่าเดิม
@theme {
--color-primary: #adc7ff;
--color-background: #10131a;
--color-surface-container: #1d2027;
}
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)
cloudflareDevProxyVitePlugin ไม่มีใน version ใหม่แล้วmarked ใน CF Workers — ใช้ @uiw/react-markdown-preview render client-side แทน เพื่อหลีกเลี่ยงปัญหา async/sync บน edge runtime--branch production ใน deploy script และผูก custom domain ไว้กับ branch นั้นใน Cloudflare Pages dashboardStack นี้เหมาะมากสำหรับ personal blog หรือ small team ที่ต้องการ
โค้ดทั้งหมดอยู่ที่ thana-blog/ และสามารถ deploy ด้วย npm run deploy คำสั่งเดียว
| Layer | Technology |
|---|---|
| Framework | React Router v7 (Remix architecture) |
| Adapter | @react-router/cloudflare + Cloudflare Pages Functions |
| Database | Cloudflare D1 (SQLite) — thana-blog-db |
| Storage | Cloudflare R2 — thana-blog-images |
| Auth | Google OAuth (single author: thana.j@gmail.com) |
| Editor | @uiw/react-md-editor v4 (Markdown) + @uiw/react-markdown-preview (render) |
| Styling | Tailwind CSS v4 — Academic Minimalist Dark theme |
| Deployment | Cloudflare Pages |
| DNS | Cloudflare (domain: thana.in.th) |