使用 Next.js 构建博客

Avatar of Pankaj Parashar
Pankaj Parashar

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

在本文中,我们将使用 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,然后再将 nextreactreact-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 以首次查看您的博客。

Screenshot of the homepage in the browser. The content says welcome to the next.js blog.

开箱即用,我们得到

  • 热重载,因此我们无需为每次代码更改刷新浏览器。
  • /pages/** 目录中所有页面的静态生成。
  • /public/** 目录中资产的静态文件服务。
  • 404 错误页面。

导航到 localhost 上的随机路径以查看 404 页面。如果您需要自定义 404 页面,则 Next.js 文档提供了很好的信息

Screenshot of the 404 page. It says 404 This page could not be found.

动态页面

具有静态路由的页面可用于构建主页、关于页面等。但是,为了动态构建所有帖子,我们将使用 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>,我们将导入首页中的配置,将 titledescription 传递给布局。

// 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 传递给页面的默认组件来预渲染页面。我们使用此函数在构建时获取所有文章的列表,并在首页上渲染文章存档。

Screenshot of the homepage showing the page title, a list with two post titles, and the footer.

文章页

此页面将渲染文章的标题和内容,该文章的 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 页面。

Screenshot of the blog page showing a welcome header and a hello world blue above the footer.

生产就绪

package.jsonscripts 部分中添加以下 buildstart 命令,然后运行 npm run build,然后运行 npm run start 来构建静态博客并启动生产服务器。

// package.json
"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}

本文中的所有源代码都可以在 此 GitHub 存储库 中找到。您可以随意将其克隆到本地并试用它。存储库还包含一些基本的占位符,用于将 CSS 应用于您的博客。

改进

虽然博客是可用的,但对于大多数普通情况来说可能过于基础。扩展框架或 提交补丁 以包含更多功能,例如

  • 分页
  • 语法高亮
  • 文章的类别和标签
  • 样式

总的来说,Next.js 在构建静态网站(如博客)方面非常有前景。结合其 导出静态 HTML 的能力,我们可以构建一个真正独立的应用程序,无需服务器!