使用 Next.js 构建 Tailwind 博客

Adam Wathan

作为一个团队,我们坚信我们所做的一切都应该是 已通过博客文章密封。我们强迫自己为每个项目撰写一篇简短的公告,这本身就是一种质量检查,确保在项目正式发布之前,我们不会将其称为 "done"。

¥One of the things we believe as a team is that everything we make should be sealed with a blog post. Forcing ourselves to write up a short announcement post for every project we work on acts as a built-in quality check, making sure that we never call a project "done" until we feel comfortable telling the world it's out there.

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

¥The problem was that up until today, we didn't actually have anywhere to publish those posts!

选择平台(Choosing a platform)

¥Choosing a platform

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

¥We're a team of developers so naturally there was no way we could convince ourselves to use something off-the-shelf, and opted to build something simple and custom with Next.js.

Next.js 有很多优点,但我们决定使用它的主要原因是它对 MDX 的出色支持,而 MDX 正是我们想要用来撰写文章的格式。

¥There are a lot of things to like about Next.js, but the primary reason we decided to use it is that it has great support for MDX, which is the format we wanted to use to author our posts.

# 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 写作的人机工程学。

¥MDX is really interesting because unlike regular Markdown, you can embed live React components directly in your content. This is exciting because it unlocks a lot of opportunities in how you communicate ideas in your writing. Instead of relying only on images, videos, or code blocks, you can build interactive demos and stick them directly between two paragraphs of content, without throwing away the ergonomics of authoring in Markdown.

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

¥We're planning to do a redesign and rebuild of the Tailwind CSS documentation site later this year and being able to embed interactive components makes a huge difference in our ability to teach how the framework works, so using our little blog site as a test project made a lot of sense.

内容组织(Organizing our content)

¥Organizing our content

我们首先将帖子写成简单的 MDX 文档,直接放在 pages 目录中。但最终我们意识到,几乎每篇帖子都需要关联资源,例如至少需要一张 Open Graph 图片。

¥We started by writing posts as simple MDX documents that lived directly in the pages directory. Eventually though we realized that just about every post would also have associated assets, for example an Open Graph image at the bare minimum.

把这些内容放在另一个文件夹中感觉有点草率,所以我们决定在 pages 目录中为每篇文章创建一个单独的文件夹,并将文章内容放在 index.mdx 文件中。

¥Having to store those in another folder felt a bit sloppy, so we decided instead to give every post its own folder in the pages directory, and put the post content in an index.mdx file.

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 将这些资源直接导入帖子。

¥This let us co-locate any assets for that post in the same folder, and leverage webpack's file-loader to import those assets directly into the post.

元数据(Metadata)

¥Metadata

我们将每篇博文的元数据存储在 meta 对象中,并导出到每个 MDX 文件的顶部:

¥We store metadata about each post in a meta object that we export at the top of each MDX file:

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 讨论区链接。

¥This is where we define the post title (used for the actual h1 on the post page and the page title), the description (for Open Graph previews), the publish date, the authors, the Open Graph image, and a link to the GitHub Discussions thread for the post.

我们将所有作者数据存储在一个单独的文件中,该文件仅包含每个团队成员的名称、Twitter 用户名和头像。

¥We store all of our authors data in a separate file that just contains each team member's name, Twitter handle, and avatar.

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,};

将作者对象实际导入到帖子中而不是通过某种标识符连接它的好处是,我们可以轻松地在需要时内联添加作者:

¥The nice thing about actually importing the author object into a post instead of connecting it through some sort of identifier is that we can easily add an author inline if we wanted to:

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",    },  ],  // ...};

这使我们能够通过为作者信息提供一个集中的数据源来轻松地保持同步,同时又不放弃任何灵活性。

¥This makes it easy for us to keep author information in sync by giving it a central source of truth, but doesn't give up any flexibility.

显示帖子预览(Displaying post previews)

¥Displaying post previews

