likes
comments
collection
share

Nextjs 构建极简博客

作者站长头像
站长
· 阅读数 1

项目依赖

环境

  • Nodejs >18.17

堆栈

React相关的依赖:

  • nextjs
  • tailwindcss
  • NextUI
  • next-themes

MDX相关的依赖:

  • next-mdx-remote
  • reading-time
  • gray-matter
  • rehype-prism-plus
  • remark-gfm

预览地址:blog-example

初始化项目

使用create-next-app创建项目,根据官方的建议:Nodejs版本要大于18.17

npx create-next-app@latest 

执行命令过程中,根据个人便好选择初始化参数;一路回车默认即可(除了配置别名,其他的都是默认)

等待一段时间,项目应该就初始化完成啦,之后删除以下文件中的无用代码。

  • app/page.tsx
  • app/globals.css
  • tailwind.config.ts

添加组件库

为了简化开发,引入了UI组件库,我选择了Tailwindcss+NextUI+next-themes

Tailwindcss

Tailwindcss 提供的CSS原子类可以直接用Nextjs的SSR模式中使用,Tailwindcss发展很快,社区也活跃。项目初始化时默认安装过这个东西,没装过的话,使用Tailwind CLI初始化一份配置文件即可。

NextUI

基于Tailwindcss的组件库有很多,我选择了NextUI,单纯因为个人觉得看起来还不错~ 你也可以选择其他组件库,或者纯手撸。

使用

NextUI集成也比较简单,官方都已经封装好了,首先要在tailwind.config.ts引入NextUI的模版,并且执行插件,接着使用NextUI封装好的NextUIProvider 在根布局包裹整个页面,以便在整个项目中生效。

next-themes

配置简单的明暗主题切换小工具,当然不需要也行,但是前期为了省心,就先用上啦,拒绝再造轮子~~

使用

  1. config.darkMode中配置主题切换方式

配置说明:

  • Next-theme
    • attribute:默认data-theme,对应tailwindcss中的darkMode配置
    • enableSystem :开启系统首选项,也就是系统自己暗黑模型(如果操作系统开启了并且浏览器支持的话)
    • defaultTheme :默认system,如果没有开启enableSystem,默认为light
    • disableTransitionOnChange :关闭切换主题时的动画

注意:因为开启了enableSystem,所以在使用useTheme钩子,可以使用resolvedTheme来获取当前模式是dark还是light,因为resolvedTheme可以识别系统主题,嫌麻烦也可以直接关闭这一项。

以下是详细步骤:

  1. 安装依赖
npm i @nextui-org/react framer-motion next-themes
# framer-motion 是NextUI的动画依赖
  1. Tailwind CSS 设置
// tailwind.config.ts
import { nextui } from "@nextui-org/react";
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}", // NextUI 配置
  ],
  darkMode: ["selector", '[data-theme="dark"]'], // 明暗主题配置:next-themes 默认修改的HTML属性data-theme来切换主题的
  theme: {
    extend: {},
  },
  plugins: [
    nextui() // NextUI 配置
  ],
};
export default config;

注意:一定要配置'[data-theme="dark"]' ,因为next-themes是基于这个属性来切换明暗主题的;当然可以修改这个属性,只需要将next-themes中的attribute和这里的配置一样即可

  1. 配置Provider

需要使用NextUIProviderThemeProvider包裹,这里直接在root组件包裹,也就是让全部页面生效。

// Provider.tsx
"use client";

import { NextUIProvider } from "@nextui-org/react";
import { ThemeProvider } from "next-themes";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider enableSystem disableTransitionOnChange>
      <NextUIProvider>{children}</NextUIProvider>
    </ThemeProvider>
  );
}

最后在root 布局组件中使用

// /app/layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Providers>
          <PageHeader />
          {children}
        </Providers>
      </body>
    </html>
  );
}

配置Markdown

这里主要借助next-mdx-remote加载和显示MDX文件,另外说明一下MDX有两大插件系统:

  • rehype 通过插件转换HTML的工具
  • remark 通过插件转换markdown的工具

本次暂时只用到了这俩大插件系统中的以下几个插件:

  • remark-gfm - 添加github风格的Markdown支持 -> 新版本有bug,只能使用3.0.1版本
  • rehype-prism-plus - 语法高亮,且支持更多功能

