chore: 初始化项目仓库
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: Nuxt Content v3 踩坑记录:目录、图片与那些反直觉的设计
|
||||
published: 2026-05-31T14:00:00
|
||||
description: 从 SvelteKit 迁移到 Nuxt 3 的过程中,遇到了不少 Nuxt Content v3 的坑。这篇文章记录了目录导航、图片路径、ProseImg 组件等问题的排查和解决过程。
|
||||
image: /posts/nuxt-content-lessons/img/cover.jpg
|
||||
tags: ['Nuxt', '踩坑', 'Nuxt Content']
|
||||
draft: false
|
||||
---
|
||||
|
||||
# 背景
|
||||
|
||||
最近在用 Nuxt 3 + Nuxt Content v3 搭建一个个人博客,参考了一个 SvelteKit 项目(svaf)的架构。原以为换个框架只是语法差异,没想到在几个关键功能上踩了不少坑。
|
||||
|
||||
这篇文章记录了三个核心问题的排查过程和最终方案。
|
||||
|
||||
# 问题一:目录导航(TOC)只有 h2
|
||||
|
||||
## 现象
|
||||
|
||||
Nuxt Content 内置了 TOC 数据,挂在 `post.body.toc` 上。直接用它渲染目录,发现**只有 h2 标题,h3 全部丢失**。
|
||||
|
||||
## 排查
|
||||
|
||||
查了官方文档,发现 `build.markdown.toc.depth` 可以控制深度:
|
||||
|
||||
```typescript
|
||||
// nuxt.config.ts
|
||||
content: {
|
||||
build: {
|
||||
markdown: {
|
||||
toc: { depth: 3 } // 包含 h3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
配置改了,清了缓存重启,**依然只有 h2**。这个配置似乎不起作用。
|
||||
|
||||
## 最终方案
|
||||
|
||||
放弃依赖 Nuxt Content 的 toc,直接从 `post.body` 的 AST 里递归提取所有标题:
|
||||
|
||||
```typescript
|
||||
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 里用相对路径引用图片:
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
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 默认的图片渲染:
|
||||
|
||||
```vue
|
||||
<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 组件**
|
||||
|
||||
```typescript
|
||||
// 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 插件思路一致:
|
||||
|
||||
```typescript
|
||||
// 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 只有 h2 | `toc.depth` 配置不生效 | 从 body AST 手动提取 |
|
||||
| 图片路径错误 | SPA 路由导致相对路径解析基准错误 | ProseImg 组件 + mdc.prose 配置 |
|
||||
| content 图片 404 | content 目录不暴露给浏览器 | Server Route 直接读取返回 |
|
||||
|
||||
Nuxt Content v3 的设计理念是"图片放 public/,用绝对路径"。如果想像 SvelteKit 项目那样把图片和文章放一起,需要额外做不少工作。
|
||||
|
||||
不过一旦这些坑都踩过,整体开发体验还是很流畅的。Nuxt 的生态系统、自动导入、模块系统都比 SvelteKit 成熟很多。
|
||||
Reference in New Issue
Block a user