作为一个团队,我们相信我们的每一个作品都应该 被一篇博客文章来封存。强迫自己为每个项目撰写一篇简短的公告文章,充当内置的质量检查,确保我们对外宣布它的时候,项目都没有被称为“完成”。
问题是直到今天,我们实际上没有地方可以发布这些文章!
选择平台
我们是一个开发者团队,因此自然没有办法说服自己使用现成的东西,而选择用 Next.js 构建一些简单且自定义的东西。
Next.js 有很多值得喜欢的地方,但我们决定使用它的主要原因是它对 MDX 的支持很好,这正是我们希望用来撰写文章的格式。
# 我的第一篇 MDX 文章MDX 是一种非常酷的撰写格式,因为它允许你在你的 Markdown 中嵌入 React 组件:<MyComponent myProp={5} />这多酷啊?
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.jsnext.config.jspackage.jsonpostcss.config.jsREADME.mdtailwind.config.js
这让我们可以将与那篇文章相关的任何资产都放在同一个文件夹中,并利用 webpack 的 file-loader 直接将这些资产导入文章中。
元数据
我们将有关每篇文章的元数据存储在一个 meta
对象中,并在每个 MDX 文件的顶部导出:
import { bradlc } from "@/app/blog/authors";import openGraphImage from "./card.jpeg";export const meta = { title: "为 Tailwind CSS IntelliSense 引入 linting", description: `今天,我们发布了 Visual Studio Code 的 Tailwind CSS IntelliSense 扩展的新版本,为您的 CSS 和标记添加了 Tailwind 特定的 linting。`, date: "2020-06-23T18:52:03Z", authors: [bradlc], image: openGraphImage, discussion: "https://github.com/tailwindcss/tailwindcss/discussions/1956",};// 文章内容在这里
这里是我们定义文章标题(用于文章页面上实际的 h1
和页面标题)、描述(用于 Open Graph 预览)、发布日期、作者、Open Graph 图像,以及指向 GitHub 讨论线程的链接。
我们将所有作者数据存储在一个单独的文件中,该文件仅包含每个团队成员的姓名、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: "一个来自不在团队中的人的访客帖子示例", authors: [ { name: "Simon Vrachliotis", twitter: "@simonswiss", avatar: "https://pbs.twimg.com/profile_images/1160929863/n510426211_274341_6220_400x400.jpg", }, ], // ...};
这使我们能方便地保持作者信息的同步,提供一个中央的真实来源,但不牺牲任何灵活性。
显示文章预览
我们想在主页上显示每篇文章的预览,而这竟然变成了一个相当具有挑战性的问题。
本质上,我们想使用 Next.js 的 getStaticProps
功能在构建时获取所有文章的列表,提取我们需要的信息,并将其传递给实际的页面组件进行渲染。
挑战在于我们希望在不实际导入每一篇文章的情况下做到这一点,因为这会导致我们主页的包中包含整个网站的每一篇博客文章,从而导致比必要更大的包。现在还好,因为我们只有几篇文章,但一旦你有数十甚至数百篇文章,那就是很多浪费的字节。
我们尝试了几种不同的方法,但我们最终决定使用 webpack 的 resourceQuery 功能结合几个自定义加载器,使其能够以两种格式加载每篇博客文章:
- 整个文章,用于文章页面。
- 文章预览,在这里我们加载主页所需的最少数据。
我们设置的方式是,任何时候我们在某个文章的导入末尾添加 ?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 = { // ...}这是文章的开头,我们希望在主页上显示的信息。<!--more-->之后的任何东西都不会包含在包中,除非你实际查看那篇文章。
以优雅的方式解决这个问题是相当具有挑战性的,但最后能想出一个解决方案,让我们在一个文件中保持所有内容,而不是为预览和实际文章内容使用单独的文件,这真是太酷了。
生成下一篇/上一篇文章链接
构建这个简单网站时,我们遇到的最后一个挑战是能够在查看单篇文章时,包括指向下一篇和上一篇文章的链接。
从根本上说,我们需要做的是加载所有文章(理想情况下是在构建时),在该列表中找到当前文章,然后抓取前一篇和后一篇文章,以便我们能够将它们作为 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 的下一个版本。
如果你有兴趣查看这个博客的代码库,甚至提交一个请求以简化我提到的任何内容, 请查看 GitHub 上的代码库。
想要讨论这篇文章吗? 在 GitHub 上讨论 →