在本文中,我们将使用 Next.js 构建一个静态博客框架,其设计和结构灵感来自 Jekyll。我一直很喜欢 Jekyll 如何让初学者更容易地设置博客,同时还为高级用户提供了对博客各个方面的强大控制能力。
近年来,Next.js 的出现,加上 React 的流行,为静态博客提供了新的探索途径。Next.js 使基于文件系统本身构建静态网站变得超级简单,几乎不需要配置。
典型的 Jekyll 博客的基本目录结构如下所示
.
├─── _posts/ ...blog posts in markdown
├─── _layouts/ ...layouts for different pages
├─── _includes/ ...re-usable components
├─── index.md ...homepage
└─── config.yml ...blog config
我们的想法是尽可能围绕此目录结构设计框架,以便通过简单地重用博客中定义的帖子和配置,更容易地将博客从 Jekyll 迁移。
对于不熟悉 Jekyll 的人来说,它是一个静态站点生成器,可以将您的纯文本转换为静态网站和博客。参考 快速入门指南 以开始使用 Jekyll。
本文还假设您具备 React 的基本知识。如果没有,React 的 入门 页面是您的最佳起点。
安装
Next.js 由 React 提供支持,并用 Node.js 编写。因此,我们需要先安装 npm,然后再将 next
、react
和 react-dom
添加到项目中。
mkdir nextjs-blog && cd $_
npm init -y
npm install next react react-dom --save
为了在命令行上运行 Next.js 脚本,我们必须将 next
命令添加到 package.json
文件的 scripts
部分。
"scripts": {
"dev": "next"
}
现在,我们可以在命令行上首次运行 npm run dev
。让我们看看会发生什么。
$ npm run dev
> [email protected] dev /~user/nextjs-blog
> next
ready - started server on http://localhost:3000
Error: > Couldn't find a `pages` directory. Please create one under the project root
编译器正在抱怨项目根目录中缺少 pages 目录。我们将在下一节中了解 pages 的概念。
页面的概念
Next.js 围绕页面的概念构建。每个页面都是一个 React 组件,可以是 .js
或 .jsx
类型,它根据文件名映射到路由。例如
File Route
---- -----
/pages/about.js /about
/pages/projects/work1.js /projects/work1
/pages/index.js /
让我们在项目的根目录中创建 pages
目录,并使用一个基本的 React 组件填充我们的第一个页面 index.js
。
// pages/index.js
export default function Blog() {
return <div>Welcome to the Next.js blog</div>
}
再次运行 npm run dev
启动服务器,并在浏览器中导航到 http://localhost:3000
以首次查看您的博客。

