Hồi mới code, mình cũng từng xài WordPress. Mất 2 tiếng cài đặt, 1 tiếng chọn theme, 3 tiếng custom CSS cho cái theme nó vừa ý, và cuối cùng nhận ra: “ủa, mình muốn viết blog chứ có muốn làm web designer đéo đâu?“.

Rồi mình chuyển qua Medium. Xài được 1 thời gian cũng ổn, cho đến khi thấy bài viết của mình bị chèn quảng cáo của Medium và khách vào đọc bị đòi đăng ký tài khoản. Cảm giác như dẫn bạn về nhà chơi mà bạn bị bảo vệ chung cư chặn lại đòi xem CCCD vậy. Đéo ổn.

Thế là mình quyết định tự build blog. Tiêu chí đặt ra:

  • Nhanh (cả cho người viết lẫn người đọc)
  • Rẻ (càng rẻ càng tốt, free thì càng tốt)
  • Không phải maintain server (ai rảnh đâu mà suốt ngày SSH với update bảo mật)
  • Code được (vì dân IT mà, không code tay ngứa)

Sau 1 hồi research, combo Gatsby + GitHub Pages sáng lên như vầng thái dương giữa đêm tối.

Tại sao lại là Gatsby mà không phải thứ khác?

Thị trường static site generator giờ nhiều như quân Nguyên: Jekyll (Ruby), Hugo (Go), Next.js (React), 11ty (JS), Hexo (JS), Docusaurus (React)… Tại sao mình chọn Gatsby?

Tiêu chí Jekyll Hugo Next.js Gatsby
Ngôn ngữ Ruby Go React React
Hot reload Chậm Nhanh Nhanh Hơi chậm
Plugin ecosystem Tạm Ít Nhiều Rất nhiều
Data layer File-based File-based Tự bơi GraphQL
Học curve Dễ Dễ Trung bình Trung bình

Lý do chính: mình là dân React. Viết blog bằng React component — có gì mà không thích? Thay vì ngồi học syntax template của Hugo hay Liquid của Jekyll, mình cứ JSX + TypeScript mà quất. Cái gì quen tay thì làm nhanh hơn, đúng không?

Thêm nữa, Gatsby có GraphQL data layer rất hay. Bạn có thể lấy data từ markdown, CMS, API, file system… tất cả thông qua GraphQL, và Gatsby tự động tạo ra static HTML từ những data đó. Hiểu nôm na: bạn viết GraphQL query, Gatsby lo phần còn lại.

Cuối cùng là plugin ecosystem. Cần SEO? gatsby-plugin-react-helmet. Cần syntax highlighting? gatsby-remark-prismjs. Cần sitemap? gatsby-plugin-sitemap. Cần responsive image? Cả tá plugin liên quan tới sharp. Gần như mọi thứ bạn cần cho 1 cái blog đều đã có người viết plugin, bạn chỉ việc npm install và thêm vào config là xong.

Kiến trúc Gatsby — hiểu trong 1 phút

Gatsby hoạt động theo flow thế này:

Markdown files → Source plugin → GraphQL Data Layer → Templates → Static HTML
  1. Source plugins kéo data từ đâu đó vào (filesystem, CMS, API…)
  2. Data được chuẩn hóa thành GraphQL nodes
  3. Transformer plugins xử lý data (markdown → HTML, ảnh → responsive variants)
  4. Ở build time, Gatsby query GraphQL để lấy data và render thành trang HTML tĩnh
  5. Sau build, bạn có 1 folder public/ chứa toàn bộ website — chỉ cần upload lên bất kỳ static host nào

Điểm mấu chốt: không có server, không có database, không có backend. Mỗi lần bạn viết bài mới, build lại là có ngay.

Setup cơ bản — bắt đầu từ đâu?

1. Tạo project

npm install -g gatsby-cli
gatsby new my-blog
cd my-blog

Gatsby CLI sẽ tạo ra cấu trúc thư mục như sau:

my-blog/
├── src/
│   ├── pages/          # Các trang tĩnh (index, 404)
│   ├── templates/      # Template cho dynamic pages (blog post, category)
│   ├── components/     # React components dùng chung
│   └── blog/           # Chỗ để file markdown bài viết
├── gatsby-config.js    # Config chính của site
├── gatsby-node.js      # Logic tạo page động
└── package.json

2. Cấu hình path prefix

Vì blog mình deploy lên GitHub Pages dạng username.github.io/blog/ nên cần path prefix:

// gatsby-config.js
module.exports = {
  pathPrefix: '/blog',
  siteMetadata: {
    title: 'Blog của tao',
    description: 'Lôm côm, ba lăng nhăng',
    author: '@taolaai',
  },
  // ...
}

Khi build: gatsby build --prefix-paths — tất cả link sẽ tự động thêm /blog vào trước.

3. Source data từ Markdown

Đây là phần quan trọng nhất — làm sao để Gatsby biết bài viết của bạn ở đâu?

