捨棄 create-react-app 之餘還架了個 astro blog 昭告天下:集中處理 .md
你已經寫好幾篇 .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:09
tag:
- [2023鐵人賽]
banner: /2023/ithome-2023-1/miguel-a-amutio-27QOmh18KDc-unsplash.jpg
summary: 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
文章們已經順利變化成靜態網頁,只可惜毫無樣式可言。為了解決這個問題,接下來的文章會來簡單介紹一下個人會如何處理部落格樣式,敬請期待 (ง๑ •̀_•́)ง