开箱即用,我们得到
- 热重载,因此我们无需为每次代码更改刷新浏览器。
/pages/**
目录中所有页面的静态生成。/public/**
目录中资产的静态文件服务。- 404 错误页面。
导航到 localhost 上的随机路径以查看 404 页面。如果您需要自定义 404 页面,则 Next.js 文档提供了很好的信息。

动态页面
具有静态路由的页面可用于构建主页、关于页面等。但是,为了动态构建所有帖子,我们将使用 Next.js 的 动态路由 功能。例如
File Route
---- -----
/pages/posts/[slug].js /posts/1
/posts/abc
/posts/hello-world
任何路由,例如 /posts/1
、/posts/abc
等,都将与 /posts/[slug].js
匹配,并且 slug 参数将作为查询参数发送到页面。这对于我们的博客帖子特别有用,因为我们不想为每个帖子创建一个文件;相反,我们可以动态传递 slug 来呈现相应的帖子。
博客的结构
现在,由于我们了解了 Next.js 的基本构建块,让我们定义博客的结构。
.
├─ api
│ └─ index.js # fetch posts, load configs, parse .md files etc
├─ _includes
│ ├─ footer.js # footer component
│ └─ header.js # header component
├─ _layouts
│ ├─ default.js # default layout for static pages like index, about
│ └─ post.js # post layout inherts from the default layout
├─ pages
│ ├─ index.js # homepage
| └─ posts # posts will be available on the route /posts/
| └─ [slug].js # dynamic page to build posts
└─ _posts
├─ welcome-to-nextjs.md
└─ style-guide-101.md
博客 API
一个基本的博客框架需要两个 API 函数:
- 一个函数用于获取
_posts
目录中所有帖子的元数据 - 一个函数用于为给定的
slug
获取单个帖子,包含完整的 HTML 和元数据
可选地,我们还想让站点的所有配置定义在 config.yml
中,以便在所有组件中可用。因此,我们需要一个函数将 YAML 配置解析为本机对象。
由于我们将处理许多非 JavaScript 文件,如 Markdown(.md
)、YAML(.yml
)等,我们将使用 raw-loader
库将这些文件加载为字符串,以便更容易地处理它们。
npm install raw-loader --save-dev
接下来,我们需要告诉 Next.js 在导入 .md 和 .yml 文件格式时使用 raw-loader,方法是在项目的根目录中创建 next.config.js
文件(有关此内容的更多信息)。
module.exports = {
target: 'serverless',
webpack: function (config) {
config.module.rules.push({test: /\.md$/, use: 'raw-loader'})
config.module.rules.push({test: /\.yml$/, use: 'raw-loader'})
return config
}
}
Next.js 9.4 引入了 用于相对导入的别名,这有助于清理由相对路径引起的导入语句杂乱。要使用别名,请在项目的根目录中创建一个 jsconfig.json
文件,指定基本路径和项目所需的所有模块别名。
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@includes/*": ["_includes/*"],
"@layouts/*": ["_layouts/*"],
"@posts/*": ["_posts/*"],
"@api": ["api/index"],
}
}
}
例如,这允许我们通过仅使用以下内容来导入布局
import DefaultLayout from '@layouts/default'
获取所有帖子
此函数将读取 _posts
目录中的所有 Markdown 文件,使用 gray-matter 解析帖子开头定义的前端内容,并返回所有帖子的元数据数组。
// api/index.js
import matter from 'gray-matter'
export async function getAllPosts() {
const context = require.context('../_posts', false, /\.md$/)
const posts = []
for(const key of context.keys()){
const post = key.slice(2);
const content = await import(`../_posts/${post}`);
const meta = matter(content.default)
posts.push({
slug: post.replace('.md',''),
title: meta.data.title
})
}
return posts;
}
典型的 Markdown 帖子如下所示
---
title: "Welcome to Next.js blog!"
---
**Hello world**, this is my first Next.js blog post and it is written in Markdown.
I hope you like it!
由 ---
概述的部分称为前端内容,它包含帖子的元数据,如标题、永久链接、标签等。以下是输出
[
{ slug: 'style-guide-101', title: 'Style Guide 101' },
{ slug: 'welcome-to-nextjs', title: 'Welcome to Next.js blog!' }
]
确保您首先使用命令 npm install gray-matter --save-dev
从 npm 安装 gray-matter 库。
获取单个帖子
对于给定的 slug,此函数将定位 _posts
目录中的文件,使用 marked 库解析 Markdown,并返回包含元数据的输出 HTML。
// api/index.js
import matter from 'gray-matter'
import marked from 'marked'
export async function getPostBySlug(slug) {
const fileContent = await import(`../_posts/${slug}.md`)
const meta = matter(fileContent.default)
const content = marked(meta.content)
return {
title: meta.data.title,
content: content
}
}
示例输出
{
title: 'Style Guide 101',
content: '<p>Incididunt cupidatat eiusmod ...</p>'
}
确保您首先使用命令 npm install marked --save-dev
从 npm 安装 marked 库。
配置
为了重新使用 Jekyll 配置以创建我们的 Next.js 博客,我们将使用 js-yaml
库解析 YAML 文件,并将此配置导出,以便它可以在组件之间使用。
// config.yml
title: "Next.js blog"
description: "This blog is powered by Next.js"
// api/index.js
import yaml from 'js-yaml'
export async function getConfig() {
const config = await import(`../config.yml`)
return yaml.safeLoad(config.default)
}
确保您首先使用命令 npm install js-yaml --save-dev
从 npm 安装 js-yaml
。
包含
我们的 _includes
目录包含两个基本的 React 组件,<Header>
和 <Footer>
,它们将用于 _layouts
目录中定义的不同布局组件。
// _includes/header.js
export default function Header() {
return <header><p>Blog | Powered by Next.js</p></header>
}
// _includes/footer.js
export default function Footer() {
return <footer><p>©2020 | Footer</p></footer>
}
布局
我们在 _layouts
目录中拥有两个布局组件。一个是 <DefaultLayout
>,它是所有其他布局组件的基础布局。
// _layouts/default.js
import Head from 'next/head'
import Header from '@includes/header'
import Footer from '@includes/footer'
export default function DefaultLayout(props) {
return (
<main>
<Head>
<title>{props.title}</title>
<meta name='description' content={props.description}/>
</Head>
<Header/>
{props.children}
<Footer/>
</main>
)
}
第二个布局是 <PostLayout>
组件,它将用文章标题覆盖 <DefaultLayout>
中定义的标题,并渲染文章的 HTML。它还包含一个指向主页的链接。
// _layouts/post.js
import DefaultLayout from '@layouts/default'
import Head from 'next/head'
import Link from 'next/link'
export default function PostLayout(props) {
return (
<DefaultLayout>
<Head>
<title>{props.title}</title>
</Head>
<article>
<h1>{props.title}</h1>
<div dangerouslySetInnerHTML={{__html:props.content}}/>
<div><Link href='/'><a>Home</a></Link></div>
</article>
</DefaultLayout>
)
}
next/head
是一个内置组件,用于将元素追加到页面的 <head>
中。next/link
是一个内置组件,用于处理页面目录中定义的路由之间的客户端转换。
主页
作为首页的一部分,我们将列出 _posts
目录中的所有文章。列表将包含文章标题和指向各个文章页面的永久链接。首页将使用 <DefaultLayout>
,我们将导入首页中的配置,将 title
和 description
传递给布局。
// pages/index.js
import DefaultLayout from '@layouts/default'
import Link from 'next/link'
import { getConfig, getAllPosts } from '@api'
export default function Blog(props) {
return (
<DefaultLayout title={props.title} description={props.description}>
<p>List of posts:</p>
<ul>
{props.posts.map(function(post, idx) {
return (
<li key={idx}>
<Link href={'/posts/'+post.slug}>
<a>{post.title}</a>
</Link>
</li>
)
})}
</ul>
</DefaultLayout>
)
}
export async function getStaticProps() {
const config = await getConfig()
const allPosts = await getAllPosts()
return {
props: {
posts: allPosts,
title: config.title,
description: config.description
}
}
}
getStaticProps
在构建时被调用,通过将 props
传递给页面的默认组件来预渲染页面。我们使用此函数在构建时获取所有文章的列表,并在首页上渲染文章存档。

文章页
此页面将渲染文章的标题和内容,该文章的 slug
是作为 context
的一部分提供的。文章页将使用 <PostLayout>
组件。
// pages/posts/[slug].js
import PostLayout from '@layouts/post'
import { getPostBySlug, getAllPosts } from "@api"
export default function Post(props) {
return <PostLayout title={props.title} content={props.content}/>
}
export async function getStaticProps(context) {
return {
props: await getPostBySlug(context.params.slug)
}
}
export async function getStaticPaths() {
let paths = await getAllPosts()
paths = paths.map(post => ({
params: { slug:post.slug }
}));
return {
paths: paths,
fallback: false
}
}
如果页面具有动态路由,Next.js 需要在构建时知道所有可能的路径。getStaticPaths
提供了必须在构建时渲染为 HTML 的路径列表。fallback 属性确保如果访问路径列表中不存在的路由,它将返回一个 404 页面。

生产就绪
在 package.json
的 scripts
部分中添加以下 build
和 start
命令,然后运行 npm run build
,然后运行 npm run start
来构建静态博客并启动生产服务器。
// package.json
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
本文中的所有源代码都可以在 此 GitHub 存储库 中找到。您可以随意将其克隆到本地并试用它。存储库还包含一些基本的占位符,用于将 CSS 应用于您的博客。
改进
虽然博客是可用的,但对于大多数普通情况来说可能过于基础。扩展框架或 提交补丁 以包含更多功能,例如
- 分页
- 语法高亮
- 文章的类别和标签
- 样式
总的来说,Next.js 在构建静态网站(如博客)方面非常有前景。结合其 导出静态 HTML 的能力,我们可以构建一个真正独立的应用程序,无需服务器!
这是一个构建您的下一个网站的绝佳工具。它具有许多出色的功能和优势,可以使 Nextjs 成为构建下一个 Web 应用程序的首选。
这是一个很棒的博客,适合软件工程师。他们喜欢用 React 原子符号和 Node 尝试新想法。Next.js 是一个很棒的新框架,用于构建通用 React 应用程序。感谢分享这篇很棒的文章。
嗨,我正在按照本教程操作。我已经完成了最后的步骤,不幸的是,我无法使我的 HTML 标签正确渲染为元素,我看到了
和 例如在我的博客文章中。所有代码本质上都相同,除了我没有使用 YAML。感谢
我想使用它,但不确定 “…js 文件” 的路径应该是什么。我使用 npm 安装了它。控制台抛出了一个错误,指出 .js 文件的路径错误。
我在 getAllPosts 函数中遇到了一个问题,我认为这里可能存在问题。我无法使用 require.context,因为该函数在服务器端运行。我使用了 readdir,这是我的解决方案
您的代码仍然在服务器端运行,因为您无法在客户端使用 fs。上面的解决方案有效,您可能需要添加 @types/webpack-env
请帮助。
错误:无法找到模块‘./osts/style-guide-101.md’
api\index.js (10:24) @ async getAllPosts
8 | for(const key of context.keys()){
9 | const post = key.slice(2);