// gatsby-config.js
plugins: [
  {
    resolve: 'gatsby-source-filesystem',
    options: {
      path: `${__dirname}/src/blog`,
      name: 'blog',
    },
  },
  {
    resolve: 'gatsby-transformer-remark',
    options: {
      plugins: [
        'gatsby-remark-prismjs',          // syntax highlight code
        'gatsby-remark-images',           // responsive ảnh trong bài
        'gatsby-remark-external-links',   // target="_blank" cho link ngoài
        'gatsby-remark-emojis',           // <img class="emoji-icon" alt="emoji-smile" data-icon="emoji-smile" style="display: inline; margin: 0; margin-top: 1px; position: relative; top: 5px; width: 25px" src="data:image/png;base64, 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" title="emoji-smile" /> → emoji
        'gatsby-remark-copy-linked-files', // copy file đính kèm qua public/
      ],
    },
  },
]

gatsby-source-filesystem quét folder src/blog/ và tạo ra các node cho mỗi file. Sau đó gatsby-transformer-remark parse markdown thành HTML, xử lý frontmatter, và các remark plugin con xử lý tiếp (highlight code, tối ưu ảnh, v.v.).

Kết quả: mỗi file src/blog/ten-bai/index.md trở thành 1 GraphQL node kiểu markdownRemark với đầy đủ html, excerpt, frontmatter (title, date, category, tags…).

GraphQL Data Layer — trái tim của Gatsby

Một khi data đã vào GraphQL layer, bạn có thể query nó ở bất kỳ đâu. Ví dụ, query tất cả bài viết để hiển thị trên homepage:

query {
  allMarkdownRemark(
    filter: { fields: { draft: { eq: false } } }
    sort: { fields: [frontmatter___date], order: DESC }
  ) {
    edges {
      node {
        fields { slug }
        frontmatter {
          title
          date
          category
          tags
        }
        excerpt
      }
    }
  }
}

Để ý: filter: { fields: { draft: { eq: false } } } — đây là cách mình lọc bài nháp. gatsby-plugin-draft tự động thêm field draft dựa trên frontmatter, và mình chỉ việc filter nó trong GraphQL query.

Tại sao GraphQL mà không phải gì khác? Vì với GraphQL bạn chỉ query đúng data mình cần, không thừa không thiếu. Và Gatsby tối ưu hóa việc này ở build time — nó chạy tất cả GraphQL query, lấy data, và nhồi vào static HTML. Đến runtime thì không còn GraphQL nữa, chỉ có HTML + CSS + JS, load nhanh như chớp.

Tạo dynamic pages với gatsby-node.js

Có data rồi, giờ làm sao tạo ra từng trang riêng cho mỗi bài viết? Đây là việc của gatsby-node.js:

exports.createPages = ({ graphql, actions }) => {
  const { createPage } = actions
  const blogPostTemplate = require.resolve('./src/templates/blog-post.tsx')

  return graphql(`
    {
      allMarkdownRemark(
        filter: { fields: { draft: { eq: false } } }
        limit: 1000
      ) {
        edges {
          node {
            fields { slug }
            frontmatter { title, category, tags }
          }
        }
      }
    }
  `).then(result => {
    const posts = result.data.allMarkdownRemark.edges

    posts.forEach((post, index) => {
      createPage({
        path: post.node.fields.slug,
        component: blogPostTemplate,
        context: {
          slug: post.node.fields.slug,
          previous: index === posts.length - 1 ? null : posts[index + 1].node,
          next: index === 0 ? null : posts[index - 1].node,
        },
      })
    })
  })
}

Flow hoạt động:

  1. Query tất cả markdown đã publish
  2. Với mỗi bài, gọi createPage() — truyền slug làm path, và truyền previous/next để tạo nút điều hướng bài trước/sau
  3. Gatsby sẽ render template blog-post.tsx với data từ context cho mỗi page

Template blog-post nhận pageContext.slug rồi tự query data của bài đó:

export const pageQuery = graphql`
  query BlogPostBySlug($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      excerpt
      frontmatter { title, date, category, tags }
    }
  }
`

Và phần render thì đơn giản:

<div dangerouslySetInnerHTML={{ __html: post.html }} />

Nghe “dangerously” nhưng thực ra an toàn vì HTML này được generate từ markdown của chính bạn, không phải từ user input.

Tương tự cho category và tag pages — mình dùng createPage() để tạo /categories/ten-category//tags/ten-tag/ từ danh sách unique category/tag lấy được từ frontmatter.

Plugin ecosystem — những viên gạch làm nên blog

Đây là danh sách plugin mình đang dùng, kèm lý do tại sao nên hoặc không nên cài:

