Nuxt Content 实现 TOC 组件

Aatrox
Aatrox25/05/13
#Nuxt

TOC 全称 table of contents ,指的是一篇文章内的 h1h2h3 等标题的导航,用于快速跳转到对应的标题处。

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 标题,所以在写文章时,要注意不要乱用 # 标题这个语法。 h2h3、 就是文章里常用的二级和三级标题,在数据中就是 depth23

组件本身使用 ulli 来渲染即可,再配合 fixedsticky,使其国定在文章的一侧。

对于 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.vueProseH2.vueProseH3.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 组件的导航根据滚动的区域使其高亮或是显示其他标识

202505131613386

 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))