Skip to content

kamomechan/moe-blog

Repository files navigation

moe-blog

简介

使用 nextjs 构建的萌萌 blog

lighthouse

home page:

screenshort

screenshort

post page:

screenshort

screenshort

favs page:

screenshort

screenshort

任务清单

  • 导航栏

  • 分页

  • markdown to html

  • 代码语法高亮/复制按钮

  • 文章侧边栏

  • RSS

  • VNDB 阅读列表

  • 类似 Pixiv 的图片缩放

  • 搜索框

  • 黑暗模式 (自动跟随系统主题)

  • 评论功能 (使用 Postgres 作为数据库)

部署

克隆仓库

git clone https://codeberg.org/nokutan/moe-blog.git

或者

git clone https://github.com/kamomechan/moe-blog.git

运行环境

下载 nodejs 最新的 LTS 版本

如果你用 GNU arch linux 可以运行下方命令从官方的 Extra 库下载

sudo pacman -S nodejs-lts-krypton npm

依赖

sudo npm i -g pnpm
pnpm i

配置

根据.env.example注释进行配置

cp .env.example .env
vim .env

配置数据库(可选)

如需开启评论,需要设置数据库。如果你使用的是云数据库托管服务,那么可以直接跳转到 #填充数据库(可选)

下载 postgres

如果你用 GNU arch linux 可以运行下方命令从官方的 Extra 库下载,这会同时创建名为 postgres 的 linux 用户

sudo pacman -S postgresql

接下来的操作你可以参考 postgres 官方文档,或者 arch wiki 的 postgres 条目配置 postgres,或者跟着下方的步骤

如果你用的是 btrfs 文件系统,需要运行下方命令禁用数据库目录的写时复制

sudo chattr +C /var/lib/postgres

切换到 postgres 用户的 shell 环境

sudo -iu postgres

初始化数据库集群

initdb --locale=C.UTF-8 --encoding=UTF8 -D /var/lib/postgres/data --data-checksums --auth-local=peer --auth-host=scram-sha-256

如果你看到下面的输出,那么就是成功了,使用 exit 返回到普通用户

Success. You can now start the database server using:

    pg_ctl -D /var/lib/postgres/data -l logfile start

启动并启用 postgresql.service 服务

systemctl enable --now postgresql

提示:如果你创建一个与 linux 用户名相同的数据库角色,那么后续连接 postgres 数据库 shell 时(psql),无需指定登录角色。例如,运行 psql -d <database_name>连接数据库时,当系统看到你没有通过-U选项指定数据库角色,会隐式将你的 linux 用户名作为数据库角色名 -U <your_linux_username>

以 postgres 用户身份,使用 crateuser 命令创建一个数据库角色

sudo -u postgres createuser <your_database_role>

以 postgres 用户身份, 使用 createdb 命令创建一个数据库

sudo -u postgres createdb -O <your_database_role> <your_database_name>

由于我们在初始化数据库集群时,传递了--auth-local=peer选项,这会在/var/lib/postgres/data/pg_hba.conf生成以下的配置

# TYPE  DATABASE        USER            ADDRESS                 METHOD

# "local" is for Unix domain socket connections only
local   all             all                                     peer

这里的意思是,在本地连接任意一个数据库时,使用 peer 方法进行连接,也就是说一个 linux 用户仅能与其同名的数据库角色进行连接。这里没理解没关系,举个例子,之前我们从 Arch 仓库下载 postgres 时会自动创建一个名为postgres的 linux 用户,相对应的数据库默认也有一个名为postgres数据库角色,因此我们可以以 postgres 身份通过名为 postgres 数据库角色来连接数据库,比如一个可行的例子为sudo -u postgres psql -U postgres -d <your_database_name>,一个不可行的例子为sudo -u alice psql -U postgres -d <your_database_name>。另外,这里第一个命令使用-U选项指定数据库角色名可以省略,因为如果没有传递-U,系统默认会将 linux 用户名作为数据库角色名

由于现有的pg_hba.conf配置,需要我们每创建一个数据库角色的同时,也要创建一个同名的 linux 用户,才能使用数据库角色进行连接,这样会导致产生不必要的 linux 用户,并且没有权限隔离,我们可以配置为名为postgres的数据库角色(或其他超级用户)使用 peer 认证方法进行本地连接,而其他角色使用 scram-sha-256 认证方法进行本地连接,这会在访问其他角色时需要输入密码。可以选择使用下方的配置替代上方的配置

# TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             postgres                                peer
local   all             all                                     scram-sha-256

重启postgresql.service,并以 postgres 身份连接数据库,使用 ALTER 命令为之后想要赋予 Login 权限的数据库角色添加密码(密码仅对具有 Login 权限的角色生效,也可以为没有此权限的角色定义密码)

systemctl restart postgresql
sudo -u postgres psql -d <your_database_name>
ALTER ROLE "<your_database_role>" WITH PASSWORD '<your_role_password>';

提示:ALTER 命令成功执行后会输出ALTER ROLE,如果没有,你可能结尾忘记了;

提示:你可能会注意到"<your_database_role>"会用引号包裹,这是因为 postgres 默认会将没有用引号包裹的标识符(如角色名、表名、列名等)自动转为小写,如果你的角色名包含大写,则务必加上引号

赋予数据库角色登录权限(安全起见,请不要为 postgres 角色或其他超级用户赋予 Login 权限,而是你新创建的角色)

ALTER ROLE "<your_database_role>" WITH LOGIN;

填充数据库(可选)

如需开启评论,还需要填充数据库

设置.env环境变量,如果你是用的是本地主机连接(localhost),需要设置 SSL 为 false,数据库默认端口为5432。如果是远程主机连接请务必设置 SSL 为 true,关于数据库如何开启 SSL 请参考官方文档,以及 arch wiki 的Configure PostgreSQL to be accessible from remote hosts 条目。如果你用的是云数据库托管服务,往往已经设置了 SSL

# required
POSTGRES_URL="postgresql://<your_database_role>:<your_role_password>@<your_host>:<your_port>/<your_database_name>"
# default:true
SSL=""

设置/login路由的用户名和密码,密码需要大于 18 位,如果记不住,可以使用自由开源的 keepassxc 作为密码管理器。设置用户凭证是用来判断你是否是管理员,会自动将密码 bcrypt 哈希加密存储到数据库

# required
USERNAME=""
# required
PASSWORD=""

开启评论功能,验证会话签名的密钥SESSION_SECRET,可以通过openssl命令,生成一个 32 个字符的随机字符串来获取

openssl rand -base64 32
# default:false (required)
COMMENTS="true"
# required
SESSION_SECRET=""

取消注释 app/seed/route.ts文件,并在开发环境中运行

pnpm run dev

访问/seed路由,如果出现Database seeded successfully,就代表数据库表和用户凭证成功填充啦 w

最后你可以选择删除/seed路由,或者注释掉

部署到开发环境

pnpm run dev

部署到生产环境

pnpm run build
pnpm start