我们希望在主页上显示每篇文章的预览,而这却出乎意料地具有挑战性。

¥We wanted to display previews for each post on the homepage, and this turned out to be a surprisingly challenging problem.

本质上,我们希望能够使用 Next.js 的 getStaticProps 功能在构建时获取所有帖子的列表,提取我们需要的信息,并将其传递给实际的页面组件进行渲染。

¥Essentially what we wanted to be able to do was use the getStaticProps feature of Next.js to get a list of all the posts at build-time, extract the information we need, and pass that in to the actual page component to render.

挑战在于,我们不想真正导入每个页面,因为这意味着我们主页的打包文件将包含整个网站的每一篇博文,从而导致打包文件比实际需要的要大得多。现在我们只有几篇帖子的时候,这也许不是什么大问题,但一旦你的帖子数量达到几十甚至几百,就会浪费很多字节。

¥The challenge is that we wanted to do this without actually importing every single page, because that would mean that our bundle for the homepage would contain every single blog post for the entire site, leading to a much bigger bundle than necessary. Maybe not a big deal right now when we only have a couple of posts, but once you're up to dozens or hundreds of posts that's a lot of wasted bytes.

我们尝试了几种不同的方法,但最终确定的是使用 webpack 的 resourceQuery 功能,并结合一些自定义加载器,以便能够以两种格式加载每篇博文:

¥We tried a few different approaches but the one we settled on was using webpack's resourceQuery feature combined with a couple of custom loaders to make it possible to load each blog post in two formats:

  1. 整篇文章,用于文章页面。

    ¥The entire post, used for post pages.

  2. 文章预览,我们会在其中加载首页所需的最少数据。

    ¥The post preview, where we load the minimum data needed for the homepage.

我们的设置方式是,每当我们在导入单个帖子的末尾添加 ?preview 查询时,我们都会返回该帖子的精简版本,其中仅包含元数据和预览摘录,而不是完整的帖子内容。

¥The way we set it up, any time we add a ?preview query to the end of an import for an individual post, we get back a much smaller version of that post that just includes the metadata and the preview excerpt, rather than the entire post content.

以下是自定义加载器的部分代码片段:

¥Here's a snippet of what that custom loader looks like:

{  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--> 标签中来定义每篇文章的摘录,从而允许我们编写完全独立于文章内容的摘录。

¥It lets us define the excerpt for each post either by sticking <!--​more--> after the intro paragraph, or by wrapping the excerpt in a pair of <!--​excerpt--> and <!--​/excerpt--> tags, allowing us to write an excerpt that's completely independent from the post content.

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.

以优雅的方式解决这个问题颇具挑战性,但最终我们找到了一个很酷的解决方案,让我们可以将所有内容保存在一个文件中,而不是分别使用单独的文件来保存预览和实际的帖子内容。

¥Solving this problem in an elegant way was pretty challenging, but ultimately it was cool to come up with a solution that let us keep everything in one file instead of using a separate file for the preview and the actual post content.

生成下一篇/上一篇帖子链接(Generating next/previous post links)

¥Generating next/previous post links

我们在构建这个简单网站时遇到的最后一个挑战是,当你查看单个帖子时,能够包含指向下一篇和上一篇帖子的链接。

¥The last challenge we had when building this simple site was being able to include links to the next and previous post whenever you're viewing an individual post.

本质上,我们需要做的是加载所有文章(最好是在构建时),在列表中找到当前文章,然后获取之前和之后的文章,以便将它们作为 props 传递给页面组件。

¥At its core, what we needed to do was load up all of the posts (ideally at build-time), find the current post in that list, then grab the post that came before and the post that came after so we could pass those through to the page component as props.

这比我们预想的要难,因为事实证明 MDX 目前并不像你通常使用的那样支持 getStaticProps。你实际上无法直接从 MDX 文件中导出它,而是必须将代码存储在单独的文件中,然后从那里重新导出。

¥This ended up being harder than we expected, because it turns out that MDX doesn't currently support getStaticProps the way you'd normally use it. You can't actually export it directly from your MDX files, instead you have to store your code in a separate file and re-export it from there.

我们不想在首页导入帖子预览时加载这些额外的代码,也不想在每篇帖子中重复这些代码,所以我们决定使用另一个自定义加载器将此导出代码添加到每篇帖子的开头:

¥We didn't want to load this extra code when just importing our post previews on the homepage, and we also didn't want to have to repeat this code in every single post, so we decided to prepend this export to the beginning of each post using another custom loader:

{  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, ''))    }),  ],}

