Nuxt Contentに目次を追加する

投稿日
Nuxt Contentに目次を追加する

Nuxt Contentで作っているサイトに目次を追加します。




今回以下のような見た目の目次を作ります。

目次の見た目

検証した環境

1 @nuxt/content 1.8.1

見出しのデータを取得する

Nuxt Contentでは記事一覧、記事詳細で使用する article の中には article.toc として既に見出しに使えるような値が用意されています

article.tocの中身
{
  "toc": [
    {
      "id": "h2-タイトル1",
      "depth": 2,
      "text": "h2-タイトル1"
    },
    {
      "id": "h3-タイトル1",
      "depth": 3,
      "text": "h3-タイトル1"
    },
    {
      "id": "h3-タイトル2",
      "depth": 3,
      "text": "h3-タイトル2"
    },
    {
      "id": "h2-タイトル2",
      "depth": 2,
      "text": "h2-タイトル2"
    },
    ・・・
  ]
}

ここまででイメージが付いた方も多いかなと思いますが、これをv-forで回せばOKです!

注意点

公式に書いてありますが、 h2とh3のみ しか article.toc の中には入っていません。 そのため、h4以下のタイトルを表示する場合は自前で何かしら実装する必要があります。

tocにはh2とh3のタイトルのみが使われます。

コンテンツを作成する - Nuxt Contentより引用


ただし目次でh6まで使えるようにするPRがNuxt Contennt公式GitHubに上がっていて既にマージ済みです。


このPRには >= v1.11.0 という記述があるため、 Nuxt Content v1.11.0以上ではh2,h3以外の見出しも目次で使えるようになりそうです!

見出し用のコンポーネントを用意

以下のものを使用しているので環境に合わせて読みかえて下さい

  • Module CSS
  • scss
  • reset.css
  • htmlに font-size: 10px; を指定した上で rem を使用
Toc.vue
<template>
  <div :class="$style.wrapper">
    <p :class="$style.title">目次</p>
    <div :class="$style.tocWrapper">
      <ul :class="$style.list">
        <li v-for="item in toc" :class="[$style.item, $style[`depth-${item.depth}`]]">
          <nuxt-link :to="`#${item.id}`" :class="$style.link">{{ item.text }}</nuxt-link>
        </li>
      </ul>
    </div>
  </div>
</template>
 
<script>
export default {
  props: {
    toc: Array,
  },
}
</script>
 
<style lang="scss" module>
.wrapper {
  width: 100%;
  background-color: #ffffff;
  border: 1px solid #e0e0e0;
  border-radius: 3px;
  padding: 3rem;
}
.title {
  font-size: 2rem;
  padding-bottom: 2.2rem;
  border-bottom: 1px solid #b1afaf;
  color: #212121;
  font-weight: 600;
}
.tocWrapper {
  margin-top: 2.2rem;
}
.list {
  list-style: none;
}
.item {
  font-size: 1.5rem;
  padding: 0.6rem 0;
 
  &.depth-3 {
    padding-left: 1.5em;
  }
}
.link {
  text-decoration: none;
  color: #0272fd;
}
</style>


6行目の :class= の部分が難読ですね

<li v-for="item in toc" :class="[$style.item, $style[`depth-${item.depth}`]]">



Module CSSの設定を [ファイル名]__[クラス名]__[ハッシュ値] としていた場合、以下のようなクラスがこの li には付与されます

  • [ファイル名]__item__[ハッシュ値]
  • [ファイル名]__depth-3__[ハッシュ値](h3の場合)
liに設定されるクラス名



そして .item.depth-3 のあるクラス(h3)のみ padding-left を使ってインデントする、という事をしています。

.item {
  font-size: 1.5rem;
  padding: 0.6rem 0;
 
  &.depth-3 {
    padding-left: 1.5em;
  }
}
h3のみインデントが適用された

見出しコンポーネントを使う

あとは見出しのコンポーネントを記事詳細ページで読み込み利用します。


ここでは v-if="article.toc.length > 0" を使って見出しがない場合は表示しないようにしてみました。



責務の分離うんぬんを考えると Toc.vue に持たせず使う側(記事詳細ページ)で判断した方がいいかなと思って記事詳細の方に実装しましたが、 これくらいなら Toc.vue に書いちゃってもいいかもですね。笑

pages/articles/_slug.vue
<template>
  <div>
    <div :class="$style.tocWrapper" v-if="article.toc.length > 0">
      <Toc :toc="article.toc" />
    </div>
    <nuxt-content :document="article" />
  </div>
</template>
 
<script lang="ts">
import { Context } from '@nuxt/types'
 
import Toc from '~/components/organisms/Toc.vue'
 
export default {
  components: { Toc },
  async asyncData({ $content, params }: Context) {
    const article = await $content('articles', { deep: true }).where({ slug: params.slug }).fetch()
    if (!article) {
      return
    }
    return { article }
  },
}
</script>
 
<style lang="scss" module>
.tocWrapper {
  margin-top: 2.2rem;
}
</style>



これで完成!! 何がすごいかって同じページ内の遷移の場合、Nuxt Contentの場合自動スクロールしてくれるんです!😍

自動スクロールしてくれる

Nuxt Contentさすがです

プロフィール画像
Yuki Takara
都内でフリーランスのエンジニアをやってます。フロントとアプリ開発メインに幅広くやってます。