บันทึกการการอัพเดท Blog v3 เปลี่ยนไปใช้ Gatsby + MDX และ Theme UI

Published on
Gatsby
2020/07/how-to-migrate-md-to-mdx-in-gatsby
Discord

Ahoy! สวัสดีครับ

วันนี้ผมจะมาเขียนบล็อกแชร์ประสบการณ์ และบันทึกการอัพเดท Blog ของผมกันนะครับ (หลายๆคนคงมีคำถามในใจ เปลี่ยนอีกแล้วหรอ?)

จริงๆ ไม่ได้ปรับแก้อะไรมากมาย ยังคงใช้ Gatsby เหมือนเดิม แต่ก็แอบอยากให้ Next.js เหมือนกัน (เดี๋ยววันหลังมาเขียนเปรียบเทียบ จากตัวผมเองดีกว่า ว่าข้อดีข้อเสียมีอะไรบ้างระหว่าง Gatsby vs Next.js เฉพาะทำ personal blog นะ ไม่รวมไปทำเว็บอื่นๆ )

สิ่งที่ผมต้องการจะทำคือ อยากเปลี่ยนจาก Markdown ธรรมดา เป็นใช้ MDX เพื่ออยากแทรกโค๊ด หรือ Component ลงไปใน Markdown ง่ายๆ ปัจจุบัน มีบาง Component ที่ใช้วิธี ก็อปปี้วาง แต่ละไฟล์ ซึ่งเริ่มไม่ค่อยสะดวก แก้ทีลำบากเลย หลัก เช่นๆ กล่อง Info หรือ Button

พอคิดว่าจะเปลี่ยน MDX แล้ว ไปๆ มาก็ไหนๆ ก็ไหนๆ Refactoring code เก่าๆ ด้วยเลย รื้อ css หมดเลยละกัน (เคยคิดไว้นานแล้ว แต่ทุกครั้งไม่กล้าแตะ เพราะมันเยอะมาก และใช้เวลา)

ก็ได้ความว่า ไหนๆ ใช้ MDX แล้ว ใช้พวก Emotion หรือ Styled Component เลยดีกว่า สรุป ก็เลยมาจบที่ Theme UI ครับ

Getting Started

เริ่มต้น ผมก็วางแผนแล้วว่าจะใช้อะไรบ้าง หลังจากนั่ง Research นั่งดูตัวอย่าง นั่งดู Doc สรุปมาได้แบบนี้

  • Gatsby - Static Site Generator ไม่เปลี่ยน (แม้จะอยากลองใช้ Next.js ก็ตาม ไม่อยากรอ build ตอน process images )
  • MDX - แน่นอน จุดประสงค์หลักเลย และโชคดี Gatsby ก็มี gatsby-plugin-mdx ให้ใช้
  • Theme UI - เอามาแทน css เดิม ข้อดีคือเอามาทำ Design System ตัวเองได้ และก็มี gatsby-plugin-theme-ui ให้ใช้
  • Digital Ocean (Referral Link)- ผมเลือกมาทำ self host เองดีกว่า (เผื่อหัด config server เองด้วย)
  • NGINX - เมื่อ host เองแล้ว ตัว web server ก็เลือก NGINX ครับ serve static files ไป แล้วก็ปรับพวก config อะไรเอาเอง
  • Cloudflare - เอาไว้เป็น DNS + Proxy และ CDN เพื่อ cache ด้วย ก่อนหน้านี้ใช้ Netlify มี CDN/DNS ในตัว เลยไม่ได้ใช้ Cloudflare

เพราะเนื่องจากว่าปัจจุบันใช้ Gatsby Cloud + Netlify ข้อดีคือสะดวกสบาย แต่ข้อเสียคือ deploy นาน เพราะรอ Gatsby Cloud build เสร็จ แล้ว upload ไฟล์ build มา Netlify ตอนอัพโหลดรอครึ่งชัว่โมง และอีกข้อสำคัญเลย คือ Netlify ให้ bandwidth มาแค่ 100GB ต่อเดือน ซึ่งมันไม่พอ เพราะแชร์หลาย app เอาเป็นว่าไว้ทำแอพเล็กๆ ทำเว็บเล่นๆ ค่อยใช้ Netlify (ซึ่งพวก Vercel / Firebase Hosting / Github Pages / Render / Surge พวกนี้ก็มี Free plan ที่จำกัด bandwidth 100GB ต่อเดือนเหมือนกัน) แต่ Netlify มี CDN และ Caching ผมก็นึกว่า cache ไม่โดย bandwidth แต่นั่งไล่ดู analytics มันโดนหมดเลย ทั้ง request ปกติ ทั้ง bot มีบางเดือน bandwidth เกิน ก็โดนไปฟรีๆ $20 ก็เลยคิดว่า Host เองดีกว่า

