文章索引

用 Nuxt 4 和 Content 把博客钉在静态文件上

· 2 分钟

用 Nuxt 4 和 Content 把博客钉在静态文件上

这个站点没有后端服务,没有数据库,也没有管理后台。写作流程只有三步:新建一个 .md 文件,写完,提交。剩下的事情交给构建管道。

为什么坚持纯静态

动态博客系统的大部分能力我用不上。评论可以外挂,搜索可以在客户端做,统计交给日志。剩下的核心诉求只有一个:把文字变成网页。既然如此,服务器就是多余的运维负担。

静态文件带来的好处非常实在:

  • 部署目标随便挑,对象存储、CDN、GitHub Pages 都行
  • 没有可以被攻破的运行时,安全面趋近于零
  • 首字节时间就是 CDN 的响应时间
  • 整站可以塞进 Git,回滚等于 git revert

内容即文件

@nuxt/contentcontent/ 目录变成一个类型安全的集合。集合的形状在 content.config.ts 里声明:

ts
import { defineCollection, defineContentConfig, z } from '@nuxt/content'

export default defineContentConfig({
  collections: {
    posts: defineCollection({
      type: 'page',
      source: 'posts/*.md',
      schema: z.object({
        date: z.string(),
        tags: z.array(z.string()).default([]),
        draft: z.boolean().default(false),
      }),
    }),
  },
})

frontmatter 写错字段,构建期就会报错,而不是等到页面渲染成 undefined 才发现。

页面里怎么查

查询 API 长得像一个极简的 ORM,在组件里直接用:

vue
<script setup lang="ts">
const { data: posts } = await useAsyncData('posts', () =>
  queryCollection('posts')
    .where('draft', '=', false)
    .order('date', 'DESC')
    .all(),
)
</script>

同一套 API 在 server route 里也能用,只是第一个参数换成 event。RSS 和站点地图就是这么生成的。

一条命令产出整站

bash
pnpm generate

nuxt generate 会启动一次性渲染,从 / 开始爬取站内链接,把每个路由渲染成 HTML,连同 RSS、站点地图一起写进 .output/public。那个目录就是最终产物,扔到任何静态托管上都能跑。

值得强调的是:预渲染的页面依然是完整的 Vue 应用。首屏是纯 HTML,水合之后站内跳转走客户端路由,无需再请求服务器。

写作体验

因为文章就是仓库里的文件,所有代码工具都顺便变成了写作工具:全文搜索用编辑器自带的,版本历史是 Git 的提交记录,写一半的文章加上 draft: true 就不会被发布。

这套组合不惊艳,但非常耐用。工具退到背景里,剩下的只有写字这件事本身。