我们还需要使用这个自定义加载器将这些静态属性传递给我们的 Post 组件,因此我们也添加了你在上面看到的额外导出。

¥We also needed to use this custom loader to actually pass those static props to our Post component, so we appended that extra export you see above as well.

但这并非唯一的问题。事实证明,getStaticProps 不会提供任何有关当前正在渲染页面的信息,因此在尝试确定下一篇和上一篇帖子时,我们无法知道正在查看的是哪篇帖子。我怀疑这个问题是可以解决的,但由于时间限制,我们选择在客户端进行更多工作,而不是在构建时进行更多工作,这样我们在尝试确定所需链接时才能真正了解当前的路线。

¥This wasn't the only issue though. It turns out getStaticProps doesn't give you any information about the current page being rendered, so we had no way of knowing what post we were looking at when trying to determine the next and previous posts. I suspect this is solvable, but due to time constraints we opted to do more of that work on the client and less at build time, so we could actually see what the current route was when trying to figure out which links we needed.

我们将所有帖子加载到 getStaticProps 中,并将它们映射到非常轻量的对象中,这些对象仅包含帖子的 URL 和帖子标题:

¥We load up all of the posts in getStaticProps, and map them to very lightweight objects that just contain the URL for the post, and the post title:

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 布局组件中,我们使用当前路由来确定下一篇和上一篇帖子:

¥Then in our actual Post layout component, we use the current route to determine the next and previous posts:

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 中只加载下一篇和上一篇帖子,而不是加载所有帖子。

¥This works well enough for now, but again long-term I'd like to figure out a simpler solution that lets us load only the next and previous posts in getStaticProps instead of the entire thing.

Hashicorp 开发了一个有趣的库,可以将 MDX 文件视为名为 Next MDX Remote 的数据源,我们将来可能会探索它。它应该允许我们切换到基于 slug 的动态路由,这样我们就能访问 getStaticProps 中的当前路径名,从而赋予我们更强大的功能。

¥There's an interesting library by Hashicorp designed to make it possible to treat MDX files like a data source called Next MDX Remote that we will probably explore in the future. It should let us switch to dynamic slug-based routing which would give us access to the current pathname in getStaticProps and give us a lot more power.

总结(Wrapping up)

¥Wrapping up

总的来说,用 Next.js 构建这个小网站是一次有趣的学习经历。I'我总是惊讶于这些工具看似简单的事情最终会变得如此复杂,但我'm 对 Next.js 的未来非常乐观,并期待在未来几个月内使用它来构建 tailwindcss.com 的下一个迭代版本。

¥Overall, building this little site with Next.js was a fun learning experience. I'm always surprised at how complicated seemingly simple things end up being with a lot of these tools, but I'm very bullish on the future of Next.js and looking forward to building the next iteration of tailwindcss.com with it in the months to come.

如果你有兴趣查看本博客的代码库,或者提交拉取请求以简化我上面提到的任何内容,请访问 查看 GitHub 上的仓库

¥If you're interested in checking out the codebase for this blog or even submitting a pull request to simplify any of the things I mentioned above, check out the repository on GitHub.

想要讨论这篇文章吗?Discuss this on GitHub →

¥Want to talk about this post? Discuss this on GitHub →

TailwindCSS v4.1 中文网 - 粤ICP备13048890号