เริ่ม Refactoring

ก่อนหน้านี้ ผมใช้ Gatsby และบทความแต่ละบทความ เขียนด้วย Markdown ไม่มี Database

ทีนี้ตัว Gatsby จะมี gatsby-remark กับ gatsby-remark-transform เพื่อแปลง Markdown เป็นรูปแบบ GraphQL เมื่อเราอยากได้ข้อมูลอะไร ก็ใช้วิธี Query Graphql มาแสดงนั่นเอง

ส่วน CSS ผมก็ใช้ SCSS คอมไฟล์จาก node-sass ตัว scss ก็ค่อนข้างเก่ามาก มีทั้ง แบบเวอร์ชั่นเก่าๆ เมื่อก่อนใช้ mixin พวก transition กับ responsive ส่วน base css ผมใช้ skeleton

สรุปก่อน Refactor

ก่อนการ Refactor ผมใช้แบบนี้ครับ

  • Gatsby - ทำ Static Site Generator
  • Markdown - เขียนบทความ
  • scss และ bourbon - ตัว preprocessor css และ mixins เก่ามากๆ ใช้ตั้งแต่ก่อนหน้าที่เว็บผม เขียนด้วย Jekyll ไล่มา Middleman และมา Gatsby ล่าสุด
  • skeleton - เป็น css หลักที่ใช้ เรียกว่าเป็น base grid system ที่ใช้ในเว็บเลย แล้วก็ค่อนมา custom scss + bourbon / neat อีกที

Gatsby MDX

พระเอกของเรื่องเลยครับ ทำให้ผมสามารถแทรก React Compnonent ลงไปในไฟล์ Markdown ได้แบบนี้

import MyCustomComponent from './components/MyCustomComponent'

# This is markdown file

This is paragraph

<MyCustomComponent />

<MyGlobalButton />

Cool?

ให้อารมณ์เหมือนเขียน React แหละครับ แต่เป็น Markdown based แทน

  • ซึ่งการเรียก Component แบบ Global โดยไม่ต้อง import ไฟล์ใน mdx ก็ทำได้ โดยใช้ shortcodes ครับ แบบนี้เลย เช่นแก้ไขในไฟล์ Layout.js
// src/components/layout.js
import React from 'react'
import { MDXProvider } from '@mdx-js/react'
import { Link } from 'gatsby'
import { YouTube, Twitter, TomatoBox } from './ui'

const shortcodes = { Link, YouTube, Twitter, TomatoBox }

export default ({ children }) => (
  <MDXProvider components={shortcodes}>{children}</MDXProvider>
)

Reference : Gatsby MDX Shortcodes

  • ทีนี้สิ่งที่เปลี่ยนไปเวลาใช้ MDX คือ gatsby-plugin-remark กับ gatsby-transform-remark ไปใช้ gatsby-plugin-mdx แทน ส่วนไฟล์ extension .md หรือ .markdown ก็ไม่ต้องเปลี่ยน แค่กำหนด extensions ใน gatsby-plugin-mdx ว่าใช้ extensions อะไรบ้างก็จบแล้ว

Reference : Getting Started with MDX

  • query จากปกติใช้ allMarkdownRemark ก็เปลี่ยนเป็น allMdx ส่วนบล็อกโพสจาก markdownRemark ก็เป็น mdx
- allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
+ allMdx(sort: { fields: [frontmatter___date], order: DESC }) {
- markdownRemark(fields: { slug: { eq: $slug } }) {
+ mdx(fields: { slug: { eq: $slug } }) {
  • และในส่วน Blog post template ที่เอาไว้ render markdown ก็ใช้ component MDXRenderer แทน และ graphql data ก็เปลี่ยนจาก html เป็น body
import { MDXRenderer } from 'gatsby-plugin-mdx'

- <section dangerouslySetInnerHTML={{ __html: post.html }} />
+ <MDXRenderer>{post.body}</MDXRenderer>

Reference : Migrating Remark to MDX

Theme UI

Theme UI เรียกว่าเป็น Design Framework ครับ ทำให้เราสามารถทำ UI ให้มีแนวคิดเดียวกัน สร้าง Design System ได้ มองว่ามันคือ ทั้ง Concept และ Framework ในตัว (มีพวก built-in Component มาให้)

ซึ่งจริงๆ แล้ว Theme UI มันก็ยืม API จาก Emotion มา หากใครเขียน Emotion มา ก็เขียน Theme UI ได้ไม่ยาก และตัว Concept หรือ Design Spec ก็จะตาม System UI

สรุปสั้นๆ Theme UI เป็นอะไรดีนะ เรียกว่าเป็น Tool ที่ช่วยให้เราออกแบบและปรับแต่ง UI ผ่าน props รวมถึงการ scale ต่างๆ และ theme color ละกัน

วิธีการใช้งาน ก็ง่ายเลย เพราะมี gatsby plugins ให้ใช้ ไม่ต้องกำหนด <ThemeProvder> และ custom อะไรมากมาย

npm install gatsby-plugin-theme-ui

จากนั้น ก็แค่เพิ่มลงไปในไฟล์ gatsby-config.js

module.exports = {
  plugins: ['gatsby-plugin-theme-ui']
}

เมื่อใช้ Theme UI แล้ว ผมก็จะไม่ใช้ remark-prism ที่เป็นตัว syntax highlighter ใน markdown แต่ใน Theme UI จะมี @theme-ui/prism มาให้ใช้งานเลย

Reference : gatsby-plugin-theme-ui

ส่วนพวก Theme config ต่างๆ Color ของสีในเว็บ และ UI theme ต่างๆ ผมก็ใช้ Gatsby Shadowing เพื่อ override default ของ theme ui ครับ โดยสร้างโฟลเดอร์ชื่อเดียวกัน ไว้ที่ไฟล์ src/gatsby-plugin-theme-ui ในนี้ผมก็กำหนด index.js กำหนด component.js ได้ครับ

ไฟล์ component.js ผมก็ใช้ @theme-ui/prism แบบนี้

import Prism from '@theme-ui/prism'

export default {
  pre: props => props.children,
  code: Prism
}

ส่วน index.js ก็เป็น Theme spec เลย ตัวอย่าง Theme Specification

Reference : Shadowing in Gatsby Themes

ไหนๆ พูดถึง Theme UI แล้ว ก็ขอยกตัวอย่าง ให้ดูคร่าวๆ ละกันครับ ในเมื่อมันเป็น Design System ฉะนั้นเราสามารถปรับ UI หรือกำหนด style ให้มันง่ายๆ ผ่าน props ได้เลย เช่น

<h1 sx={{ variant: 'text.title' }}>About Me</h1>

ตัว h1 มันก็จะได้ css ตามที่เรากำหนดไว้ใน theme เช่น

{
  "text": {
    "title": {
      "color": "#ff0000",
    }
  }
}

หรือกำหนด เป็น color ก็ได้ เหมาะสำหรับการกำหนด พวก primary, text, secondary color เช่น

{
  "text": {
    "title": {
      "color": "primary"
    }
  }
}

อีกตัวอย่างเช่น เรื่อง spacing หรือ fontSize หากเรากำหนด theme ไว้แบบนี้

{
  breakpoints: ['40em', '52em', '64em', '80em'],
  space: [0, 4, 8, 16, 32, 64, 128, 256, 512],
  fontSizes: [12, 14, 16, 18, 24, 36, 48, 64, 72]
}

เราสามารถกำหนด props ได้แบบนี้เลย เพื่อทำ Responsive

<div sx={{ my: [1, 2, 3, 4]}}>
  <h2 sx={{ fontSize: [2, 3, 4, 4]}}>
</div>

ด้านบน my (หรือ margin-top และ margin-bottom) ถูกกำหนดเป็น array หมายความว่า ตัวแรก คือตอน breakpoint = 40em หรือบน mobile นั่นเอง ตัวสองคือ breakpoint 52em ไล่ไปเรื่อยๆ

หากใครสนใจ Theme UI หรือแนวๆ คล้ายๆ กันลองดู links พวกนี้เพิ่มเติมครับ

Optimized Build time

ข้อนี้ก็เป็นปัญหาใหญ่อยู่เหมือนกัน เนื่องจากว่า Gatsby ใช้เวลา build นานมากๆ สาเหตุหลักก็คือ Generate Images ตอน build นั่นเอง ซึ่ง ก่อนหน้านี้ เวลา build ตอนไม่มี .cache ผมใช้เวลา 25-30 นาที นะ ซึ่งนานมากๆ (Generate รูปประมาณ 18000 รูป)

แต่ถ้ามี .cache แล้ว มันก็ไม่นานแล้ว เฉลี่ย 1-5 นาที

โดยเงื่อนไข มันจะไม่ build หรือ generate ไฟล์ใหม่ ถ้าเราไม่ไปแก้ไฟล์

  • gatsby-config.js - เวลาเพิ่ม ลบ plugins ยังไงก็ต้องแก้
  • Query ต่างๆ - เช่น หน้า Home หรือหน้า Post เราเปลี่ยนขนาด Image size มันก็ generate ใหม่

ซึ่งช่วง Development แน่นอน เราต้องแก้ไฟล์พวกนี้บ่อยอยู่แล้ว ทำให้ทุกครั้งมันต้อง generate images ใหม่ตลอด มีบางวัน ผมใช้เวลารอ build นานกว่านั่งโคีด เลยตัดสินใจ เอาพวกรูปเก่าๆ สมัยปี 2014-2016 ซึ่งช่วงนั้นอัพไว้เยอะมากๆ และแทบไม่ได้อัพเดทแล้ว ย้ายไป CDN เลยดีกว่า เพราะเราไม่ได้แก้ไข แต่ทำไมต้องให้มัน build / generate ด้วย

  • ทีแรกคิดไว้ว่าจะ host S3 / Cloudfront ไปๆ มาๆ ง่ายๆ อัพขึ้น Github public repo แล้ว link ไปเลย ง่าย จบ ฮ่าๆ
  • อีกส่วนนึงคือ gatsby-remark-images - พยายามลด Quality และ width size ให้เล็กลงมากที่สุด เพราะยิ่ง quality และ size ใหญ่เท่าไหร่ ตอน generate ยิ่งได้ไฟล์เยอะ และยิ่งนานไปอีก
  • เรื่องของ Traced SVG ของรูป และสังเกตตอน build มันนานมากๆ สุดท้าย เลิกใช้ ไปใช้แบบ blur effect ปกติครับ
  • อีกส่วนคือ gatsby-node.js พยายามดูว่า มี function ไหนที่มัน query ช้าๆ หรือไม่ได้ทำ async หรือเปล่า query หลายๆ อันก็จับยัดใส่ Promise.all ซะ
  • ในแต่ละหน้า จะมี Query ซ้ำซ้อน รวมถึง Query image ที่ size ใหญ่กว่าใช้งานจริงบ้าง ก็พยายามลดให้ size เหมาะกับใช้งานจริง และยิ่ง width น้อย ยิ่ง generate images ได้ไวขึ้น

สุดท้าย ตอนนี้ ไฟล์รูปเวลา generate สุดท้าย จริงๆ เหลือแค่ 5000 แล้ว ตอนนี้ ถ้า build ครั้งแรก จาก 25-30 นาที เหลือประมาณ 10 นาทีเอง และยิ่งตอนออัพเดท หรือมี .cache แล้ว ก็สบายเลย ดีกว่าเดิมเยอะ (จริงๆ ตอน build production ไม่เท่าไหร่) มีแค่ตอนนั่ง Development นี่แหละ บางทีไปแอบแก้พวก config หรือ images ไง ตอนนี้ก็ไม่ต้องรอนานแล้ว เพราะลดรูป ทั้งจำนวน ขนาด ลงไปเยอะ

Deploy Time

หลังจากตัดสินใจ มาทำ Self host เอง ก็ต้องมาเสียเวลา config server เองด้วย nginx ซึ่งสกิลเราก็ basic มาก อาศัย google ช่วย แล้วก็ลองดูโค๊ดชาวบ้านเค้าประกอบ ค่อยๆ ปรับไป

Workflow ก็ทั่วๆไปครับ คือใช้ Github master branch และเราไม่ต้อง manual แค่เขียนบทความ และ push code ก็พอ

  1. ทุกๆ feature ทุกๆ commit ก็จะต้องเปิด Pull Request
  2. ใช้ [Semaphore CI] เป็น CI/CD Service ครับ เพื่อรัน ESLint + Prettier ครับ ในแต่ละ branch
  3. ถ้า CI ผ่านหมด ก็ merge ไป master ผมใช้วิธี rebase ครับ และชอบ rebase มากกว่า merge :)
  4. master branch หลักจากรัน ESLint + Prettier เสร็จ มันก็จะไป Trigger Host เราด้วยครับ
  5. ตัว Host ก็ทำการดึง git ล่าสุดมา build ครับ เป็นอันเรียบร้อย flow คร่าวๆ ก็ไม่ได้มีอะไรพิเศษ

แม้ repo นี้ผมจะทำคนเดียว แต่ผมก็ยังต้องทำ git workflow แบบทีมครับ เพราะตอนเปิด Pull Request ผมก็ยังนั่ง Review ตัวเองเลย ฮ่าๆ หรือถ้าจะให้ดีก็ใส่พวก Code quality ช่วยเช็ค เช่น Codacy หรือฟรีๆ ก็ Sonarqube ก็ช่วยหลีกเลี่ยงบัคได้เยอะเหมือนกัน

สิ่งสำคัญคือเรื่องของการ Caching เพราะต้องทำให้เว็บเรา cache ได้ด้วย เพื่อเพิ่มความเร็วในการโหลด และต้องไม่ไปกระทบกับไฟล์ sw.js ซึ่งเป็นไฟล์ Service Worker ใน Gatsby ซึ่ง ถ้าเราไป cache ไฟล์นี้ไว้ เวลา content อัพเดท ผู้อ่าน หรือ Client ก็ไม่ได้อัพเดท อ่านโดน cache ตลอด

สิ่งที่ยากคือ ต้อง cache ร่วมกับ Cloudflare จนทำให้ผมได้ เขียนบทความไปก่อนหน้านี้แล้วคือ

สุดท้าย nginx ผมได้หน้าตาประมาณนี้ จากการทำตามบทความเรื่อง Gatsby Caching

{
  server ...

  location ~* \.(?:html)$ {
    add_header Cache-Control "public, max-age=0, must-revalidate";
  }

  location = /sw.js {
    expires off;
    if_modified_since off;
    etag off;
    add_header Cache-Control "no-cache, no-store, max-age=0, must-revalidate, proxy-revalidate";
  }

  location /page-data {
    expires off;
    if_modified_since off;
    etag off;
    add_header Cache-Control "no-cache, no-store, max-age=0, must-revalidate, proxy-revalidate";
  }

  location /static {
    add_header Cache-Control "public, max-age=31536000, immutable";
    expires 365d;
  }
}

ก็ไม่แน่ใจว่า ทำไมพอใช้ Cache-Control "public, max-age=0, must-revalidate" แล้ว Cloudflare มัน Override set Header expires เพิ่มตลอดเลย ทำให้ไฟล์ sw.js ถูก cache บน Browser ตลอด ผมก็เลย set ให้เป็น no-cache, no-store, expires off ไปเลย ใส่ทุก options ฮ่าๆ

expires off;
if_modified_since off;
etag off;
add_header Cache-Control "no-cache, no-store, max-age=0, must-revalidate, proxy-revalidate";

Reference : Caching Static Sites

สรุป

บทความนี้ก็เป็นบันทึก ที่ผมใช้เวลานั่งปรับบล็อกตัวเอง อยู่นานเหมือนกัน น่าจะเป็นเดือนเลย ใช้เวลาว่างๆ หลังเลิกงาน บางทีก็เสาร์ อาทิตย์ แล้วแต่จะหาเวลาได้ มาปรับแต่งให้มีหน้าตาใหม่

หากใครสนใจ Theme UI มีตัวอย่างใน Github เยอะเลยครับ theme-ui

สิ่งที่ได้คือ ทำให้ผมได้เรียนรู้สิ่งใหม่ๆ วิธีการใหม่ๆ ได้ลองผิด ลองถูก เพราะการที่เราได้ลงมือทำอะไรจริงๆ มันดีกว่าอ่านหนังสือหรือดู Tutorial แน่นอน เพราะงานจริงๆ มันไม่จบแค่ทฤษฎี หรือแค่ทำตามแล้วจะได้ มันมีทั้ง error หรือสิ่งที่ไม่คาดคิด เราก็ต้องนั่งแก้ปัญหาเฉพาะหน้าไป นั่ง debug นั่งไล่โค๊ด ทุกๆ ปัญหา ก็ทำให้เราได้เรียนรู้นั่นเอง

ก็หวังว่าบทความนี้จะเป็นประโยชน์สำหรับใครที่หลงเข้ามาอ่านนะครับ และก็ไม่รู้ว่า มีคนใช้ Gatsby กันอยู่รึเปล่านะ?

Happy Coding ♥️

Buy Me A Coffee
Authors
Discord