Nuxt Content 实现 TOC 组件
TOC
全称 table of contents
,指的是一篇文章内的 h1
、h2
、h3
等标题的导航,用于快速跳转到对应的标题处。
Nuxt Content
会为 markdown
内容生成 toc
数据。
通过 queryCollection
获取的 page
数据中,通过 page?.body?.toc?.links
拿到当前文章的 toc
数据
以《Nuxt 3.17 发布,对比3.16有一个重大改变》这篇文章为例,数据是这样的
[
{
"id": "更新日志",
"depth": 2,
"text": "更新日志",
"children": [
{
"id": "数据获取改进",
"depth": 3,
"text": "数据获取改进"
},
{
"id": "新增内置组件",
"depth": 3,
"text": "新增内置组件"
},
{
"id": "路由改进",
"depth": 3,
"text": "路由改进"
},
{
"id": "加载指示器自定义",
"depth": 3,
"text": "加载指示器自定义"
},
{
"id": "文档作为包",
"depth": 3,
"text": "文档作为包"
},
{
"id": "开发体验改进",
"depth": 3,
"text": "开发体验改进"
},
{
"id": "模块开发增强",
"depth": 3,
"text": "模块开发增强"
},
{
"id": "性能改进",
"depth": 3,
"text": "性能改进"
},
{
"id": "其他改进",
"depth": 3,
"text": "其他改进"
}
]
},
{
"id": "主要影响点",
"depth": 2,
"text": "主要影响点",
"children": [
{
"id": "useasyncdatausefetch",
"depth": 3,
"text": "useAsyncData、useFetch"
},
{
"id": "_2025年05月06日093334-更新",
"depth": 3,
"text": "2025年05月06日09:33:34 更新"
}
]
}
]
一般一篇文章里,h1
表示的是文章标题,在一个页面中通常只会存在一个 h1
标题,所以在写文章时,要注意不要乱用 # 标题
这个语法。 h2
、h3
、 就是文章里常用的二级和三级标题,在数据中就是 depth
为 2
或 3
组件本身使用 ul
、li
来渲染即可,再配合 fixed
或 sticky
,使其国定在文章的一侧。
对于 Nuxt
来说,TOC
组件可以完全是一个客户端组件,因为不需要被爬虫抓取,也不是初次渲染需要的重要信息。而且如果要做一些简单的交互,也需要等前端环境加载出来之后才能做到。
所以只需要使用一个 computed
拿到 toc
数据,然后把数据传递给组件即可。
我观察了一圈,感觉少数派的 TOC 组件是比较美观的,于是我仿照他们的思路封装了自己的 TOC 组件
toc 的配置位于 nuxt.config.ts
:
content: {
build: {
markdown: {
toc: {
depth: 2,
searchDepth: 2
}
}
}
}
默认深度是 2
,我一般会用到 3
。
同时,如果要想自己定义 h1、h2、h3 标题的样式,需要在 app/components/content
目录下新建 ProseH1.vue
、ProseH2.vue
、ProseH3.vue
组件。
写样式时,不管如何封装,记得不要丢掉 id 属性
ProseH3.vue 为例
<template>
<div :id="props.id" class="heading my-4 cursor-pointer scroll-mt-14">
<span class="px-2 py-1 text-xl font-bold bg-zinc-800 text-white dark:bg-zinc-200 ">
<a v-if="props.id && generate" :href="`#${props.id}`" class="!text-zinc-200 dark:!text-zinc-800">
<slot />
</a>
<slot v-else />
</span>
</div>
</template>
<script setup lang="ts">
import { computed, useRuntimeConfig } from '#imports'
const props = defineProps<{ id?: string }>()
const { headings } = useRuntimeConfig().public.mdc
const generate = computed(() => props.id && ((typeof headings?.anchorLinks === 'boolean' && headings?.anchorLinks === true) || (typeof headings?.anchorLinks === 'object' && headings?.anchorLinks?.h1)))
</script>
对应 TOC 组件中:
# template v-for child in link.children
<li>
<span>#</span>
<NuxtLink :href="`#${child.id}`"> {{ child.text }} </NuxtLink>
</li>
也可以像我的组件一样,配合 IntersectionObserver
,做到 TOC 组件的导航根据滚动的区域使其高亮或是显示其他标识
const headings = document.querySelectorAll('.heading')
observer.value = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
activeId.value = entry.target.id
}
})
})
headings.forEach(heading => observer.value.observe(heading))