使用 Next.js 建構 Tailwind 部落格

Adam Wathan

我們團隊秉持的一個理念是,我們製作的每一項成果都應該透過一篇部落格文章來宣佈。強迫自己為每個專案撰寫一篇簡短的公告文章,可以作為內建的品質檢查,確保我們在覺得可以向世界發布之前,不會將專案稱為「完成」。

問題是,直到今天,我們實際上沒有任何地方可以發布這些文章!

選擇平台

我們是一群開發人員,所以很自然地,我們無法說服自己使用現成的東西,而是選擇使用 Next.js 來建構一些簡單且客製化的東西。

Next.js 有很多值得讚賞的地方,但我們決定使用它的主要原因是它對 MDX 有很好的支援,而 MDX 是我們想要用來撰寫文章的格式。

# My first MDX postMDX is a really cool authoring format because it letsyou embed React components right in your markdown:<MyComponent myProp={5} />How cool is that?

MDX 非常有趣,因為與一般的 Markdown 不同,您可以直接在內容中嵌入即時的 React 元件。這令人興奮,因為它為您在寫作中傳達想法的方式開啟了很多機會。您不必只依賴圖片、影片或程式碼區塊,您可以建構互動式示範,並將它們直接放在兩個內容段落之間,而不會失去使用 Markdown 撰寫的便利性。

我們計劃在今年稍後重新設計和重建 Tailwind CSS 文件網站,能夠嵌入互動式元件對於我們教導框架如何運作的能力有很大的影響,因此將我們的小部落格網站作為測試專案非常有意義。

組織我們的內容

我們首先將文章撰寫為直接放在 pages 目錄中的簡單 MDX 文件。不過,最後我們意識到幾乎每篇文章也會有相關的資源,例如至少需要一張 Open Graph 圖片。

將它們儲存在另一個資料夾中感覺有點草率,所以我們決定改為在 pages 目錄中為每篇文章建立自己的資料夾,並將文章內容放在 index.mdx 檔案中。

public/src/├── components/├── css/├── img/└── pages/    ├── building-the-tailwindcss-blog/    │   ├── index.mdx    │   └── card.jpeg    ├── introducing-linting-for-tailwindcss-intellisense/    │   ├── index.mdx    │   ├── css.png    │   ├── html.png    │   └── card.jpeg    ├── _app.js    ├── _document.js    └── index.jsnext.config.jspackage.jsonpostcss.config.jsREADME.mdtailwind.config.js

這讓我們可以將該篇文章的任何資源放在同一個資料夾中,並利用 webpack 的 file-loader 直接將這些資源匯入文章中。

中繼資料

我們將每篇文章的中繼資料儲存在我們在每個 MDX 檔案頂部匯出的 meta 物件中

import { bradlc } from "@/app/blog/authors";import openGraphImage from "./card.jpeg";export const meta = {  title: "Introducing linting for Tailwind CSS IntelliSense",  description: `Today we’re releasing a new version of the Tailwind CSS IntelliSense extension for Visual Studio Code that adds Tailwind-specific linting to both your CSS and your markup.`,  date: "2020-06-23T18:52:03Z",  authors: [bradlc],  image: openGraphImage,  discussion: "https://github.com/tailwindcss/tailwindcss/discussions/1956",};// Post content goes here

我們在此定義文章標題(用於文章頁面上的實際 h1 和頁面標題)、描述(用於 Open Graph 預覽)、發布日期、作者、Open Graph 圖片以及文章的 GitHub Discussions 討論串連結。

我們將所有作者資料儲存在一個單獨的檔案中,該檔案只包含每位團隊成員的姓名、Twitter 帳號和頭像。

import adamwathanAvatar from "./img/adamwathan.jpg";import bradlcAvatar from "./img/bradlc.jpg";import steveschogerAvatar from "./img/steveschoger.jpg";export const adamwathan = {  name: "Adam Wathan",  twitter: "@adamwathan",  avatar: adamwathanAvatar,};export const bradlc = {  name: "Brad Cornes",  twitter: "@bradlc",  avatar: bradlcAvatar,};export const steveschoger = {  name: "Steve Schoger",  twitter: "@steveschoger",  avatar: steveschogerAvatar,};

實際將作者物件匯入文章而不是透過某種識別碼連結的好處是,如果我們想要,可以輕鬆地內嵌加入作者

