捨棄 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.astroComponent Script 區塊直接引用處理好的文章內容,分為前五、後五篇。接著再將切好的內容丟到排版專用的 FeaturedPostLayoutListedPostLayout 元件來處理畫面樣式。

---
// 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 文章們已經順利變化成靜態網頁,只可惜毫無樣式可言。為了解決這個問題,接下來的文章會來簡單介紹一下個人會如何處理部落格樣式,敬請期待 (ง๑ •̀_•́)ง