Nuxt Content v3 踩坑记录:目录、图片与那些反直觉的设计

从 SvelteKit 迁移到 Nuxt 3 的过程中,遇到了不少 Nuxt Content v3 的坑。这篇文章记录了目录导航、图片路径、ProseImg 组件等问题的排查和解决过程。

Nuxt踩坑Nuxt Content
Nuxt Content v3 踩坑记录:目录、图片与那些反直觉的设计

背景

最近在用 Nuxt 3 + Nuxt Content v3 搭建一个个人博客,参考了一个 SvelteKit 项目(svaf)的架构。原以为换个框架只是语法差异,没想到在几个关键功能上踩了不少坑。

这篇文章记录了三个核心问题的排查过程和最终方案。

问题一:目录导航(TOC)只有 h2

现象

Nuxt Content 内置了 TOC 数据,挂在 post.body.toc 上。直接用它渲染目录,发现只有 h2 标题,h3 全部丢失

排查

查了官方文档,发现 build.markdown.toc.depth 可以控制深度:

// nuxt.config.ts
content: {
  build: {
    markdown: {
      toc: { depth: 3 }  // 包含 h3
    }
  }
}

配置改了,清了缓存重启,依然只有 h2。这个配置似乎不起作用。

最终方案

放弃依赖 Nuxt Content 的 toc,直接从 post.body 的 AST 里递归提取所有标题:

function extractHeadings(node: any, list: Heading[] = []): Heading[] {
  if (!node) return list
  if (Array.isArray(node)) {
    // minimark 格式:["h2", {id: "xxx"}, "文本"]
    if (typeof node[0] === 'string' && /^h[1-6]$/.test(node[0])) {
      const level = Number(node[0].charAt(1))
      const text = extractText(node.slice(2)).trim()
      list.push({ id: node[1]?.id || slugify(text), text, level })
    }
    for (const child of node) extractHeadings(child, list)
  }
  return list
}

关键点:Nuxt Content v3 的 body 使用 minimark 格式,标题节点是数组 ["h2", {id: "xxx"}, "标题文本"],不是对象。

问题二:图片路径解析错误

现象

markdown 里用相对路径引用图片:

![SSHFS挂载效果](img/sshfs.avif)

HTML 输出的 <img src="img/sshfs.avif"> 看起来没问题,但浏览器请求的却是 /posts/img/sshfs.avif 而不是 /posts/sshfs/img/sshfs.avif

排查

  1. 检查了 <base> 标签 —— 没有
  2. 检查了重定向 —— 没有
  3. 直接访问 /posts/sshfs/img/sshfs.avif —— 返回 200 OK
  4. 直接访问 /posts/img/sshfs.avif —— 404

服务器路由没问题,问题是浏览器解析相对路径时基于了错误的基准 URL

这可能和 Nuxt 的客户端路由有关:SPA 导航时,浏览器的"当前页面"和 Vue Router 的"当前路由"不同步,导致相对路径解析出错。

最终方案

两步解决:

第一步:自定义 ProseImg 组件

创建 components/content/ProseImg.vue,覆盖 Nuxt Content 默认的图片渲染:

<script setup>
const route = useRoute()
const resolvedSrc = computed(() => {
  if (!props.src) return ''
  if (props.src.startsWith('/') || props.src.startsWith('http')) return props.src
  const base = route.path.replace(/\/$/, '')
  return `${base}/${props.src}`
})
</script>

<template>
  <img :src="resolvedSrc" :alt="alt" />
</template>

第二步:启用 MDC prose 组件

// nuxt.config.ts
mdc: {
  components: {
    prose: true  // 让 Nuxt Content 使用自定义 ProseImg
  }
}

问题三:content 目录的图片如何服务

现象

原站(SvelteKit)的图片放在 content/posts/{slug}/img/ 下,markdown 用相对路径引用,一切正常。Nuxt 项目里,content 目录的文件不会被当成静态资源服务。

方案对比

方案优点缺点
复制到 public/简单文件重复,需同步
Vite 插件拦截原站方案Nuxt 不直接支持
Server Route不复制文件需要手动写路由

最终选择 Server Route,和原站的 Vite 插件思路一致:

// server/routes/posts/[slug]/img/[...file].ts
export default defineEventHandler(async (event) => {
  const url = getRequestURL(event)
  const match = url.pathname.match(/^\/posts\/([^/]+)\/img\/(.+)$/)
  if (!match) return

  const [, slug, filename] = match
  const filePath = resolve('content/posts', slug, 'img', filename)
  const data = await readFile(filePath)
  setResponseHeader(event, 'content-type', MIME[ext])
  return data
})

请求 /posts/sshfs/img/sshfs.avif → 读取 content/posts/sshfs/img/sshfs.avif → 返回图片。

总结

问题根因方案
TOC 只有 h2toc.depth 配置不生效从 body AST 手动提取
图片路径错误SPA 路由导致相对路径解析基准错误ProseImg 组件 + mdc.prose 配置
content 图片 404content 目录不暴露给浏览器Server Route 直接读取返回

Nuxt Content v3 的设计理念是"图片放 public/,用绝对路径"。如果想像 SvelteKit 项目那样把图片和文章放一起,需要额外做不少工作。

不过一旦这些坑都踩过,整体开发体验还是很流畅的。Nuxt 的生态系统、自动导入、模块系统都比 SvelteKit 成熟很多。

评论