使用 Next.js 构建 Tailwind 博客

Adam Wathan

作为一个团队,我们坚信我们所做的每一件事都应该通过一篇博客文章来宣告。强迫自己为我们做的每个项目撰写一篇简短的公告文章,可以作为一种内置的质量检查,确保我们永远不会在一个项目“完成”之前就安心地告诉全世界它已经发布了。

问题是,直到今天,我们实际上还没有任何地方可以发布这些文章!

选择平台

我们是一个开发者团队,所以很自然地,我们无法说服自己使用现成的解决方案,而是选择使用 Next.js 构建一些简单而自定义的东西。

关于 Next.js,有很多值得喜欢的地方,但我们决定使用它的主要原因是它对 MDX 提供了很好的支持,而 MDX 正是我们想要用来创作文章的格式。

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

MDX 非常有趣,因为与普通的 Markdown 不同,您可以直接在内容中嵌入实时的 React 组件。这令人兴奋,因为它为你在写作中交流想法的方式解锁了很多机会。您可以构建交互式演示,并将它们直接放在两个内容段落之间,而无需抛弃 Markdown 创作的人体工程学,而不是仅仅依赖图像、视频或代码块。

我们计划在今年晚些时候对 Tailwind CSS 文档站点进行重新设计和重建,能够嵌入交互式组件对于我们教授框架的工作原理至关重要,因此将我们的小博客站点作为一个测试项目非常有意义。

组织我们的内容

我们首先将文章编写为简单的 MDX 文档,这些文档直接位于 pages 目录中。但最终我们意识到,几乎每篇文章都会有相关的资源,例如,至少需要一个 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.js
next.config.js
package.json
postcss.config.js
README.md
tailwind.config.js

这使我们可以将该文章的任何资源放在同一个文件夹中,并利用 webpack 的 file-loader 将这些资源直接导入到文章中。

元数据

我们将每篇文章的元数据存储在一个 meta 对象中,我们在每个 MDX 文件的顶部导出该对象

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 功能在构建时获取所有文章的列表,提取我们需要的信息,并将这些信息传递到实际的页面组件进行渲染。

挑战在于我们希望在不实际导入每个页面的情况下执行此操作,因为这将意味着我们主页的 bundle 将包含整个站点的每篇博客文章,从而导致 bundle 比必要的更大。当我们只有几篇文章时,这可能不是什么大问题,但是一旦你有多达几十篇或数百篇文章,那将浪费很多字节。

我们尝试了几种不同的方法,但我们最终确定使用 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 to
show on the homepage.
<!--​more-->
Anything after that is not included in the bundle unless
you 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 中访问当前路径名,并为我们提供更大的能力。

总结

总的来说,使用 Next.js 构建这个小站点是一次有趣的學習体验。我总是惊讶于很多看似简单的事情最终在使用这些工具时变得多么复杂,但我非常看好 Next.js 的未来,并期待在未来的几个月里使用它构建下一代 tailwindcss.com

如果您有兴趣查看此博客的代码库,甚至提交 pull request 来简化我上面提到的任何内容,请查看 GitHub 上的存储库

想讨论这篇文章吗?在 GitHub 上讨论 →

直接在您的收件箱中获取我们所有的更新。
注册我们的新闻通讯。

版权所有 © 2025 Tailwind Labs Inc.·商标政策