以及以下几个插件

  • gray-matter - 解析元数据
  • reading-time - 添加阅读时间的小工具
  1. 安装依赖
npm i next-mdx-remote rehype-prism-plus remark-gfm gray-matter reading-time
  1. 配置next-mdx-remote
import { MDXRemote } from "next-mdx-remote/rsc";
import rehypePrism from "rehype-prism-plus";
import remarkGfm from "remark-gfm";

function Page(){
  const content = "# hello, world"
  return <MDXRemote
          source={content}
          options={{
            parseFrontmatter: true,
            mdxOptions: {
              remarkPlugins: [remarkGfm as any], // 注入 remarkGfm插件后面还可以加入你想加入的 remark 插件
              rehypePlugins: [rehypePrism as any], // 注入 语法高亮插件同样可以添加更多rehype插件
            },
          }}
        />
}

代码中content 为Markdown文件内解析出来的数据,这里是占位,接下来使用Nodejs去解析.mdx文件中的数据

// mdx.ts
import fs from "fs";
import path from "path";
import matter from "gray-matter";
import readingTime from "reading-time";

const articlesDirectory = path.join(process.cwd(), "app/_articles");

// 获取 MDX/MD 原始数据
export function getMdxRawData(fileName: string, hasSuffix: boolean) {
  let fullPath = path.join(articlesDirectory, `${fileName}`);
  let suffix = hasSuffix // 判断是否有后缀,没有的话就加上后缀
    ? ""
    : fs.existsSync(`${fullPath}.mdx`)
    ? ".mdx"
    : ".md";
  const fileContnts = fs.readFileSync(`${fullPath}${suffix}`, "utf8");
  return fileContnts;
}

// 处理 MDX/MD 原始数据中的 frontmatter
export function getMdxFrontmatter(mdxRawData: string) {
  const { content, data } = matter(mdxRawData);
  return {
    content,
    frontmatter: data,
    readingTime: readingTime(content).text, // 计算阅读时间
  };
}

// 获取文章的所有信息
export function getArticlesData(fileName: string, hasSuffix = false) {
  return {
    ...getMdxFrontmatter(getMdxRawData(fileName, hasSuffix)),
    fileName: fileName.split(".").slice(0, -1).join("."), // 去除后缀
  };
}

// 获取 _articles 目录下的所有文章
export function getAllArticlesData() {
  const fileNames = fs.readdirSync(articlesDirectory);
  const allArticlesData = fileNames.map((fileName) => {
    return getArticlesData(fileName, true);
  });
  return allArticlesData;
}

之后完善React部分,直接来代码吧~

import { Log } from "@/components/Log";
import { getArticlesData } from "@/lib/mdx";
import { formatDateTime } from "@/lib/time";
import { MDXRemote } from "next-mdx-remote/rsc";
import rehypePrism from "rehype-prism-plus";
import remarkGfm from "remark-gfm";

const Page = async ({ params }: any) => {
  const { content, frontmatter, readingTime } = getArticlesData(params.id);
  return (
    <main className="container pb-24">
      <article className="prose !max-w-none dark:prose-invert">
        <MDXRemote
          source={content}
          options={{
            parseFrontmatter: true,
            mdxOptions: {
              remarkPlugins: [remarkGfm as any],
              rehypePlugins: [rehypePrism as any],
            },
          }}
        />
      </article>
    </main>
  );
};
export default Page;

到这里还差最后一步,还没有排版样式,直接使用@tailwindcss/typography插件

安装一下

npm install -D @tailwindcss/typography

然后在tailwind.config.ts中引入这个插件

module.exports = {
  // ...
  plugins: [
    nextui(), // NextUI 配置
    require("@tailwindcss/typography"), // markdown typography
  ],
}

最后是在需要使用排版样式的地方加上类名prose(这个是插件的默认类型,如果有冲突可以在插件上配置自己的类名)

<article className="prose !max-w-none dark:prose-invert"> // 这里支持了明暗主题,以及取消max-width
	// HTML 代码片段
</article>

以上一个使用Nextjs搭建的简单博客已经完成啦~

源码地址:blog-example

参考