跳到主要内容
Loxander
返回项目列表
维护中2026.09 - 至今设计与开发

个人技术博客平台

用 Next.js App Router + Markdown 搭建的技术博客与作品集,全站静态预渲染,零数据库依赖,支持暗色模式、代码高亮与自动目录。

  • Next.js
  • TypeScript
  • Tailwind CSS
  • Shiki
  • Markdown

这个站点本身就是第一个项目。目标很明确:做一个三年后还改得动的个人博客。

背景

原来的笔记散落在各种文档工具里,缺乏统一的组织方式,也没法沉淀成作品集。我需要一个能长期维护的载体:写文章的成本要低,改样式的成本也要低。

目标

  • 写文章只需要新建一个 Markdown 文件,不碰任何代码
  • 全站静态生成,不引入数据库和 CMS
  • 支持暗色模式、代码高亮、文章目录、标签筛选
  • SEO 完整:metadata、sitemap、robots、结构化数据
  • 移动端可用,键盘可操作

架构

content/*.md  ──►  lib/posts.ts(读取 + 校验)
                        │
                        ├──►  lib/markdown.ts(渲染 + 高亮 + 目录)
                        │
                        └──►  app/blog/[slug]/page.tsx(构建时预渲染)

内容读取与渲染完全分离:列表页只需要 frontmatter,不触发 Markdown 渲染;详情页才做完整渲染。

技术栈与选择理由

选择 理由
Next.js App Router 页面即文件,静态生成开箱可用,无需额外 SSG 配置
Markdown(非 MDX) 内容不需要运行时编译,避免维护组件映射表
Shiki 双主题 高亮结果与编辑器一致,暗色切换纯 CSS 完成
Tailwind CSS 样式与结构写在一起,改版时不用在多个文件间跳转
无数据库 内容即仓库文件,可回溯、可评审

核心实现

构建期预渲染

内容读取刻意使用同步 fs.readFileSync,属于「可预测值」——同一份输入永远得到同一份输出,因此 Next.js 会在构建时完成渲染,不需要任何显式缓存指令。

export function generateStaticParams() {
  // 构建时枚举全部文章,生成静态页面
  return getAllPosts().map((post) => ({ slug: post.slug }));
}

// 未列出的 slug 直接返回 404,不进入运行时渲染
export const dynamicParams = false;

目录与正文的一致性

目录锚点必须和正文标题 id 完全一致。实现时对所有层级的标题都调用 slug 生成器,只把 h2/h3 收进目录,避免同名标题导致后续锚点整体偏移。

问题与解决

问题 原因 解决方案
暗色切换时高亮闪烁 重新执行高亮有延迟 Shiki 双主题 + CSS 变量,切换零成本
移动端表格溢出 表格宽度超过视口 表格改为可横向滚动容器
首屏主题闪白 服务端不知道用户偏好 layout 内联脚本,渲染前读取并应用主题
目录跳转错位 标题 id 生成序列不一致 所有标题统一调用 slug 生成器

结果

  • 全站 20 余个页面全部静态预渲染,首屏无阻塞请求
  • 客户端 JavaScript 仅用于导航高亮、主题切换与标签筛选三处
  • Lighthouse 各项指标均在 95 分以上(桌面端)

后续计划

  • 加入 RSS 订阅
  • 文章支持系列(series)聚合
  • 补充站内搜索(基于构建期生成的索引,仍然不需要服务端)

相关文章

从零搭建一个可维护的个人技术博客不引入数据库、CMS 和用户系统,只用 Next.js + Markdown + Git 搭一个能长期维护的技术博客:目录结构、内容管线、代码高亮、目录生成与部署流程。