在第一次接触网页开发大约 1 年之后,我开始想着想要拥有一个属于自己的博客。
于是我就大概花了 6 个月几乎只埋头做这件事。

前端部分的功能,参考金度亨先生的博客就已经足够了。
我用 mdx 搭建起一个博客,其实连一周都没花到。
事实上,仅靠前端也并不会给博客运营带来什么大问题。
有一阵子我也觉得,只要写 mdx,然后做前端开发就已经够用了,但还是放不下“无论在哪都能连上网写文章的全栈博客”这个梦想。
1. 功能
功能其实没什么特别的。
可以发文章,也可以查看文章。

登录之后,右上角的按钮形态会发生变化。
点击之后就可以上传文章。
从下面的视频可以看到写文章和上传的过程。
有一个图片转换的流程,所有上传的图片都会以效率最高的 avif 格式进行管理。
YouTube 只要粘贴链接,就可以像上面那样插入。
表格管理功能的想法是从 Tistory 得到的。

当焦点落在表格上时,上方会弹出一个可以新增或删除表格的工具栏。
当然也支持临时保存。
在编辑器中按 cmd+s 或者 ctrl+s 就会保存。
2. 架构
“架构”这个词听起来很酷,用韩语说就是“结构”。
最先进入服务器的请求会由 Caddy 作为反向代理进行分发。
如果只有前端的话,就会直接把 mdx 转成页面后发送出去,但既然已经做了后端,就用 Next.js 和 Prisma 把两边连了起来。

大家都知道,如果把来自用户的数据不加处理直接塞进后端,很快就会被黑掉。
一定要通过 API 验证之后再写入。

有一次我希望博客的评论能多一点,于是就把 API 对所有人开放,谁都可以发表评论。
结果有家伙不断地通过那个 API 发起 SQL 注入攻击,评论居然堆到了大概 100 条。
也有人在 svg 图片内部插入 fetch 和命令,试图进行黑客攻击。
说不定此刻在中国某个地方,有一个我自己都不知道的“另一个我”正好端端地走来走去也不一定。
3. 编辑器

我花最多心思做的就是编辑器。
编辑器是在 Tiptap 的基础上叠加了各种功能制作而成的。
因此光是和 Tiptap 相关的组件就超过了 10 个。

以插入图片为例,是像下面这样实现的。
const editor = useEditor({
extensions: editorExtensions,
content,
onUpdate: ({ editor }) => {
onChange(editor.getHTML());
},
editorProps,
immediatelyRender: false,
});
const { uploadProgress, uploadImage, clearProgress } = useImageUpload({
onUploadComplete: (imageUrl, isAnimated) => {
if (editor) {
try {
editor
.chain()
.focus()
.insertContent([
{
type: "image",
attrs: {
src: imageUrl,
alt: "업로드된 이미지",
"data-animated": isAnimated || false,
},
},
{
type: "paragraph",
content: [],
},
])
.run();
} catch (error) {
console.error("이미지 삽입 오류:", error);
}
}
},
});为了在性能方面占优势,我直接保存并加载 HTML。
从 Tiptap 往后端保存时,我在中间加了不少处理流程。
对代码块、Google 地图、标题、图片、链接预览等做了各种预处理,然后把 content 和 processedContent 分开保存。
4. 后端 Schema
最近在做多语言服务的时候,我意识到,一旦改动已经做好的后端 Schema,就几乎是灾难。
突然又想起了那位总强调 Schema 重要性的教授说的话。
如果你也打算做到博客后端这一层,希望你能花很长时间思考要提供什么样的服务。
东加一点西加一点,结果 Schema 变得太臃肿了,虽然有点羞愧,但还是公开出来。
model Post {
id String @id @default(cuid())
title String
slug Int @unique @default(autoincrement())
excerpt String
content String
processedContent String?
thumbnail String?
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime?
likes Int @default(0)
views Int @default(0)
comments Comment[]
likesRows PostLike[]
viewDedups PostViewDedup[]
notifications Notification[]
locales PostLocale[]
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
category String
@@map("posts")
}5. 后记
现在该来回答下面这个问题了。
做博客的时候,有必要做到后端吗?
全部做完之后,我脑子里冒出的念头是:何必呢?
到头来我才意识到,WordPress 才是那条非常轻松的道路。
一旦开始实现后端和 API,要考虑的事情就会急剧增多。
最近在 react2shell 事件期间,攻击几乎是以分钟为单位涌过来,终端里不断出现从来没见过的命令,有好几天都很煎熬。
即便是现在,我也还是每隔几天就用 npm audit 检查并修复安全问题。
偶尔改 DB Schema,迁移一旦出错就会冷汗直冒。
不过回头看,通过实现后端我确实学到了很多东西,而且拥有了一个可以亲手实现功能的博客,这一点还是挺不错的。
接下来我不打算再给博客添功能了,更多是想通过做其他项目来让自己的作品集更加充实。








댓글을 불러오는 중...