การสร้างบล็อกแบบกำหนดเองตั้งแต่เริ่มต้นในปี 2026 คือการตัดสินใจที่ผ่านการคิดมาอย่างดี นักเขียนส่วนใหญ่แค่เลือกแพลตฟอร์มแบบโฮสต์สำเร็จรูปแล้วก็ใช้งานต่อ แต่ผมตัดสินใจสร้างบล็อกสายเทคของผมใหม่ด้วย Astro เพราะผมต้องการควบคุมทุกเลเยอร์ของ stack และต้องการเรียนรู้รูปแบบ (patterns) ที่ทำให้ static site เร็วขึ้นจริงๆ ไม่ใช่แค่ทำให้มันแตกต่าง ผลลัพธ์ที่ได้คือเว็บไซต์สองภาษาที่ให้บริการเนื้อหาทั้งภาษาญี่ปุ่นและภาษาอังกฤษ โดยไม่มีการส่ง JavaScript ไปยังเบราว์เซอร์เลยโดยค่าเริ่มต้น (zero JavaScript by default) และเก็บความซับซ้อนทั้งหมดไว้ที่เครื่องของผม แทนที่จะไปเป็นภาระให้กับเบราว์เซอร์ของผู้เข้าชม
และนี่คือ 5 รูปแบบที่ทำให้มันใช้งานได้จริง
Content Collections ด้วย Zod
Content Collections ของ Astro ทำได้มากกว่าแค่การจัดระเบียบไฟล์ Markdown ลงในโฟลเดอร์ แต่มันช่วยบังคับใช้ข้อตกลง (contract) ระหว่างเนื้อหากับโค้ดของคุณ ผมได้แนบ Zod schema เข้ากับทุก article collection ซึ่งหมายความว่าในขั้นตอนการ build จะมีการตรวจสอบความถูกต้องของ frontmatter ก่อนที่หน้าเว็บจะถูกเรนเดอร์ออกมาแม้แต่หน้าเดียว
Schema นี้กำหนดให้ต้องมีฟิลด์ language ซึ่งรับค่าได้เพียงสองค่าเท่านั้นคือ ja หรือ en ทำให้ไม่มีความคลุมเครือว่าผู้อ่านจะได้อ่านภาษาอะไร นอกจากนี้ผมยังเพิ่มฟิลด์ pair เพื่อเชื่อมโยงบทความที่แปลกันไว้ด้วยกัน หากผมเผยแพร่โพสต์เกี่ยวกับ Astro เป็นภาษาญี่ปุ่นและแปลเป็นภาษาอังกฤษในภายหลัง ทั้งสองไฟล์จะมี pair ID เดียวกัน สิ่งนี้ทำให้การสร้างตัวสลับภาษา (language switcher) เป็นเรื่องง่ายมาก เพราะความสัมพันธ์ถูกระบุไว้อย่างชัดเจนในข้อมูล ไม่ใช่การคาดเดาจากชื่อไฟล์
วันที่ที่มาจาก Markdown frontmatter จะมาในรูปแบบ string ดังนั้น schema จึงทำการแปลง (coerce) ให้เป็น object Date จริงๆ โดยอัตโนมัติ ซึ่งช่วยกำจัดตรรกะการจัดการ string ที่ยุ่งเหยิงภายใน page templates ของผม แต่ประโยชน์ที่สำคัญที่สุดคือรูปแบบการแจ้งข้อผิดพลาด (failure mode) หากไฟล์ใดขาดฟิลด์ที่จำเป็นหรือใช้รหัสภาษาที่ไม่ถูกต้อง การ build จะหยุดทำงานทันทีพร้อมแจ้ง error ที่ชัดเจน ทำให้ผมสามารถแก้ไขได้ใน terminal แทนที่จะต้องไปพบ layout ที่พังหรือเจอหน้า 404 เงียบๆ หลังจาก deploy ไปแล้ว
กระบวนการแปลงบทความ (Article Conversion Pipeline)
ผมไม่ได้เขียนทุกโพสต์ในรูปแบบใหม่นี้ตั้งแต่วันแรก เนื้อหาหลายปีที่ผ่านมาถูกเก็บไว้บน Zenn และ Dev.to ซึ่งแต่ละแพลตฟอร์มก็มีเอกลักษณ์และ syntax เฉพาะตัว แทนที่จะใช้วิธี copy-paste แล้วมานั่งแก้ด้วยมือ ผมจึงเขียนสคริปต์ TypeScript ขึ้นมาเพื่อแปลงบทความทั้งชุดให้กลายเป็น Markdown มาตรฐาน
Zenn ใช้ syntax สำหรับ callout แบบกำหนดเองเพื่อแสดงคำแนะนำ (tips) และคำเตือน (warnings) สคริปต์ของผมจะเปลี่ยนสิ่งเหล่านั้นให้เป็นแท็ก HTML aside ที่มีความหมายเชิงโครงสร้าง (semantic) เพื่อให้การแสดงผลสม่ำเสมอกันทั้งเว็บไซต์ ส่วน Dev.to จะใช้ Liquid tags สำหรับการฝังเนื้อหา (embeds) และบล็อกพิเศษต่างๆ กระบวนการนี้จะแปลสิ่งเหล่านั้นให้กลายเป็นลิงก์ Markdown ธรรมดาที่ใช้งานได้ทุกที่
บางโพสต์มีเนื้อหาเสริม (asides) ที่ตั้งใจให้ใช้เฉพาะบนแพลตฟอร์มเดิมเท่านั้น เช่น คำปฏิเสธความรับผิดชอบเกี่ยวกับ paywall ของ Medium หรือเส้นทางรูปภาพเฉพาะของ Zenn ผมจึงครอบสิ่งเหล่านั้นด้วย HTML comments เพื่อให้ตัวแปลงสามารถลบออกได้ในระหว่างการย้ายข้อมูล สคริปต์จะประมวลผลไฟล์ทีละบรรทัด แต่จะเคารพขอบเขตของโค้ด เมื่อตรวจพบ fenced code block มันจะข้ามกฎการแปลงทั้งหมดไปทันที เพราะการทำให้ตัวอย่าง syntax เสียหายจะทำลายจุดประสงค์ทั้งหมดของบล็อกสายเทค ดังนั้น parser แบบบรรทัดต่อบรรทัดจึงปฏิบัติกับ code blocks เสมือนเป็นเขตหวงห้ามที่ไม่สามารถแตะต้องได้
เพียงแค่รันคำสั่งเดียว ตอนนี้ผมก็สามารถเผยแพร่ผลงานการเขียนหลายปีได้ใหม่ โดยไม่ทำให้ลิงก์หรือ callout ใดๆ เสียหายเลย
การสร้างรูปภาพ OGP ในช่วง Build-Time
รูปภาพสำหรับแชร์ลงโซเชียล (Social sharing images) มักจะเป็นสิ่งที่ถูกนึกถึงเป็นลำดับท้ายๆ คุณไม่ว่าจะออกแบบเองด้วยมือ หรือติดตั้งบริการ runtime ที่หนักเครื่องเพื่อสร้างการ์ดตามคำขอ แต่ผมไม่ต้องการทั้งสองอย่าง รูปภาพ Open Graph ทุกรูปบนไซต์นี้ถูกสร้างขึ้นในระหว่างการ build เพื่อให้ผู้เข้าชมได้รับเพียงแท็ก img น้ำหนักเบาที่ชี้ไปยังไฟล์ PNG แบบ static เท่านั้น
ผมใช้ Satori ซึ่งรับ JSX markup แล้วเรนเดอร์ออกมาเป็น SVG ผลลัพธ์ที่ได้นั้นคมชัด คาดเดาได้ และง่ายต่อการทำ template การปรับแต่ง (optimization) ที่แท้จริงมาจากเรื่องการจัดการฟอนต์ ฟอนต์เว็บภาษาญี่ปุ่นแบบเต็มรูปแบบอาจมีขนาดเกิน 5 MB ได้อย่างง่ายดาย การโหลดสิ่งนั้นในระหว่างการ build หรือยิ่งไปกว่านั้นคือการสั่งให้เบราว์เซอร์ไปดึงข้อมูลมานั้นเป็นเรื่องที่ไร้สาระมาก
แทนที่จะทำแบบนั้น ผมใช้การทำ Google Fonts subsetting สคริปต์จะตรวจสอบข้อความหัวข้อของโพสต์นั้นๆ และร้องขอเฉพาะชุด glyph ที่จำเป็นต้องใช้ในการเรนเดอร์ข้อความนั้นจริงๆ หากหัวข้อใช้ตัวอักษรญี่ปุ่นที่ไม่ซ้ำกัน 40 ตัว ก็จะมีเพียง 40 ตัวนั้นเท่านั้นที่ถูกส่งผ่านเครือข่าย การ build จึงยังคงรวดเร็ว และรูปภาพที่เรนเดอร์ออกมาก็จะไม่แสดงบล็อกสี่เหลี่ยม (tofu blocks) ที่แสดงถึงตัวอักษรที่อ่านไม่ได้ เพราะ subset นั้นมีความแม่นยำสูง ไม่มีการปล่อยให้เป็นเรื่องของความบังเอิญในขณะ runtime
Dark Mode ผ่าน Tailwind Tokens
ผมปฏิเสธที่จะตกแต่งทุกองค์ประกอบด้วย utility classes แบบ dark: เพราะแนวทางนั้นขยายขนาดได้ยาก (scales poorly) และทำให้ markup ของคุณเต็มไปด้วยขยะ (noise) ผมจึงกำหนดค่า color tokens ใหม่ เพื่อให้ชื่อ class เดียวกันสามารถแสดงค่าที่แตกต่างกันได้ขึ้นอยู่กับธีมที่ใช้งานอยู่
ผมใช้ CSS custom properties สำหรับสีพื้นผิวและสีข้อความทั้งหมด ในโหมดสว่าง (light mode) --color-white จะแมปไปยัง #ffffff ส่วนในโหมดมืด (dark mode) ชื่อตัวแปรเดียวกันนี้จะชี้ไปยังค่าที่เกือบเป็นสีดำ โครงสร้าง HTML ของผมจึงไม่ขึ้นกับโหมดใดโหมดหนึ่งเลย การ์ดหนึ่งใบสามารถใช้ bg-ui-surface และ text-ui-primary ได้โดยไม่ต้องสนใจว่าเป็นเวลาไหนของวัน การสลับธีมจะเปลี่ยนการกำหนดค่าตัวแปรที่ root และอินเทอร์เฟซทั้งหมดจะตอบสนองในทันที
ความเสี่ยงเพียงอย่างเดียวของแนวทางนี้คือการเกิดแสงวาบของเนื้อหาที่เป็นสีสว่าง (flash of light content) ก่อนที่ stylesheet จะโหลดเสร็จ ผมแก้ปัญหานี้ด้วย inline script ขนาดเล็กใน head ของเอกสาร มันจะทำงานก่อนการวาดหน้าจอครั้งแรก (first paint) โดยจะตรวจสอบ localStorage และการตั้งค่าของระบบ (system preference) จากนั้นจึงกำหนด data attribute ที่ถูกต้องในทันที เนื่องจากสคริปต์นี้ขัดขวางการเรนเดอร์เพียงไม่กี่มิลลิวินาที ผู้เข้าชมจึงไม่เห็นแสงสีขาวที่แสบตา (jarring white burst) ก่อนที่โหมดมืดจะเริ่มทำงาน
Island Architecture และ Zero-JS
หลักการสำคัญของ Astro คือหน้าเว็บควรเริ่มต้นจากการเป็น static HTML และ JavaScript จะเข้ามาทำงานก็ต่อเมื่อมีความจำเป็นต้องมีการโต้ตอบ (interaction) จริงๆ เท่านั้น ซึ่งผมให้ความสำคัญกับเรื่องนี้อย่างมาก
ผมหลีกเลี่ยงการใช้ React สำหรับเมนูหลัก (global menu) และปุ่มสลับธีม (theme toggle) โดยทั้งสองส่วนนี้จัดการด้วย vanilla JavaScript จำนวนเล็กน้อยที่รวมอยู่ในโมดูลเดียว จึงไม่มีภาระเรื่อง hydration, ไม่มีการทำ virtual DOM diffing และไม่ต้องดาวน์โหลด framework runtime
ไลบรารีหนักๆ เพียงตัวเดียวที่ผมใช้คือ Mermaid.js สำหรับการเรนเดอร์แผนภาพจากข้อความ แทนที่จะนำเข้า (import) แบบ global ผมเลือกที่จะห่อหุ้มมันไว้ภายใน Intersection Observer โดยตัว observer จะคอยเฝ้าดูคอนเทนเนอร์ของแผนภาพ เมื่อผู้ใช้เลื่อนหน้าจอมาอยู่ในระยะไม่กี่ร้อยพิกเซลจากแผนภาพ สคริปต์จะทำการฉีด (inject) โมดูล Mermaid เข้าไปแบบไดนามิกและเรนเดอร์แผนภาพนั้นออกมา หากโพสต์นั้นไม่มีแผนภาพ ไลบรารีดังกล่าวก็จะไม่ถูกเรียกใช้งานผ่านเครือข่ายเลย การโหลดหน้าเว็บในตอนแรกจึงยังคงเบา และเบราว์เซอร์จะเสียทรัพยากรเฉพาะกับสิ่งที่ผู้อ่านเห็นจริงๆ เท่านั้น
แนวคิดแบบ Build-First
แกนหลักที่แทรกซึมอยู่ในทุกรูปแบบคือเรื่องที่ตรงไปตรงมา: หากคุณสามารถจัดการงานต่างๆ ได้ในขั้นตอนการ build ก็จงทำที่นั่น ตรวจสอบความถูกต้องของข้อมูล (validate) ด้วย Zod ก่อนที่จะ deploy เว็บไซต์, แปลงไวยากรณ์เฉพาะของแพลตฟอร์ม (proprietary platform syntax) ไว้ล่วงหน้าแทนที่จะทำในขณะที่มีการร้องขอ (request time), เรนเดอร์รูปภาพสำหรับโซเชียลเป็นไฟล์ static แทนที่จะต้องรันเซิร์ฟเวอร์, จัดการสีของธีมผ่าน tokens แทนที่จะส่ง logic ไปยัง client ทุกราย และชะลอการโหลด JavaScript หนักๆ ไว้จนกว่าผู้ใช้จะต้องการใช้งานจริงๆ
การผลักความซับซ้อนไปทางซ้าย (pushing complexity leftward) เข้าสู่ขั้นตอนการ build ช่วยให้ runtime คาดเดาได้, ขนาด payload เล็ก และภาระในการบำรุงรักษาอยู่ในระดับที่จัดการได้ เว็บไซต์ยังคงทำงานได้รวดเร็ว ไม่ใช่เพราะเทคนิคใดเทคนิคหนึ่งเพียงอย่างเดียว แต่เป็นเพราะมีการทำงานเกิดขึ้นภายในเบราว์เซอร์ของผู้เข้าชมน้อยลงนั่นเอง นี่คือผลตอบแทนที่แท้จริงของการเลือกใช้ static architecture ในปี 2026