export const meta = {  title: "An example of a guest post by someone not on the team",  authors: [    {      name: "Simon Vrachliotis",      twitter: "@simonswiss",      avatar: "https://pbs.twimg.com/profile_images/1160929863/n510426211_274341_6220_400x400.jpg",    },  ],  // ...};

這讓我們可以透過提供中央來源來保持作者資訊的同步,但不會放棄任何彈性。

顯示文章預覽

我們想在首頁上顯示每篇文章的預覽,而這被證明是一個出乎意料的挑戰性問題。

本質上,我們想要能夠做的是使用 Next.js 的 getStaticProps 功能在建構時取得所有文章的列表,提取我們需要的資訊,並將其傳遞給實際的頁面元件以進行渲染。

挑戰在於我們想要做到這一點,而實際上不匯入每個頁面,因為這意味著我們首頁的套件將包含整個網站的每篇部落格文章,導致套件比必要的要大得多。當我們只有幾篇文章時,現在可能不是什麼大問題,但是一旦你有幾十篇或數百篇文章時,那將會浪費很多位元組。

我們嘗試了一些不同的方法,但我們最終決定使用 webpack 的 resourceQuery 功能,並結合幾個自訂載入器,使其可以以兩種格式載入每篇部落格文章

  1. 整個文章,用於文章頁面。
  2. 文章預覽,我們在首頁上載入所需的最小資料。

我們設定的方式是,當我們在匯入個別文章的結尾新增 ?preview 查詢時,我們會取得該文章的小得多的版本,其中只包含中繼資料和預覽摘錄,而不是整個文章內容。

以下是自訂載入器的程式碼片段

{  resourceQuery: /preview/,  use: [    ...mdx,    createLoader(function (src) {      if (src.includes('<!--​more​-->')) {        const [preview] = src.split('<!--​more​-->')        return this.callback(null, preview)      }      const [preview] = src.split('<!--​/excerpt​-->')      return this.callback(null, preview.replace('<!--​excerpt​-->', ''))    }),  ],},

它讓我們可以透過在導言段落後貼上 <!--more-->,或將摘錄包在 <!--excerpt--><!--/excerpt--> 標籤對中,來定義每篇文章的摘錄,讓我們可以撰寫完全獨立於文章內容的摘錄。

export const meta = {  // ...}This is the beginning of the post, and what we'd like toshow on the homepage.<!--​more-->Anything after that is not included in the bundle unlessyou are actually viewing that post.

以優雅的方式解決這個問題相當具有挑戰性,但最終能夠提出一個讓我們將所有內容都放在一個檔案中,而不是使用單獨的檔案來存放預覽和實際的文章內容的解決方案,感覺很酷。

產生上一篇/下一篇文章連結

我們在建構這個簡單網站時遇到的最後一個挑戰是,當您瀏覽個別文章時,能夠包含指向上一篇和下一篇文章的連結。

核心來說,我們需要做的是載入所有的文章(理想情況是在建置時),在列表中找到目前文章,然後抓取前一篇和後一篇的文章,以便將這些資訊作為 props 傳遞給頁面元件。

這比我們預期的還要困難,因為事實證明 MDX 目前不支援像平常那樣使用 getStaticProps。你實際上無法直接從你的 MDX 檔案中匯出它,而是必須將你的程式碼儲存在一個單獨的檔案中,然後從那裡重新匯出。

我們不想在首頁匯入文章預覽時載入這些額外的程式碼,而且我們也不想在每篇文章中重複這些程式碼,所以我們決定使用另一個自訂載入器將這個匯出預先加到每篇文章的開頭。

{  use: [    ...mdx,    createLoader(function (src) {      const content = [        'import Post from "@/components/Post"',        'export { getStaticProps } from "@/getStaticProps"',        src,        'export default (props) => <Post meta={meta} {...props} />',      ].join('\n')      if (content.includes('<!--​more-->')) {        return this.callback(null, content.split('<!--​more-->').join('\n'))      }      return this.callback(null, content.replace(/<!--​excerpt-->.*<!--\/excerpt-->/s, ''))    }),  ],}

我們還需要使用這個自訂載入器來實際將這些靜態 props 傳遞給我們的 Post 元件,所以我們也附加了你在上面看到的額外匯出。

這並不是唯一的問題。事實證明 getStaticProps 沒有提供任何關於目前正在渲染的頁面的資訊,所以當我們試圖確定下一篇和前一篇的文章時,我們沒有辦法知道我們正在查看哪篇文章。我認為這可以解決,但由於時間限制,我們選擇在客戶端做更多的工作,而在建置時減少工作量,這樣我們在嘗試找出我們需要的連結時,才能實際看到目前的路由是什麼。

我們在 getStaticProps 中載入所有文章,並將它們映射到非常輕量級的物件,這些物件僅包含文章的 URL 和文章標題。

import getAllPostPreviews from "@/getAllPostPreviews";export async function getStaticProps() {  return {    props: {      posts: getAllPostPreviews().map((post) => ({        title: post.module.meta.title,        link: post.link.substr(1),      })),    },  };}

然後在我們實際的 Post 版面配置元件中,我們使用目前的路由來確定下一篇和前一篇的文章。

export default function Post({ meta, children, posts }) {  const router = useRouter();  const postIndex = posts.findIndex((post) => post.link === router.pathname);  const previous = posts[postIndex + 1];  const next = posts[postIndex - 1];  // ...}

這目前運作得還不錯,但再次強調,長遠來看,我希望找出一個更簡單的解決方案,讓我們可以在 getStaticProps 中僅載入下一篇和前一篇的文章,而不是載入全部。

Hashicorp 有一個有趣的函式庫,旨在讓 MDX 檔案能夠像資料來源一樣被處理,它叫做 Next MDX Remote,我們未來可能會探索它。它應該可以讓我們切換到基於動態 slug 的路由,這樣我們就可以在 getStaticProps 中存取目前的 pathname,並給予我們更多的能力。

總結

總體來說,用 Next.js 建構這個小網站是一次有趣的學習體驗。我總是驚訝於許多工具中看似簡單的事情最終變得如此複雜,但我非常看好 Next.js 的未來,並期待在未來幾個月用它來建構下一版的 tailwindcss.com

如果你有興趣查看這個部落格的程式碼庫,甚至是提交 pull request 來簡化我上面提到的任何事情,請查看 GitHub 上的儲存庫

想討論這篇文章嗎?在 GitHub 上討論 →

直接在您的收件匣中接收我們所有的更新資訊。
訂閱我們的電子報。