日志

  • 2025-11-08 本来想使用 mdx 的,不过不支持直接导入相对路径的图片,暂且放弃,主要是因为在 markdown 文件中使用 import 有点麻烦,改天再试试,如果对 lighthouse 提升较大就使用

  • 2025-11-09 切换到 mdx compiler 试试,顺便开启 GFM 插件

  • 2025-11-09 测试了下性能果然提升了,毕竟 mdx 支持使用 Image 等组件嘛,与之相对的 marked 库只能转换 html,没有懒加载,自动设置尺寸等优化

  • 2025-11-14 使用 VNDB-API 根据 User ID 获取投票前几的视觉小说,静态路由默认缓存图片和条目,这意味着仅请求一次,符合 vndb-api 速率限制周期

  • 2025-11-15 目前所有路由都为 SSG 渲染,之后的评论功能预计使用 ISR 渲染

  • 之前为分页功能实现 SSG 思考了一段时间,首先排除使用 searchParams props,这会将路由变为动态的 SSR。其次排除使用 useSearchParams hooks,这会增加客户端包的体积并且数据离服务端近,如果使用 api-token 还会暴露。这样搜索参数就无法使用了,但可以改为路径,也就是使用 params props 获取路径的页码。但是又有个新问题,如果直接使用 /[slug] 获取页码,首页就需要重定向到 /1,这会影响 SEO 和加载速度,到这里思考停止了,直到查看文档无意间看到 [[...slug]] 的 slug 参数如果返回 undefined 会回到上一个路由段,也就是说我完全可以利用这个特性,而不是使用重定向,实现相同的效果,结合 generateStaticParams(),完美实现 SSG 渲染

  • 2025-11-17 目前只做了简单的图片缩放,实际上是类似 pixiv 移动端的图片缩放功能,手动实现起来有点复杂,我又不太想使用别人写好的库,暂时先咕咕咕了

  • 本来以为比较简单,改改 scale 和 translate 属性就好了,但双击缩放的功能,还需要计算一些东西,目前还不能理解下面的公式

  • cx​=px​*S+tx​

  • cy-py*S+ty

  • tx′​=cx​−((cx​−tx​​)/S)×S′

  • ty′​=cy​−((cy​−ty​​)/S)×S′

  • cx​,cy​: 鼠标点击的视口坐标。

  • tx​,ty​: 当前的平移量 (translate.x, translate.y)。

  • S: 当前的缩放比例 (scale)。

  • px​,py​: 缩放中心点在原始图片坐标系中的位置。

  • 2025-11-19 新增了搜索框,首先对于个人 blog 而言,SSG 渲染更适合,对于实现的搜索页面而言,这需要排除 searchParams props,这意味我们可以使用 useSearchParams hooks 来更新搜索参数。虽说我们也可以使用路径参数的 params props 替代,以减少客户端包体积,但是这会导致路径污染,而简单的首页分页使用 params props 来获取页码参数却是最佳实践,但是搜索页面不太合适。另外就是搜索数据如何筛选处理的问题,既然我们根据权衡选择了 hooks,那么就变成了客户端组件,这时我们需要从客户端获取数据,有两个选择,一个是选择从客户端获取服务端预构建的索引数据 json 文件,并筛选显示;另一个选择是构建一个路由 api,传递搜索参数,服务端处理数据筛选后返回给客户端渲染。如何选择呢,首先回归到问题本质,选择 SSG 是为了 FCP (First Contentful Paint) 速度,而剩下的两个选择是否有一个影响呢,第一个选项需要客户端获取 json 索引文件可能会影响,而第二个选项只会在执行搜索时才会执行,那么第二个选择是权衡后比较适合的选择,即 SSG + api route 混合渲染(Hybrid rendering)

  • 2025-11-20 添加了切换主题的按钮,之后再为每个页面添加样式

  • 2025-11-21 添加了评论 UI,另外把 github 的代码迁移到了 codeberg,当然两个远程存储库之间会同步的,不过之后如果有新项目的话就不一定了

  • 2025-11-22 添加了嵌套评论的样式

  • 2025-11-26 为帖子页面的评论功能实现 ISR 渲染时遇到个问题,已提issue,但未得到解决,目前使用 SSG + api route 混合渲染来作为替代方案,这样有个好处,如果将来有好多好多人评论可以实现 Load More,而不用担心一个静态页面在首次加载时包含体积过大的评论文本

  • 对于评论功能的 spam 预防,目前设计了 honeypot field 和是否触发滚动等方式来阻止。如果效果微乎其微的话,再考虑其他方式,比方说本地部署轻量级的 FOSS(Free and Open Source Software) AI 来检查评论文本 w,其实对于个人项目来说还真能这么做。总之是不会给评论功能添加登录注册的,包括第三方登录等等,因为我也不想在回复别人评论时还要进行登录注册操作,这也是我为什么没有使用第三方的 api 来实现评论功能,而是手动实现。不过之后会预留一个 admin 账号或令牌用来删除 spam 评论

  • 2025-11-30 由于我想要访客发评论并不需要登录,并且可以编辑和删除自己的帖子。使用数据库存储会话有点不太合适,因此选择了 stateless 会话。于是便用 jwt 的 payload 直接记录访客发表的帖子 id,并存储在 http-cookie 中,最后在服务端验证签名,设置了 7 天的有效时间,7 天内再次访问会刷新会话时间。之前想使用 next-auth 来进行会话管理的,它默认使用的就是 jwt 并做了点抽象,不过它似乎只能在 authentication 后才能管理会话,因此并不适合这个场景(不得不说,next-auth 的文档看起来有点云里雾里的 くもくも

  • 评论功能完成了,之后打算优化下 UI 并重构部分代码,最后再考虑添加其他的 TODO

License

GNU AGPLv3

Others

The background image for the mobile view is Xid, and the background image for the desktop view is Pid:124691047.

About

Next.js-built Static Moe Blog

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages