返回技术文章

把博客做成纯静态:Nuxt 4 生成 + Nginx 托管

这个站点本身的实现笔记:为什么选静态生成而不是 SPA,以及 Nginx 上要注意的三件事。

约 3 分钟读完 前端工程
把博客做成纯静态:Nuxt 4 生成 + Nginx 托管 封面

写博客的工具选型里有一个常见的误解:静态站 = 不需要服务端 = 简单。真正麻烦的地方在于「哪些东西必须发生在构建期」。这篇文章记录这个站点的几个决定。

为什么不是 SPA

同样的 Nuxt 项目,ssr: false 打包出来只有 20 KB HTML 骨架,看起来更省。但博客的两个核心需求会直接落空:

需求SPA(ssr: false)静态生成(nuxt generate)
文章被搜索引擎完整收录取决于爬虫是否执行 JS拿到的是完整正文
分享链接的 OG 卡片只有前端渲染后才有内容HTML 里就带着 meta
首屏下载 JS → 执行 → 渲染直接是可见内容

博客的生命周期以年计,读者从搜索与聊天窗口进来,这两条都在赌客户端会不会执行 JS。不值得。

内容查询必须留在构建期

@nuxt/content 的查询在服务端执行。纯静态部署没有服务端,所以浏览器里重新执行查询就会请求一个不存在的接口。解决办法是把查询包进 useAsyncData:

export function useArticles() {
  return useAsyncData('content:articles', () =>
    queryCollection('articles')
      .where('draft', '=', false)
      .order('date', 'DESC')
      .select('title', 'description', 'date', 'category', 'tags', 'path')
      .all(),
  )
}

两件事容易被忘掉:

  • select() 不写,payload 里就会带上整篇正文的 AST,列表页凭空胖几十 KB;
  • 详情页的 key 必须带 slug,并且用 definePageMeta({ key: route => route.fullPath }) 强制重建组件,否则从 A 文章点到 B 文章,setup 不会重新执行,页面上还是 A 的内容。

Nginx 上要注意的三件事

1. 目录形式的静态文件

nuxt generate 把 /articles/foo 输出成 articles/foo/index.html。try_files 要按这个顺序找:

location / {
  try_files $uri $uri/index.html $uri.html =404;
}

只写 $uri/ 的话,/articles/foo 会先命中目录再落到 index 指令,看起来能用,但大小写敏感的目录列表配置一旦改动就会露馅。

2. 真正的 404 状态码

error_page 404 /404/index.html;

不要写成 error_page 404 = /404/index.html;。加了 = 之后响应状态被改写成 200,搜索引擎会把不存在的链接当成可用页面收录,留下一堆软 404。

3. 哈希产物强缓存,HTML 不缓存

location /_nuxt/ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

/_nuxt/ 下的文件名带内容哈希,可以放心 immutable;HTML 一旦缓存,读者点开链接看到的永远是旧构建。

检索是构建的最后一步

搜索用的是 Pagefind:它在生成之后扫描 .output/public 下的 HTML,产出静态索引文件。于是 package.json 里的脚本顺序是固定的:

{
  "scripts": {
    "dist": "nuxt generate && pagefind --site .output/public"
  }
}

只跑 nuxt generate 也能得到一个能访问的站点,但搜索页会因为拿不到 /pagefind/pagefind.js 而报错。这类「构建步骤少一步、线上坏一个功能」的坑,最好在读者的浏览器里也能看见——所以搜索页在这种情况会显示一句明确的提示,而不是空白。

剩下的都交给 Markdown

写完这些之后,日常只需要两种动作:在仓库里改 .md,或者在网页编辑器里改同一批 .md。两者共用 content.config.ts 里的字段定义,保存下来仍然是同一个文件、同一套字段。静态站点的好处正在于此:内容的表示形式只有一种,工具可以换,数据不用迁。