跳到主要内容

写博客这件事,最大的敌人不是没时间,而是工具太重。装插件、配数据库、升级挂了要修——最后你会发现自己一直在折腾博客,而不是写文章。

我最终选了 VitePress:Markdown 即内容,构建产物是纯静态文件,丢到任意对象存储就能跑。这篇文章记录搭建过程里的关键决策。

为什么选 VitePress ​

对比过几个方案后的判断:

方案优点为什么没选
WordPress生态成熟、后台完善需要 PHP + MySQL,要维护、要打补丁
Hexo / Hugo纯静态、主题多主题质量参差,中文排版要自己调
自建 Nuxt完全可控写代码的时间超过写文章
VitePressVite 秒级热更新、默认主题完成度高、Vue 组件可直接用在 Markdown 里—

决定性的一点:它是 Vite 生态的,热更新和构建速度对写作体验有实质影响,而且想加功能时,直接在 Markdown 里写 Vue 组件就行。

适合谁

如果你主要写技术文章、能接受用 Git 管理内容、不需要评论系统,VitePress 几乎是最省心的选择。

目录约定 ​

先定好目录,后面所有数据都靠它自动收集:

text
docs/
├─ .vitepress/
│  ├─ config.mts          # 站点配置
│  └─ theme/
│     ├─ index.ts         # 主题入口(注册全局组件)
│     ├─ Layout.vue       # 扩展默认布局
│     ├─ posts.data.ts    # 文章数据加载器
│     ├─ components/      # 列表/归档/标签组件
│     └─ styles/blog.css
├─ posts/                 # 所有文章,一个文件一篇
│  └─ hello.md
├─ index.md               # 首页
├─ archive.md             # 归档
├─ tags.md                # 标签
└─ about.md               # 关于

约定只有一条:文章放 posts/,文件名随便起。新增文章不需要改任何配置,构建时自动收录。

自动收集文章 ​

核心是 createContentLoader,它在构建时扫描指定目录,把 frontmatter 抽成数组:

ts
// docs/.vitepress/theme/posts.data.ts
import { createContentLoader } from 'vitepress'

export default createContentLoader('posts/**/*.md', {
  includeSrc: true,            // 需要统计字数
  excerpt: '<!-- more -->',    // 摘要分隔符
  transform(raw) {
    return raw
      .filter(({ frontmatter }) => Boolean(frontmatter.date))
      .map(({ url, frontmatter, src }) => ({
        title: frontmatter.title,
        url,
        date: String(frontmatter.date),
        tags: frontmatter.tags ?? [],
        words: countWords(src ?? '')
      }))
      .sort((a, b) => +new Date(b.date) - +new Date(a.date))
  }
})

在组件里直接 import { data } from '../posts.data' 就能拿到结果,不需要手写任何索引文件。

一个真实的坑

如果项目 package.json 里没有 "type": "module",数据加载器会被当成 CJS 打包,而 vitepress 是纯 ESM,构建会报:

text
"vitepress" resolved to an ESM file. ESM file cannot be loaded by `require`.

两种解法:加 "type": "module",或把文件改为 .data.mts。我选了前者。

文章 frontmatter 约定:

yaml
---
title: 文章标题
date: 2026-07-30
category: 工具
tags: [VitePress, 静态站点]
description: 一句话摘要,会显示在列表卡片上
---

归档与标签 ​

两个页面都只是"把数据换个方式分组",逻辑很短。

归档页:按年份分组 ​

vue
<script setup lang="ts">
import { computed } from 'vue'
import { data } from '../posts.data'

const groups = computed(() => {
  const map = new Map<number, typeof data>()
  for (const post of data) {
    const list = map.get(post.year)
    if (list) list.push(post)
    else map.set(post.year, [post])
  }
  return [...map.entries()].sort((a, b) => b[0] - a[0])
})
</script>

标签页:按标签聚合 ​

同理,只是把 key 从年份换成标签,并顺手做一次计数排序:

ts
[...map.entries()].sort(
  (a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0], 'zh-Hans-CN')
)

让 Markdown 直接用这些组件 ​

在主题入口全局注册,页面里就能直接写标签:

ts
// docs/.vitepress/theme/index.ts
export default {
  extends: DefaultTheme,
  Layout,
  enhanceApp({ app }) {
    app.component('ArchiveList', ArchiveList)
    app.component('TagList', TagList)
  }
}
markdown
<!-- docs/archive.md -->
# 归档

<ArchiveList />

首页自动列出最新文章 ​

不用改首页内容,通过布局插槽注入即可:

vue
<!-- Layout.vue -->
<template>
  <Layout>
    <template #home-features-after>
      <HomePosts />   <!-- 内部取 data.slice(0, 6) -->
    </template>
  </Layout>
</template>

部署 ​

构建产物在 docs/.vitepress/dist/,纯静态:

bash
npm run docs:build
npm run docs:preview   # 本地验证产物

三个要注意的点:

  1. 子路径部署要配 base:部署到 https://xxx.io/ai-docs 时,config.mts 里加 base: '/ai-docs/'
  2. cleanUrls 谨慎开启:开启后 URL 无 .html 后缀,但需要服务端配合重写规则;直接放对象存储建议保持默认 false
  3. sitemap.hostname 要写真实域名,否则生成的站点地图链接都是错的

小结 ​

整套东西加起来不到 300 行代码,换来的是:写文章 = 新建一个 Markdown 文件。

  • 目录约定代替配置:posts/ 下新增文件即发布
  • createContentLoader 代替手工索引:列表、归档、标签全自动
  • 布局插槽 + 全局组件:首页、文章页的定制都不侵入默认主题

工具的意义是让人忘记工具的存在。目前这套我挺满意的。

最后更新于:

本站内容采用 CC BY-NC 4.0 许可