使用 Next.js 构建博客

Last updated:

Next.js 的 SSG 很适合用来创建内容型站点,因为它可以在打包时就将 markdown 编译成 HTML,这意味着站点的加载速度会很快。

这篇文章教你怎么做。

# Before Getting Started

开始之前,你可以从 这个 GitHub Pages 预览到最终效果。它的代码托管在 这个仓库,solution 分支的一系列 Commits 对应了下文的每一个小节,所以 Commits 也是一个 Step by step 的代码攻略。

# Setting Up the Project

初始项目已经为你搭建好了,克隆 这个仓库,然后开始。

# Rendering Post on the Web

项目预先创建好了 2 篇 Markdown 文章,它们在:

  |- ...
  |- post
+    |- build-blog-with-mdx
+    |- what-is-css-modules
  |- ...

接下来,你要把 build-blog-with-wdx 加载到网页中去。怎么做?直接用 Node.js 读取它,然后注入到网页中去,就像下面这样。

// 📄 src/app/page.tsx
import { reqPost } from '@/helper/post';

async function Page() {
  const post = await reqPost('/post/build-interative-blog/index.md');

  return <article>{post.content}</article>;
}

现在,文章渲染到网页上去了,虽然很简陋。

reqPost 是什么?一个使用 Node.js 来读取 Markdown 文件的工具函数,它已经预先写好了,它还会处理 Markdown 的 Front Matter 信息。

Front Matter 是什么?位于 Markdown 顶部的用栅栏语法包围起来的东西,适合用来存放诸如创建日期之类的信息,就像下面这样。

---
date: '1970-01-01T00:00:00+00:00'
---

Here's the main content...

# Beautify Post Page

页面现在很简陋,这是因为我直接把 Markdown 源码渲染到网页上去了,但这不是我想要的。怎样美化它呢?很简单,将 Markdown 转译成 HTML,然后再增加一些 CSS。

我使用 Hashicorp 的 next-mdx-remote 来做转译工作,这个第三方库简单、靠谱、还支持 RSC。

  // 📄 src/app/page.tsx
  import { reqPost } from "@/helper/post";
+ import { MDXRemote } from "next-mdx-remote/rsc";

  async function Page() {
    const post = await reqPost("/post/build-interative-blog/index.md");

+   return <MDXRemote source={post.content} />;
  }

为什么不用 Next.js 官方的 @next/mdx?因为在我写作的时候,它还有很多问题,但现在好多了。

为了更好的演示,我添加了 water.css,它是一套预定义好的全局样式。

  // 📄 src/app/layout.tsx
  <head>
+   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css" />
  </head>

# Adding Syntax Highlighting

语法高亮对可读性很重要。

我采用 shiki,因为它支持大部份语法,以及 VSCode 的所有颜色主题。语法高亮的原理是将代码字符串解析成抽象语法树,然后转译为 HTML,最后给各个元素赋予 CSS 样式。

我将代码高亮的工作封装为 CodeSnippet

// 📄 src/component/code-snippet/CodeSnippet.tsx
import { codeToHtml } from 'shiki';

type Props = React.DetailedHTMLProps<
  React.HTMLAttributes<HTMLPreElement>,
  HTMLPreElement
> & { children?: any };

async function CodeSnippet(props: Props) {
  // Extract the code string.
  const code = props.children.props.children;

  // Extract the code language type.
  const lang = props.children.props.className?.split('language-')[1] ?? '';

  // Convert the code string into an HTML string.
  const html = await codeToHtml(code, { lang, theme: 'github-dark-dimmed' });

  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

然后将 CodeSnippet 传递给 <MDXRemote />,它会接管 <MDXRemote /> 对代码块的处理工序。

  // 📄 src/app/page.tsx
  import { reqPost } from "@/helper/post";
  import { MDXRemote } from "next-mdx-remote/rsc";
+ import CodeSnippet from "@/component/code-snippet";

  async function Page() {
      const post = await reqPost("/post/build-interative-blog/index.md");
+     const components = { pre: CodeSnippet };

+     return <MDXRemote source={post.content} components={components} />;
  }

这样一来,语法高亮就做好了✨。

# Parsing JSX in Markdown

只要为 Markdown 解析器配置对应的 JSX 组件,那么就可以在 Markdown 中使用 JSX 了。

## How to use JSX in Markdown

<MousePosition />

How to use JSX in Markdown

[0, 0]

怎么做?首先在 Markdown 中引用这个自定义组件。

  // 📄 post/build-interative-blog/index.md
  ...
+ <MousePosition />
  ...

然后将 <MousePosition> 组件传递给解析器(<MDXRemote>),如此一来,就完成了。

  // 📄 src/app/page.tsx
  import CodeSnippet from "@/component/code-snippet";
+ import MousePosition from "$/post/build-interative-blog/component/mouse-position";

  async function Page() {
    const post = await reqPost("/post/build-interative-blog/index.md");
+   const components = { pre: CodeSnippet, MousePosition };

<MDXRemote /> 会将 Markdown 解析成抽象语法树,然后转译成 JSX,然后根据 components 来调用 JSX,这就是原理,<MousePosition /><CodeSnippet /> 都是如此生效的。另外,JavaScript 表达式也会在编译阶段被执行掉。

这就是 MDX,即 Markdown + JSX。

真正的博客会有很多篇文章,你需要使用 Next.js 的 Dynamic Routes 来实现一个动态的文章页,这需要重构 src/app 的目录结构。另外,还要创建一个文章清单页。

📦 实现代码:Commit - Add More Posts

# Extra | Adding an RSS Feed to the Site

RSS Feed 是什么?它是内容摘要,订阅器会通过检查它来判断站点更新与否,所以许多内容型站点都是配置它。比如 就是本站的 RSS Feed,它就是一串 XML 格式的字符串数据。

我采用 Next.js 的 Route Handlers 来自动生成 RSS Feed,当用户访问 https://www.jynxio.com/rss 时,服务器就会返回 RSS Feed。

📦 实现代码:Commit - Add RSS Feed

# Extra | Adding Metadata

Metadata 是那些不直接显示在网页上,但又非常重要的信息,比如浏览器标签的 icon 和 title、社媒的 Open Graph。

设置好 Open Graph 之后,你需要去社媒的开发平台激活你的 Open Graph,比如 Twitter - Card Validator

实现代码没有展示所有的 Metadata,如果需要,你可以从类型文件中找到所有的 Metadata,😉请检查这个地址:node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts

📦 实现代码:Commit - Metadata

# Extra | Deploy to GitHub Pages

Next.js 在默认情况下采用 SSR 模式,所以你不能将它直接部署到静态服务器上,因为这会无法工作,不过你可以直接部署到 Vercel 上,因为 Vercel 提供了开箱即用级别的支持。

Next.js 也支持 SSG 模式,这可以让你将项目部署到静态服务器上,比如 GitHub Pages。

📦 实现代码:Commit - Deploy to GitHub Pages