從 Gatsby.js 轉移到 Astro 部落格

9 min read

# tech

寫文章前就是要先寫部落格(X)上一版使用 Gatsby 漸漸的覺得 Gatsby 臃腫麻煩,想要換一個簡潔好改的 SSG 框架,此時看到 用 Astro 重寫部落格的心得 - Apprentice Log 這篇文章,發現了 Astro 這個框架。

Island Architecture

根據官網所述,Astro 是基於 Island Architecture 的網頁框架,其中 Island Architecture 為了解決 SSR 中為了做到與 CSR 同等的互動, TTI (Time To Interaction 使用者開始互動時間) 時間越來越長的問題而提出的架構。

原先在 SSR 比起 CSR 的優勢就是有更短的 FCP (First Content Print 所需內容回傳出現時間) 與 TTI 時間,但當隨著需求又要兼顧 SSR 優點與 CSR 高互動的特點時,後續就出現了 Rehydration 的架構,將 SSR 與 CSR 混合,server 會給 client 一個渲染完成的 HTML,接著再透過 client 端操作時,來逐步 hydrate a.k.a 注入 - 需要的 JavaScript 來達成互動,維持 CSR 的高互動性。

但 hydrate 很花時間,雖然還保有 SSR 有的低 FCP 時間,卻因為要讀取更多的 JavaScript 而逐漸失去了原先低 TTI 的優點,所以衍伸出各種 hydrate 方式,例如:只 hydrate 一部分 (partial hydration),更甚者逐步的 hydrate JavaScript (progressive hydration),目的上都是為了讓最一開始不用 hydrate 全部的內容,減少一開始 JavaScript loading 負擔,減少必要元件的 TTI 時間。

然而即便如此還是有缺點,能做到上面的事情,前提是必須先知道使用者會與什麼元素互動才能決定要先 hydrate 什麼元件,否則使用者還是得等前面 hydrate 完才有辦法進行互動。

Island Architecture 主張可互動元件間應是獨立的區塊(有一點 micro frontend 的味道 1)可以各自執行自己的 hydrate 有自己的靜態內容 + JavaScript。有別於 Progressive Hydration 是由上而下,由一個整合的 root 來排程執行,Island Architecture 的各個動態元件會自己去執行自己的 hydrate script,更甚者在 Astro 裡面各個動態元件要使用不同的框架語言 e.g. react, vue ... etc 皆可。

而 Island Architecture 缺點就是,當互動性真的要高到像是社群媒體或是 SaaS 一樣,動態元件會切到瘋掉,不過依舊提供需要:低 TTI、FCP 、高 SEO 需求的網站很棒的一個選擇。

Astro 簡介

當時在選擇要離開 Gatsby.js 的時候,也一度在眾多開發者使用的 11ty 猶豫不絕,但接觸了 11ty 一陣子,發現最多人使用的 google/eleventy-high-performance-blog - GitHub 與平常開發習慣落差極大,若是從零開始養 11ty 文件上也並非非常友善,而最終選擇了 Astro。

Astro 支援 SSG, SSR 渲染,且可以插入不同的框架語法,而且原生就有類似於 jsx 的語法,並且能輕易利用 component script 來處理動態渲染的內容,並讓下方 component template 來決定 DOM 結構。

---
// component script
const { posts } = Astro.props
---
// component template
<ul class="post-list space-y-2">
    { posts.map(post => (
        <li class="post flex justify-between">
            <a href={post.url}>{post.frontmatter.title}</a>
            <time class="hidden md:inline" datetime={post.frontmatter.date}>
                {new Date(post.frontmatter.date)
	                .toLocaleDateString('en-us', {
                    year: 'numeric',
                    month: 'short',
                    day: 'numeric',
                })}
            </time>
        </li>
    )) }
</ul>

這種做法令開發體驗大幅提升,什麼元件要什麼資料寫在一起即可,更甚者也可以用 Astro 提供的 framework component 來處理更複雜的互動(雖然沒有用到它這個最大的優點 XD)。

Astro Usage

目前的部落格參考 adwinmbd/astro-blog-template: A simple Astro.js starter blog template. repo 進行調整:

