yliu

时来天地皆同力,运去英雄不自由

Next.js 构建博客之博客搭建


image.png

  1. Next.js 构建博客之资源抓取
  2. Next.js 构建博客之博客搭建
  3. Next.js 构建博客之打包 SSG
  4. Next.js 构建博客之常见问题处理
  5. Next.js 构建博客之功能拓展
  6. Next.js 构建博客之自动构建

在上一篇 Next.js 构建博客之资源抓取 介绍了如何把 GitHub 上的 issues 和 labels 资源拉取下来,方便构建使用。下面就介绍如何使用 Next.js 来完成博客的搭建,以及介绍一下常见会遇到的一些问题。

如果你想看已经部署博客的地址可以点击查看,代码仓库地址点击查看

不过需要注意这里不会介绍博客的样式和布局要怎么设计,这个全凭心意。本节主要注重怎么把 issues 和 labels 进行展示,以及路由和详情页面的展示要使用什么技术,可能会遇到的问题等。

这里也不会花费时间来介绍 Next.js 相关的概念,阅读本节默认你已经知道 Next.js 要怎么使用了。

创建

这里按照文档的做法

cd packages
npx create-next-app@latest

然后根据提示一步步选择,这里我附一下自己的选择

What is your project named? view
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? No
Would you like to use `src/` directory? N
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/*)? Yes
What import alias would you like configured? @/*

不过 Next.js 的版本可能会在后面发生变化,以及 api 也可能会调整,这里贴一下自己在写这篇文章时的 Next.js 版本

"next": "14.0.2",

首页搭建

这里分析一下我们现在已经有的信息:

  1. 知道所有 issues
  2. 知道所有的 labels

其中每一个 issue 对应的就是一篇文章,这篇文章可能会存在多个 labels 下,labels 可以简单理解为栏目。

布局这块,首页和详情变化的部分只是中间区域不同

image-1.png

image-2.png

其他部分基本是复用的,所以这里就有了思路,在 Next.js 文档中 app/layout.tsx 这个文件是布局文件,每个页面都会默认复用这个文件,除非你显示在子页面创建新的 layout.tsx 文件。

import "./styles/index.scss";
import { RightSide } from "./components/rightSide";
import Side from "./components/side";

export const dynamic = "error";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  return (
    <html lang="zh">
      <head></head>
      <body>
        <div id="qzhai-net" className="wp qzhai-net">
          <Side></Side>
          <div className="qzhai-net-main">{children}</div>
          <RightSide></RightSide>
        </div>
      </body>
    </html>
  );
}

之后就是在每次路由变化的时候改变 layout.tsx 下的 children 即可,整个博客系统大概存在 4 个路由:

  • 首页,路由对应 /
  • pages 页面,跟首页类似,但是首页路由是 /,如果跳转到第二页就会跳转在这里,路由会变成 page/2,后面的 2 代表具体的页数
  • 详情页面,从首页点击文章进行详情,会跳转到具体的详情页面,对应路由为 details/id
  • 分类页面,文章会存在分页,对应路由为 types/id

这里首页的搭建其实跟具体的 pages 页面是一回事,我们新建一个 components/content/index.tsx 文件,把相同的功能抽离出来。

这里首页数据其实就是固定展示前面 20 条,如果存在分页也就是把 issues 的数据进行截取条数进行展示。基于分析的这个情况,我们定义一个 Props,它的内容如下:

type Props = {
  // 当前页数
  page: number,
};

之后安装一下依赖,把上一节爬取下来的数据拿来使用

pnpm add @blog/side-effect

之后简单描述一下 content/index.tsx

import React, { FC } from "react";
import all from "@blog/side-effect";

interface Props {
  page: number;
}

export const Content: FC<Props> = ({ page }) => {
  const data = all.issuesData.slice((page - 1) * 20, page * 20);

  return (
    <ul>
      {data.map((f) => {
        return (
          <li key={f.id}>
            <p>{f.title}</p>
            ...
            <p>{f.created_at}</p>
          </li>
        );
      })}
    </ul>
  );
};

之后在 app/page.tsx 中,直接这个页面传递 Content 组件 page 传递 1,就完成了调用,不过在实际的开发中还需要考虑其他额外的情况,例如分页、文章简介截取、图片提取等,这里会专门出一篇文章进行介绍,这里快速略过关注怎么来构建。

详情

打开爬取 data.json,可以看到文章内容储存在 body 中,那么思路就很简单了,在首页中添加一个 a 标签,在跳转过来的时候携带文章对应的 id,之后把对应的数据进行渲染出来即可。

image-3.png

使用 a 标签在 Next.js 中是不推荐的行为,Next.js 提供了 Link 标签,它的作用跟 a 标签一致,遇到需要路由跳转的使用 Link 即可。

之后创建 details/[id]/page.tsx 页面,这里 [id] 代表了一个动态参数,会根据传递过来的 id 不同来进行变化,在 Next.js 中还有其他可选参数等,这里不展开一一介绍了,之后重点讲一下详情页面要怎么展示 md 内容。

import { useMemo } from "react";
import data, { classification } from "@blog/side-effect";

interface Params {
  id: string;
}
interface Props {
  params: Params;
  searchParams: Record<string, string>;
}

export default function Page({ params: { id } }: Props) {
  const current = useMemo(() => {
    return data.issuesData.find((f) => f.id === +id);
  }, [id]);
}

上面直接根据 id 来对 issues 进行搜索,之后从 current.body 就可以获取到具体的文章内容,这里推荐使用 bytemd 作为内容展示,bytemd 就是掘金同款编辑器,它包含两部分 Editor, Viewer,这里显然只需要使用 Viewer。

image-4.png

对于样式之类的可以参考这个仓库 juejin-markdown-themes

这一步搭建完成大概会得到一个这样的界面

image-5.png

说明内容已经被成功渲染了。

如果在使用 bytemd 过程中提示,提示使用了 useEffect 之类的钩子,直接在文件顶部添加

"use client";
// ...

把当前组件渲染方式变成客户端渲染。

栏目

对于栏目页面则更简单一些,只需要把爬取下来的 labels 的标签进行展示即可,下面是一个示例

// side.tsx

import data from "@blog/side-effect";
import Link from "next/link";
import { classification } from "@blog/side-effect";

export const Side = () => {
  return (
    <>
      <h4>分类查看</h4>
      <ul>
        {data.label.map((item) => {
          const length = classification.get(`${item.id}`)?.length || 0;
          return (
            <li key={item.id}>
              <Link href={`/types/${item.id}`} title={item.description}>
                {item.name}
                <span className="types-notes">[{length}]</span>
              </Link>
            </li>
          );
        })}
      </ul>
    </>
  );
};

分页

对于分页上面其实也有提及,首页以及分页和点击栏目跳转的页面其实都高度依赖 content 这个组件,所以这个组件的灵活性需要保证。

  1. 需要保证可以自定义 header 区域,例如在首页中,展示的应当是最新文章;
  2. 如果是栏目跳转那么需要展示对应的栏目名称;

其他的其实就是把对应 pages/id,传递给 content 这个组件即可,下面是一个示例

// page/[page]/page.tsx

interface Params {
  // 当前页数
  page: string;
}

interface Props {
  params: Params;
  searchParams: Record<string, string>;
}

export default function Page(props: Props) {
  const {
    params: { page },
  } = props;

  return (
    <>
      <Content page={+page}></Content>
    </>
  );
}

最后

这节简单探讨了下如何结合 issues 和 labels 进行使用,在下一节 Next.js 构建博客之打包 SSG 会介绍如何构建 SSG 的应用。

如果文章有错别字之类也欢迎指出。