Plugin Mục đích Nên dùng?
gatsby-plugin-typescript Viết component bằng TypeScript Nếu bạn xài TS
gatsby-plugin-react-helmet Quản lý thẻ <head>, SEO, OG tags Luôn luôn
gatsby-plugin-image + sharp Ảnh responsive, lazy load, blur-up placeholder Luôn luôn
gatsby-plugin-manifest PWA manifest, icon cho mobile Nên
gatsby-plugin-sitemap Tự sinh sitemap.xml cho SEO Nên
gatsby-plugin-robots-txt Tự sinh robots.txt Nên
gatsby-plugin-draft Lọc bài nháp, không publish nhầm Có viết draft thì nên
gatsby-plugin-offline Service worker, offline mode Mình bỏ rồi vì service worker cache quá aggressive, code cũ không tự update
gatsby-remark-prismjs Highlight syntax code trong markdown Có viết code thì phải có
@weknow/gatsby-remark-twitter Nhúng tweet vào bài viết Nếu hay dẫn tweet

Tip: đừng cài plugin chỉ vì nó có vẻ ngầu. Mỗi plugin là 1 dependency, thêm vào là thêm maintenance burden. Mình từng cài gatsby-plugin-offline vì nghĩ “ồ offline mode, ngầu vãi” — xong rồi phải cài thêm gatsby-plugin-remove-serviceworker để gỡ nó ra vì nó cache lỗi và người đọc không thấy bài mới.

Deploy lên GitHub Pages — đỉnh cao của sự lười biếng

Đây là phần khiến mình sướng nhất: deploy hoàn toàn miễn phí, tự động 100%, không cần server.

Ý tưởng:

  1. Source code để ở branch develop
  2. Mỗi lần push lên develop, CI tự chạy gatsby build
  3. Output (thư mục public/) được deploy lên branch master
  4. GitHub Pages phục vụ từ branch master với path prefix /blog

Trước đây mình xài Travis CI, nhưng giờ chuyển qua GitHub Actions cho gọn — đỡ phải maintain 2 nơi. Workflow đơn giản như sau:

name: Deploy to GitHub Pages

on:
  push:
    branches: [develop]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '16'
      - run: yarn install --frozen-lockfile
      - run: yarn build
      - uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./public
          publish_branch: master

Thế là xong. Mỗi lần viết bài mới, chỉ cần:

git add src/blog/bai-moi/index.md
git commit -m "new post: bài mới"
git push origin develop

Vài phút sau bài viết đã xuất hiện trên chungtran4078.github.io/blog/. Không cần SSH, không cần FTP, không cần mở mắt dậy sớm. Chỉ cần git push và cầu nguyện cho CI đừng fail.

Tổng chi phí vận hành blog: 0 đồng. Không tiền server, không tiền domain (xài username.github.io), không tiền CDN (GitHub Pages chạy trên Fastly), không tiền CI (GitHub Actions free cho public repo). Giá mà mọi thứ trong đời đều free như này nhỉ.

Vài thứ bạn nên biết trước khi lao vào

1. Build time tăng dần theo số lượng bài viết. Hiện tại blog mình 14 bài, build mất ~2 phút. Nếu bạn có 500 bài, có thể mất 10-15 phút. Gatsby có gatsby develop để dev cực nhanh (chỉ build page đang xem), nhưng production build thì phải chịu. Nên nếu blog nhiều bài, cân nhắc Hugo hoặc 11ty.

2. Gatsby 3 → 4 → 5, version nhảy như châu chấu. Blog này đang ở Gatsby 3, lên 4 hay 5 cũng được nhưng mà lười. Breaking changes giữa các major version là có thật, nên nếu đang chạy ổn thì đừng vội upgrade chỉ vì FOMO.

3. Hosting trên username.github.io chỉ được 1 site. Nếu muốn nhiều project site, bạn phải dùng custom domain hoặc repo riêng kiểu username.github.io/project-name/. Cái này GitHub Pages hỗ trợ tốt.

4. Không có backend, nghĩa là không có comment, không có form, không có search server-side. Muốn có comment thì nhúng Disqus hay Giscus. Muốn search thì dùng gatsby-plugin-local-search hoặc Algolia. Nhưng thú thật, mình thấy để blog không có comment cũng hay — đỡ phải trả lời mấy ông vào hỏi “sao code t copy từ bài của bạn không chạy?“.

Tổng kết

Gatsby + GitHub Pages là combo hoàn hảo cho dân IT xài React, muốn viết blog nhanh, đẹp, free, và quan trọng nhất: toàn quyền kiểm soát. Không ai chèn quảng cáo vào bài viết của bạn, không ai bắt khách đọc phải đăng ký tài khoản, không ai giới hạn bandwidth.

Nhược điểm thì cũng có: build hơi chậm, config hơi nhiều, version Gatsby update nhanh vl. Nhưng đổi lại, bạn có 1 cái blog mà bạn hiểu từng dòng code, toàn bộ source nằm trong tầm tay, và chi phí là 0 đồng trọn đời. Quá hời.

Nếu bạn đang đọc bài này và cũng đang chán Medium/WordPress, thì: npm install -g gatsby-cli và bắt đầu thôi. Nhớ quay lại đây khoe blog mới nhé, để mình còn vô comment dạo.