この記事をおすすめしたい人
- ヘッドレスCMSから取得した記事に動的に目次をつけたい人
- Nuxt.js×microCMSでコンテンツ管理しようとしている人
- つまりオレ
何も知らない超初心者が脱WordPressしたくてNuxt.jsでサイト構築していくシリーズです。
今回はNuxt.js×microCMS環境で取得した記事から動的に目次を生成する方法を考えていきます。
(テストはmicroCMSで行っていますが、WordPressのREST APIや他のヘッドレスCMSでも同様だと思います)
WordPressの上でな!
※ このブログはまだWordPress製です
事前の準備
APIから記事を取得
前提として、この↓ように_id.vueや_slug.vueで記事データを取得できているものとします。
export default {
async asyncData({ $config, params }) {
const { data } = await axios.get($config.apiRoot + '/blog/' + params.id, {
headers: { 'X-API-KEY': $config.apiKey }
})
return data
}
}
v-runtime-templateを使えるように
最終的にv-runtime-templateというモジュールで出力するので、前回の記事を参考に、
- モジュールのインストール
- nuxt.config.jsへの設定追加
- vueファイル内でモジュールをインポート
- <template>内にコンポーネントを追加
を済ませておいてください。
cheerioモジュールをインストール
cheerioというモジュールをインストールします。
$ npm install cheerio
Node.js上でjQueryのようにHTMLを操作するためのモジュールです。
目次用のコンポーネントを作成
目次を出力するためのコンポーネントを用意します。 最小限だとこんな↓感じ。
<template>
<ul>
<li v-for="item in items">
<a :href="'#' + item.id">{{ item.title }}</a>
</li>
</ul>
</template>
<script>
export default {
name: 'blog-toc',
props: [ 'items' ],
}
</script>
propsでitemsという目次用の配列を受け取ります。 それぞれの要素にはidとtitleを持たせる予定なので、idをリンク先として、titleをテキストとして使用します。
nameは必須のようです。 「BlogToc」のような形ではなく「blog-toc」のようにハイフンで繋いでいる方がいいっぽい。
記事内に目次コンポーネントを追加
microCMSにアップする記事内の、目次を表示したい位置に上記コンポーネントを追加します。
<blog-toc :items="list"></blog-toc>
閉じタグがないとエラーが出たので、</blog-toc>は必須です(たぶん)。 <blog-toc />ではダメでした。
変数listはこの段階では存在しないので変な感じがしますが問題ありません。
あと、最終的にv-runtime-templateというモジュールで出力するので、コンポーネントの追加は明示しておく必要があります。
<script>
import VRuntimeTemplate from 'v-runtime-template'
import toc from '@/components/toc'
export default {
components: {
VRuntimeTemplate,
toc,
},
}
</script>
「toc」は目次コンポーネントの名前です。 適宜読み替えてください。
_id.vue内でのメインの処理
_id.vueや_slug.vueなど、記事を出力するページ内の処理です。 以下、asyncData内の処理です。
cheerioで記事本文をロード
cheerioモジュールを呼び出し、APIから受け取った記事本文をロードします。
const cheerio = require('cheerio')
const $ = cheerio.load(
current.content,
{ decodeEntities: false }
)
「current」はAPIから受け取ったデータで、「content」に記事本文が入っている想定なので、名前が違う場合は適宜読み替えてください。
「decodeEntities」はcheerioのオプションで、可読性を下げないためにオフにしています。
前回の記事で書きましたが、v-runtime-templateで出力するには、記事部分をひとつのタグで囲っておかなければなりません。 記事側でやってもいいですが、囲い忘れが起きると面倒なのでここで処理しておきます。
const cheerio = require('cheerio')
const $ = cheerio.load(
'<article id="article">\n' +
'<h1>' + current.title + '</h1>\n' +
current.content +
'</article>\n',
{ decodeEntities: false }
)
今回は<article>で囲っています。 後でcheerioから出力しやすいようidを指定しました。 ついでにh1も追加しています。
h2タグの抽出
上のcheerio(変数$)を利用して、h2タグを抽出します。
$('h2').each( (index, element) => {
})
cheerioはjQueryと同じセレクタが使えるそうなので、h2以外も目次にしたい場合はセレクタを調べてください。
ここでやることは2つです。
h2タグにidを追加(リンク用)することと、目次コンポーネントに渡す用の配列(idとtitleを持たせる)を作ることです。
まずはid追加のコード↓。
$('h2').each( (index, element) => {
const id = 'i' + index
$(element).attr('id', id)
})
WordPressで使っていた目次プラグインに習って「#i0」「#i1」のようなidにしています。
次に配列取得のコード↓。
let list = []
$('h2').each( (index, element) => {
const id = 'i' + index
const title = $(element).html()
$(element).attr('id', id)
list.push({ id: id, title: title })
})
titleにはh2タグ内の内側のテキストが入ります。
titleと先ほどのidを合わせて、listという配列に追加しています。
cheerioをHTMLに戻す
cheerioはhtml()でHTMLを出力することができます。
$.html()
ですがこれでやると<html>や<head>が追加された状態で出力されてしまうため、部分的にHTMLとして出力します。
const html = cheerio.html($('#article'), { decodeEntities: false })
先ほど外側を囲った<article>にidを追加したのはここで使うためです。
これで<article>の部分だけ出力されます。
asyncDataの結果を返す
処理した結果をページ内で使えるように計算結果を返します。
return {
html: html,
list: list,
}
「html」は<v-runtime-template>に渡す名前と合わせてください。
「list」は上で作った目次用コンポーネントに渡す名前と合わせてください。
出力部分
ここはシンプルです。
<template>
<v-runtime-template :template="html" />
</template>
これで目次コンポーネントが展開された状態でページが表示されます。
まとめ
目次作成部分だけまとめるとこんな↓コードになります。
const cheerio = require('cheerio')
const $ = cheerio.load(
'<article id="article">\n' +
'<h1>' + current.title + '</h1>\n' +
current.content +
'</article>\n',
{ decodeEntities: false }
)
let list = []
$('h2').each( (index, element) => {
const id = 'i' + index
const title = $(element).html()
$(element).attr('id', id)
list.push({ id: id, title: title })
})
const html = cheerio.html($('#article'), { decodeEntities: false })
return {
html: html,
list: list,
}
コピペでそのままペタッとはいきませんが、自分用に調整してみてください。
以上、WordPressからお届けしました!