你已經寫好幾篇 .md 文章,現在準備好讓 astro 把這些好棒棒的內容轉成網頁了。
你查看了 astro 的官方文件,知道可以透過 Astro.glob() 取得指定路徑中所有的 .md 內容。這包 MarkdownInstance[] 型態的資料能讓你決定怎麼展示文章,比如在網站首頁列出「最近十篇文章」,或是在「標籤」分頁列出「分類屬於 X 的所有文章」。
但這個方便的功能僅能在 .astro 檔案內使用。如果我想要在部落格專案中建立一個 tools 資料夾,專門收納「讀取所有 .md 檔案」「預處理排序」等通用工具,這時候我們需要換個方式——使用未經 astro 包裝的 import.meta.glob() 來達成在 .astro 以外的地方讀取 .md 的需求。
以下就來介紹個人在自家部落格處理 .md 讀檔以及排序的方式。
本文
取得所有 .md 內容
首先在 ./src/tools 中新增一個檔案 post.ts 並新增第一個功能:
async function getAllPosts() { const metaAllPosts = import.meta.glob<MarkdownInstance<Post>>( "../pages/**/*.md", ); const keys = Object.keys(metaAllPosts); const allPostsPromises = keys.map((key) => metaAllPosts[key]()); return await Promise.all(allPostsPromises);}getAllPosts 的任務是透過 import.meta.glob() 讀取位於 ./src/pages 資料夾內「所有的 .md 檔案」。
在這裡傳入型別 MarkdownInstance<Post> 代表指定每一份 .md 檔案中的 frontmatter 會有哪些項目(官分型別定義可參考 astro/packages/astro/src/@types/astro.ts 內容)。比如我的每一篇部落格文章 .md 檔案都有以下開頭:
---title: 鐵人賽 Modern Web 組「捨棄 create-react-app 之餘還架了個 astro blog 昭告天下」第 1 天date: 2023-09-01 7:29:09tag: - [2023鐵人賽]banner: /2023/ithome-2023-1/miguel-a-amutio-27QOmh18KDc-unsplash.jpgsummary: 2023 鐵人賽開始了,今年來聊聊前端基礎建設以及如何用 astro 架部落格draft: true---
...那麼我的 interface Post 就會長得像下面這樣:
interface Post { title: string; date: Date; tag: string[]; banner?: string; summary?: string; draft?: boolean;}以上列出來的鍵值即是我可以透過 frontmatter 取得的欄位資訊。比如等一下我們就能透過 frontmatter.date 來根據日期排序文章。
(注意 astro 3.0 已經不再支援 draft 功能,如果需要根據特定條件來判定文章是否該在打包過程中被剔除,請參考 Breaking Changes 中的說明)
繼續往下看。根據 vite 官分文件我們可以知道變數 metaAllPosts 會有類似以下結構的內容:
const metaAllPosts = { "../pages/2023/ithome-2023-1.md": () => import("../pages/2023/ithome-2023-1.md"), "../pages/2023/ithome-2023-2.md": () => import("../pages/2023/ithome-2023-2.md"), // ...};所以我們先透過 Object.keys(metaAllPosts) 取出所有的鍵值,再透過 keys.map((key) => metaAllPosts[key]()) 執行 metaAllPosts 中的每一筆 import。最後使用 await Promise.all() 來取得「所有位在 ./src/pages 資料夾中的 MarkdownInstance 內容」。
這個功能最後回傳的資料型別會是 MarkdownInstance<Post>[]
依照日期排序
繼續在 ./src/tools/post.ts 中新增以下內容:
import dayjs from "dayjs";
function sortPostByDate(posts: MarkdownInstance<Post>[]) { return posts.sort( (a, b) => dayjs(b.frontmatter.date).valueOf() - dayjs(a.frontmatter.date).valueOf(), );}sortPostByDate 的任務非常單純,即是根據每一篇文章的日期來進行排序。所以當我們執行以下組合:
export const ALL_SORTED_POSTS = sortPostByDate(await getAllPosts());就能得「根據日期從新到舊排序」的所有部落格文章了 (́◉◞౪◟◉‵)
以個人部落格為例,我的首頁會顯示目前最新的十篇文章。達成的方式是:在 src/pages/index.astro 的 Component Script 區塊直接引用處理好的文章內容,分為前五、後五篇。接著再將切好的內容丟到排版專用的 FeaturedPostLayout 與 ListedPostLayout 元件來處理畫面樣式。
---// Component Script
import FeaturedPostLayout from "@Components/layout/FeaturedPostLayout.astro";import ListedPostLayout from "@Components/layout/ListedPostLayout.astro";import { ALL_SORTED_POSTS } from "@Tools/post";
const firstTenPosts = ALL_SORTED_POSTS.slice(0, 10);const postShouldWithSummary = firstTenPosts.slice(0, 5);const restOfPost = firstTenPosts.slice(5, firstTenPosts.length);---
<!-- Component Template (HTML + JS Expressions) --><div class="post-container"> <div class="featured-posts-container"> { postShouldWithSummary.map((post, index) => ( <div class:list={["post", `post-${index}`]}> <FeaturedPostLayout title={post.frontmatter.title} date={post.frontmatter.date} tag={post.frontmatter.tag} banner={post.frontmatter.banner} summary={post.frontmatter.summary || ""} url={post.url} /> </div> )) } </div> <div class="listed-posts-container"> { restOfPost.map((post) => ( <ListedPostLayout title={post.frontmatter.title} date={post.frontmatter.date} tag={post.frontmatter.tag} url={post.url} /> )) } </div></div>在其他需要用到文章內容的元件,也都是直接引用處理好的 ALL_SORTED_POSTS 即可。集中到 ./src/tools/post.ts 可以避免在各個 .astro 檔案中重複同樣的讀檔、排序邏輯。
總結
現在你知道怎麼把讀檔的邏輯集中到一個檔案裡面了。
而結合讀取與 astro 的渲染,你的 .md 文章們已經順利變化成靜態網頁,只可惜毫無樣式可言。為了解決這個問題,接下來的文章會來簡單介紹一下個人會如何處理部落格樣式,敬請期待 (ง๑ •̀_•́)ง