記事に目次をつけたかったのでプラグインgatsby-remark-autolink-headersを利用して目次を実装しました。
ulタグからolタグに変え、目次が長くなるので閉じるボタンをつけ、アコーディオンさせるなど少し改造しました。そのやり方について綴ります。
※ 2021年12月v4バージョンアップに伴いリライトしました。
かみーゆ/フロントエンドエンジニア
今までのGatsbyの記事と注意点
現在ここまで記載しています。
制作するまでを目標にUPしていくので順を追ったらGatsbyサイトが作れると思います。
- インストールからNetlifyデプロイまで
- ヘッダーとフッターを追加する
- 投稿テンプレにカテゴリやらメインビジュアル(アイキャッチ)追加
- ブログ記事、カテゴリ、タグ一覧の出力
- プラグインを利用して目次出力(←イマココ)
- プラグインナシで一覧にページネーション実装
- 個別ページテンプレート作成
- プラグインHelmetでSEO調整
- CSSコンポーネントでオリジナルページを作ろう!!
- 関連記事一覧出力
- タグクラウドコンポーネントを作成する
- パンくずリストを追加する
- 記事内で独自タグ(コンポーネント)を使えるようにする
Gatsbyのv4からv5へアップグレードしたのでそのやり方をメモしておきます。Netlify の Nodeバージョンの...
このシリーズはGithub・gatsby-blogに各内容ブランチごとで分けて格納しています。
今回のソースはtable-of-contentブランチにあります。
このシリーズではテーマGatsby Starter Blogを改造
この記事は一番メジャーなテンプレート、「Gatsby Starter Blog」を改造しています。同じテーマでないと動かない可能性があります。
目次出力のためのプラグインgatsby-remark-autolink-headersを利用
Gatsbyは豊富なプラグインが魅力です。
gatsby-remark-autolink-headers はプラグインの1つです。
以下のようなことができます。
- 見出しタグにidを振る
- 見出しタグを抽出しリンク付きのリストタグを出力
gatsby-remark-autolink-headers
gatsby-remark-autolink-headersをインストール
npm
インストールします。
npm install gatsby-remark-autolink-headers
gatsby-config.jsにプラグインの追記
gatsby-remark-autolink-headers は gatsby-transformer-remark のサブプラグインです。
なので、gatsby-config.jsの gatsby-transformer-remarkのoption に記載します。
テーマGatsby Starter Blogを利用していればgatsby-transformer-remarkはインストールされているはずです。
module.exports = {
plugins: [
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [`gatsby-remark-autolink-headers`], },
},
],
}
このプラグイン1コ問題があって、公式サイトによるとプラグインgatsby-remark-prismjsよりも前に読み込む必要があります。
// good
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
`gatsby-remark-autolink-headers`,
`gatsby-remark-prismjs`,
],
},
}
// bad
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
`gatsby-remark-prismjs`, // should be placed after `gatsby-remark-autolink-headers`
`gatsby-remark-autolink-headers`,
],
},
}
gatsby-remark-autolink-headersを実装
実装します。
オプションの説明については記事の後ろに記載します。
module.exports = {
plugins: [
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [ { resolve: `gatsby-remark-autolink-headers`, options: { icon: false, maintainCase: false, }, }, ], },
}
]
}
今回はシンプルにアイコンなし。以下のように設定しました。
オプションを設定しなければ、以下のように無駄なコードも出力されます。
optionのmaintainCaseをtrueにするとアンカーリングが効かなくなることも
maintainCaseをtrueにすると見出しに追加されるidに含まれるアルファベットは大文字が小文字に変換されます。目次のアンカーと差異ができて飛ばなくなることがあります。なのでここではfalseにしておきましょう。
目次を出力するコンポーネントを作成する
次に目次を出力するコンポーネントを作成します。
table-of-content.jsを追加します。
src/
├ templates/
| └ blog-post.js
└ components/
└ table-of-content.js(新規作成)
コードはこんな感じです。
import React from "react";
const Topic = props => {
return (
<div>
<h2>目次</h2>
<div
dangerouslySetInnerHTML={{
__html: props.data,
}}
>
</div>
</div>
);
};
export default Topic;
リスト化されたデータはdata.markdownRemark.tableOfContents
に格納されます。
blog-post.jsのGraghQLのスキーマmarkdownRemark()
内にtableOfContents
を追記します。
ポイントはmaxDepth
を指定することによって表示する見出しの深さを調整できます。
tableOfContents(maxDepth: 3)
目次のネスト(入れ子)が深いのはあまり好きじゃないんだよね
ということで、見出し3(maxDepth: 3)まで取得することにしました。
あとは記事詳細テンプレの読み込みたい場所にコンポーネントを出力するだけです。
import TOC from "../components/table-of-content"
//~コード省略~
const BlogPostTemplate = ({ data, location }) => {
{/*~コード省略*/}
<TOC data={data.markdownRemark.tableOfContents} /> {/*~コード省略*/}
}
export default BlogPostTemplate
export const pageQuery = graphql`
query BlogPostBySlug(
$id: String!
$previousPostId: String
$nextPostId: String
$hero: String
) {
site {
siteMetadata {
title
}
}
allFile(
filter: {
relativePath: { eq: $hero }
sourceInstanceName: { eq: "images" }
}
) {
edges {
node {
relativePath
childImageSharp {
gatsbyImageData(
width: 640
formats: [AUTO, WEBP, AVIF]
placeholder: BLURRED
)
}
}
}
}
markdownRemark(id: { eq: $id }) {
id
excerpt(pruneLength: 160)
html
tableOfContents(maxDepth: 3) frontmatter {
title
date(formatString: "YYYY-MM-DD")
description
cate
tags
}
}
# 省略
}
`
出力されるタグをulからolに変え、開閉ボタンをつける
リスト出力がul(アンオーダーリスト・順不同リスト)なのは個人的にはちょっと気に入らないです。
なのでここから少し改変します。
JavaScript replace
で ul>
から ol>
に置換します。
(閉じタグもあるのでこのような形にしました)
後ほどアコーディオン機能を実 装します。チェックボックスを追加して h2
を label
に書き換えておきます。
import React from "react";
const Topic = props => {
const list = props.data.replace(/(ul>)/gi, 'ol>');
return (
<div className="p-box--gray u-mblg">
<input type="checkbox" class="mokuji" id="mokuji" /> <label className="c-content__heading" for="mokuji">目次</label> <div className="c-editArea mokujiList">
<div
dangerouslySetInnerHTML={{
__html: list,
}}
>
</div>
</div>
</div>
);
};
export default Topic;
スタイリングする
カウンター関数を利用してスタイリング、アコーディオンは手間なのでCSSのみで実装しました。
今回はコードしか紹介しませんので詳しく原理を知りたい方はこちらをご覧ください。
import React from "react";
import styled from "styled-components" //追加
const TableOfContent = props => {
const list = props.data.replace(/(ul>)/gi, "ol>")
return (
<TOC> <input type="checkbox" class="mokuji" id="mokuji" />
<label className="heading" for="mokuji">
目次
</label>
<div
dangerouslySetInnerHTML={{
__html: list,
}}
></div>
</TOC> )
}
export default TableOfContent
// 省略
style用のコンポーネントのコードです。
const TOC = styled.div`
border: 1px solid #aaa;
padding: 0;
margin: 20px 0;
input {
display: none;
&:checked ~ div {
max-height: 0;
}
&:checked ~ .heading::before {
transform: rotate(90deg);
}
}
div {
transition: .3s;
max-height: 200vh;
overflow: hidden;
p {
margin: 0;
}
ol {
counter-reset: cnt;
list-style: none;
}
& > ol {
margin: 0;
padding: 10px 20px;
border-top: 1px solid #aaa;
li {
counter-increment: cnt;
position: relative;
padding-left: 2em;
&::before {
left: 0;
font-size: 1.4rem;
font-weight: 700;
position: absolute;
content: counters(cnt, " - ")'.' ;
}
ol {
padding-left: 0;
li {
padding-left: 3em;
}
}
}
}
}
.heading {
background: #eee;
font-size: 1.4rem;
font-weight: 700;
display: flex;
align-items: center;
padding: 0 20px;
height: 40px;
font-size: 18px;
margin: 0;
position: relative;
&::after,
&::before {
position: absolute;
content: '';
height: 2px;
width: 20px;
background: #999;
right: 20px;
top: 19px;
transition: .3s;
}
}
`
コンポーネント化してスタイルを書きたい場合は、styled-componentsのインストールが必要です。
npm i styled-components
最初から閉じておきたい場合は、input
にchecked
を付与しておきましょう。
<input type="checkbox" class="mokuji" id="mokuji" checked>
オプションの一覧
オプションの一覧です。icon以外はあまり使うことないかもしれません。
オプション | 用途 |
---|---|
offsetY | リンクをクリックして移動した時の見出しの上の空き(オフセット)の調整。pxです |
icon | Boolean。デフォルトはtrueでホバーすると左にアイコンが表示されまます。 |
class | アンカーに独自のクラス名を指定するそう |
maintainCase | Boolean。含まれる要は大文字小文字を維持するか指定できる。 |
removeAccents | Boolean。アクセント削除。日本人のサイトにはまず必要なさそう。 |
isIconAfterHeader | Boolean。アイコンの位置を右側に移動 |
elements | リンクを自動挿入するためのタグ一覧を配列で指定 |
まとめ
目次があると記事の全貌がわかり、読むか読まないかの判断ができ、ユーザーに優しいです。
次の記事は「プラグインナシで一覧にページネーション実装」です。
実際私のサイトでもヒートマップで確認すると、目次ってかなりクリックされているんですよ。
この記事が皆さんのコーディングライフの一助となれば幸いです。
最後までお読みいただきありがとうございました。