
摘要: 关于我是如何搭建博客的历程,和一些构建博客的技术选型
我个人在学习的过程中,喜欢记录笔记,这是因为我觉得记录笔记是一个很好的学习方式。通过记录笔记,我可以更好地理解和消化所学的知识,同时也可以在以后的学习中方便地查阅和复习。
起初我是使用纸质笔记,但是随着时间的推移,我发现纸质笔记的查找和整理都很麻烦。于是我购买ipad
和apple pencil,开始使用数字笔记。最常使用的软件是Noteability
和OneNote
。这两者各有千秋,Noteability配合apple pencil使用非常流畅。OneNote则是可以在不同设备上同步,结合学校提供的oneDrive学生账号使用非常方便。
但是以上笔记形式有几个问题:
因此我决定搭建一个属于自己的博客。而这个想法的起源是,我在B站上浏览到一个up主对于笔记记录的分享视频。
我了解过构建博客的几种方式:
github pages
和 Hexo
Next.js
结合vercel
最后我选择了Next.js
结合vercel
。原因如下:
Next.js
是一个基于React的框架,有文件系统路由和开箱即用的SSR(服务端渲染)功能,适合构建博客。vercel
是一个免费的云平台,支持静态网站和动态网站的托管,使用方便而对于博客文章的编写,我选择了mdx
。mdx相对于markdown的优势在于:可以在markdown中使用JSX语法,方便插入组件和交互式内容。Next.js对mdx的支持也很好,使用起来非常方便。对于转译mdx的工具,我选择了mdx-bundler
。它可以将mdx文件转译成React组件,并能够接入自定义插件,支持代码高亮等功能。其实next-mdx-remote
也可以实现类似的功能,但是mdx-bundler
是我接触的第一个转译工具,所以我选择了它。
在他人博客中有提到他选择mdx-bundler
的原因,但是我并没有实际比对过其他的转译工具,所以不做评价。
选择了Next.js和mdx-bundler后,我还需要选择一个UI框架。我选择了Tailwind CSS
。它是一个功能强大的CSS框架,提供了丰富的组件和样式,可以快速构建响应式网站。还有Antd
,这是一个组件种类十分丰富、使用方法易懂的组件库。
最后还有一些小功能,例如代码高亮样式库、图床网站、图标库、评论组件和搜索组件等。都能在我的github仓库中找到。
正如以上目录结构,app
目录下的page.js
是一个特殊的文件,它是Next.js的约定文件,用于定义路由和页面。layout.js
也是一个特殊的文件,用于定义布局和样式。page.js
和layout.js
的组合可以实现页面的嵌套和布局。
我的博客主要的功能就是记录笔记,我将笔记的内容放在blog
目录下的[category]\[slug]
,这个路径是个动态路由,用于匹配不同的文章和页面。category是文章的分类,slug是文章的标题。这样可以实现对文章的分类和管理。而page\[page]
是一个静态路由,用于匹配不同的页面页码。blog\page.js
主要用于罗列文章,以及实现搜索功能。blog\about
是关于我个人信息的页面。
最核心的功能是blog\[category]\[slug]\page.js
内的mdx转译和渲染。由于浏览器不能够渲染mdx语言,所以我们需要使用mdx-bundler
将mdx转译成React组件。然后在页面中渲染这个组件。
在Next.js官网文档中有详细搭建脚手架的步骤。
由于我是2025年4月开始搭建的博客,所以使用的是Next.js 15
版本。
npx create-next-app@latest my-blog
运行以上命令之后,会弹出一个交互式的命令行界面,要求你选择一些选项。
我第一次接触的编程语言是Python,它没有变量类型的声明,所以我个人是比较喜欢使用JavaScript。也因此在搭建脚手架时,我选择了JavaScript
。
接着我勾选了app路由
、ESlint
、Tailwind CSS
。这里值得一提的是,一开始我勾选了Turbopack
,但是在实际使用过程中,它频繁报错,而且vercel
build的过程中默认使用的是webpack
,所以我改用了后者。
创建完项目脚手架后,我们进入项目根目录my-blog
,对以下文件进行修改:
/** @type {import('next').NextConfig} */
const nextConfig = {
eslint: {
ignoreDuringBuilds: true,
},
webpack: (config) => {
// 添加 SVG 处理规则
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack']
});
return config;
},
images: {
dangerouslyAllowSVG: true,
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;"
}
}
module.exports = (nextConfig)
以上配置文件的作用是:
为什么要在生产环境中忽略ESlint错误?因为在build的过程中,ESlint会检查代码的规范性,如果有错误会导致build失败。而我在开发过程中缺少经验,使用了许多不规范的代码形式,导致build失败。选择忽略ESlint错误,能够使得build成功进行下去。
如果项目中需要用到SVG图片,并希望SVG图片能够被作为组件使用,那么就需要添加SVG处理规则。
import NextJsIcon from '@/icon/nextjs-fill.svg';
<NextJsIcon className="h-5 w-5 text-gray-800 dark:text-gray-100 transition-colors" />
再就是样式文件的配置:
/** @type {import('tailwindcss').Config} */
const colors = require('tailwindcss/colors')
const { fontFamily } = require('tailwindcss/defaultTheme')
module.exports = {
// tailwind css 能够作用的文件路径
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
'./pages/**/*.{js,ts,tsx}',
"./content/**/*.{md,mdx}",
'./Layouts/**/*.{js,ts,tsx}',
],
darkMode: 'class',
theme: {
extend: {
...加入你的自定义配置
}
}
}
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 加入你的自定义样式 */
最后还有简化路径的配置:
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}
至此,大致的项目脚手架搭建完成。
在app\blog\[category]\[slug]\page.js
中,我们需要使用mdx-bundler
将mdx文件转译成React组件。具体的代码如下:
const { code } = await bundleMDX({
source: mdxSource, // mdx文件的内容
cwd:path.join(process.cwd(), 'app', 'components', 'Plugins'), // 自定义插件的路径
mdxOptions: (options, frontmatter) => {
options.remarkPlugins = [...(options.remarkPlugins ?? []), ...[你想加入的插件]]
options.rehypePlugins = [...(options.rehypePlugins ?? []), ...[
你想加入的插件
]]
return options
},
esbuildOptions: options => {
options.outdir = path.join(process.cwd(), 'public')
options.write = true
return options
}
})
return {
code,
frontmatter: {
...post,
date: post.date,
}
}
...
const MDXComponent = getMDXComponent(code)
<MDXComponent components={{
自定义组件
}}/>
首先配置好转译器bundleMDX
,然后使用getMDXComponent
将mdx文件转译成React组件,最后在页面中渲染这个组件。
在app\blog\page.js
中,我们需要实现搜索功能。具体的代码如下:
export default function ListLayout({
posts, // 全部原始文章数据(始终基于完整数据集)
initialDisplayPosts = [], // 初始分页数据
pagination,
title
}) {
const [isLoading, setIsLoading] = useState(true)
const [searchValue, setSearchValue] = useState('')
const [selectedTags, setSelectedTags] = useState([])
useEffect(() => {
// 数据加载完成后关闭加载状态
setIsLoading(false)
}, [posts]) // 当 posts 数据变化时触发
// 标签点击处理(每次点击都基于完整数据集重新筛选)
const handleTagClick = (tag) => {
setSelectedTags(prev =>
prev.includes(tag)
? prev.filter(t => t !== tag) // 移除标签
: [...prev, tag] // 添加标签
)
}
const filteredPosts = posts.filter(post => {
const tagMatch = selectedTags.length === 0 ||
selectedTags.every(tag => post.tags?.includes(tag))
const searchMatch = (post.title + (post.summary || '') + (post.category || '') + (post.tags || ''))
.toLowerCase()
.includes(searchValue.toLowerCase())
return tagMatch && searchMatch
})
}
...
<span
key={tag}
className={`rounded-lg px-3 py-1 text-sm font-medium cursor-pointer transition-colors
${
selectedTags.includes(tag)
? 'bg-primary-400 text-white dark:bg-primary-300 dark:text-gray-900'
: 'bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-primary-600 dark:text-primary-300'
}`}
onClick={(e) => {
e.stopPropagation() // 阻止事件冒泡
handleTagClick(tag)
}}
>
{tag}
</span>
搜索的逻辑是:根据文章title
、summary
、category
和tags
进行搜索。使用useState
和useEffect
来管理搜索状态和加载状态。使用filter
方法对文章进行筛选。并且给tags一个点击按钮,能够让用户点击标签得到含有该标签的所有文章。
我选用prismjs及其样式文件。首先从仓库中选择一个你喜欢的代码高亮样式文件,引入到你的项目中。然后注册代码语言,这样代码就能高亮显示。
import '@/app/prism-dracula.css'
import { refractor } from 'refractor'
import js from 'refractor/lang/javascript'
import py from 'refractor/lang/python'
import css from 'refractor/lang/css'
import bash from 'refractor/lang/bash'
import json from 'refractor/lang/json'
import sql from 'refractor/lang/sql'
export function registerPrismLanguages() {
// 注册所有需要的语言
refractor.register(js)
refractor.register(py)
refractor.register(css)
refractor.register(bash)
refractor.register(json)
refractor.register(sql)
}
import { registerPrismLanguages } from '@/app/lib/lib.js'
registerPrismLanguages()
之前存储文章都是直接将.md
文件放入项目目录中,然后通过动态路由读取文章内容。这样的存储方式会使得整个项目过于臃肿,同时,调取文章内容也不够灵活。因此我决定选取一个数据库
作为文章内容的存储仓库。
网上有许多的数据库可以选择,挑选过后我选择了supabase
作为我的后端数据库。
为什么选择supabase
呢?首先supabase
集成到了vercel
上,我们能够十分方便地在vercel
的操作界面上配置和连接supbase
。除此之外,supabase
的官网上自带有数据库的可视化操作界面,不再需要别的软件介入。同时它是免费的。
那么有了数据库,如何提交数据到数据库呢?一开始我想的流程是:现在外部编辑器中撰写.mdx文件内容,然后开放一个接口,直接提交一整个文件。后来,我认为构建一个在线编辑和预览的页面更加合适。在这个页面中,执行数据库的增删改写,和内容的实时预览。
网上有很多开源的在线编辑器组件,我选用了monaco-editor
。挑选的原因也只是它最先被我搜索出来而已,再者,实际试用效果也不错。
'use client'
import React,{ useRef, useEffect, useCallback } from 'react';
import { Editor, loader } from '@monaco-editor/react';
// china加载方式,否则载入速度特别慢。
loader.config({
paths: { vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.33.0/min/vs/" }
});
有了在线编辑器,接着就是预览功能,由于有现成的mdx-bundler
代码,直接copy成Api,然后在page.js
中调取就行了。
有了在线编辑功能,那就很自然的要引入用户登录功能了。因为不能让所有人都能编辑他人笔记,所以需要用户鉴权。
完成登录功能其实是一个很复杂工作,但我们能够直接使用第三方库Auth.js
,引入github
就能很简单的完成登录和登出。
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
import { generateStableId } from "@/app/lib/utils"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [ GitHub ],
session:{
strategy: "jwt",
},
callbacks: {
jwt: async ({ token }) => {
return token
},
session: async ({ session, token }) => {
if (xxx) {
session.user.id = generateStableId(xxx)
}
return session
},
},
})
类似以上过程,我们可以自定义一个session.user.id作为一个用户的唯一标识,依据该加密id实现简单的鉴权功能。
其实这是个不太安全的鉴权步骤,更加安全的方法应该要配合supabase
的鉴权功能,但是对于后端数据库这方面我知之甚少,这也是我后续需要更新的部分。