.
├── assets
│   ├── min.svg
│   ├── ... 靜態資源
├── components
│   ├── BaseHead.astro
│   ├── Content.astro
│   ├── ... 各種元件
├── config.ts
├── env.d.ts
├── layouts
│   ├── ArchiveLayout.astro
│   ├── BaseLayout.astro
│   └── ... markdown 或模板頁面
├── pages
│   ├── 404.astro
│   ├── about.astro
│   ├── article
│   │   ├── [...page].astro
│   │   ├── all
│   │   │   └── index.astro
│   │   ├── ...markdown 檔案
│   ├── index.astro
│   ├── newsletter
│   │   ├── ...markdown 檔案
│   │   ├── [...page].astro
│   │   └── all
│   │       └── index.astro
│   ├── note
│   │   ├── all
│   │   │   └── index.astro
│   │   ├── ...markdown 檔案
│   │   ├── tag
│   │   │   └── [tag].astro
│   ├── rss.xml.js
│   └── series
│       └── index.astro
└── styles
    └── global.css

Astro 的 routing 預設是跟著 page 的資料架構決定,若有多個子頁面 e.g. pagination, tag 分類頁面,則透過類似 [page].astrogetStaticPaths api 來做處理。

針對 markdown 檔案的部分,可以在 markdown 上方指定 layout,即可產生對應的頁面:

---
layout: ../../layouts/BlogPost.astro
title: issue#4 不只要好,還要是友善的工程師 :)
date: August 28, 2022
slug: 2022-08-28
---

經過了幾次電子報的嘗試慢慢找到一點感覺,定調了內容方向包括:

... 各種文章內容
  

在 layout 中,會將 markdown compile 後的內容放入 slot 區塊中:

---
import BaseLayout from "./BaseLayout.astro";
const { content } = Astro.props;
---
<BaseLayout>
	<article>
		<h1 class="text-3xl font-medium my-5">{ content.title }</h1>
		{content?.date && <time>{content.date}</time>}
		<content class="markdown">
			<slot />
		</content>
	</article>
</BaseLayout>

若是有需要對 markdown 做額外處理 e.g. 選擇 prism 做 code highlight, 增加 toc, admonitions 等功能,也可以於 astro.config.mjs 進行設定 remark 與 rehype plugins:

import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import tailwind from "@astrojs/tailwind";
// https://astro.build/config
export default defineConfig({
  site: 'https://minw.blog',
  integrations: [mdx(), sitemap(), tailwind()],
  server: {
    port: 5000
  },
  markdown: {
    syntaxHighlight: 'prism',
    remarkPlugins: ['remark-gfm', 'remark-emoji'],
    rehypePlugins: [
      ['rehype-slug', {}],
      ['rehype-toc', { headings: ['h2', 'h3'] }],
      // ...
    ],
  },
});

而一般頁面的部分,也可以透過 Astro.glob api 來取得所有檔案資料,像是下方式文章的 archive 頁面:

---
import ArchiveLayout from "../../../layouts/ArchiveLayout.astro";

const posts = (await Astro.glob('../*.{md,mdx}')).sort(
	(a, b) => new Date(a.frontmatter.date).valueOf() - new Date(b.frontmatter.date).valueOf()
)
const groupPosts = posts.reduce((acc, post) => {
	const year = new Date(post.frontmatter.date).getFullYear();
	if(!acc[year]) acc[year] = [];
	acc[year].push(post);
	return acc;
}, {})
---

<ArchiveLayout 
	title="文章列表"
	link="/article"
	groupPosts={groupPosts}
/>

另外樣式部分,Astro 也有支援 Tailwind Plugin 或者可以直接撰寫 CSS 來做處理,由於原先 Gatsby 既有使用 TailwinCSS,就延續安裝了 TailwindCSS 的 astro plugin。另外若需要一些互動除了選擇 framework component,也可以簡單的撰寫 <script> 於 component template 中,就像是回歸到最原初的網頁開發一般簡單。

最終部署就看是採用 SSR 模式還是 SSG 模式了,因為部落格內容量還小,選擇了 SSG 模式並延續之前部落格的部署,同樣部署在 Netlify 上,僅需要從 Netlify 設定 repo 選擇對應的 branch 與 build 指令即可執行。不過最近 Netlify 開始公告針對 private repo 要進行收費,接下來可能會開始尋覓其他遷徙的可能吧!

小結

每次研究部落格技術都可以從頭複習 render 趨勢長長知識,整體使用下來 Astro 開發體驗相當直覺,以開發部落格來說非常友善。而目前 Astro 的社群規模還小,也才剛 release 1.0.0 版是相當新的框架,作為部落格來新嘗試是一個不錯的選擇。

最後謝謝大家閱讀到這邊,以上如果有任何需要補充或是錯誤的地方,都歡迎可以留言告訴我,之後有任何後續使用狀況也會再更新回這篇文章,感謝大家。

References

Footnotes