Gatsbyのブログに目次を追加した
2020年06月08日
Gatsby で作っている当ブログに、先日目次を追加したのでご紹介します!
デモ
いま目次が表示されているかと思いますが、PC とスマホで表示が違うのでデモを御覧ください!
PC
こんな感じに右のサイドバーに表示してます!
スマホ
画面右下のボタンをタップすると目次が現れます!
React の公式ドキュメント風!
データ取得
目次に使う各タイトルのデータを取得するには以下のプラグインを使います。
gatsby-transformer-remark または gatsby-plugin-mdx
すでに.md
ファイルや.mdx
ファイルを追加してページを作成しているなら入っているプラグインだと思います。
これらのプラグインが入っていることで、tableOfContents
という node が追加されます。
(これ以降の記事では、gatsby-transformer-remark ではなく gatsby-plugin-mdx を使っていることを想定して説明します。gatsby-transformer-remark を使っている方は適宜読み替えてください。)
ローカルで作業中の方は、http://localhost:8000/__graphqlにアクセスし、GraphiQL で確認してみましょう。
query {allMdx {edges {node {tableOfContents}}}}
{"data": {"allMdx": {"edges": [{"node": {"tableOfContents": {"items": [{"url": "#機能紹介","title": "機能紹介","items": [{"url": "#シンタックスハイライト","title": "シンタックスハイライト"},{"url": "#タイトル","title": "タイトル"},{"url": "#コードのコピー","title": "コードのコピー"},{"url": "#行のハイライト","title": "行のハイライト"},{"url": "#ワードのハイライト","title": "ワードのハイライト"},{"url": "#jsx-のデモ表示","title": "JSX のデモ表示"},{"url": "#jsx-のライブエディター","title": "JSX のライブエディター"}]},{"url": "#コード","title": "コード"}]}}},{"node": {"tableOfContents": {"items": [{"url": "#テーマを探す","title": "テーマを探す"},{"url": "#first-step","title": "First Step"},{"url": "#スターターの役割","title": "スターターの役割"},{"url": "#デプロイする","title": "デプロイする"},{"url": "#author-を追加する","title": "Author を追加する","items": [{"url": "#どこを変更するか?調査する","title": "どこを変更するか?調査する"},{"url": "#自分自身を-author-に追加し、既存-author-を削除する","title": "自分自身を author に追加し、既存 author を削除する"}]},{"url": "#サイトの設定を変える","title": "サイトの設定を変える","items": [{"url": "#ヘッダー左上のサイトロゴ","title": "ヘッダー左上のサイトロゴ"},{"url": "#トップページの説明","title": "トップページの説明"},{"url": "#meta-タグ","title": "meta タグ"},{"url": "#コピーライト","title": "コピーライト"},{"url": "#フッターの-sns-リンク","title": "フッターの SNS リンク"},{"url": "#pwa-の設定","title": "PWA の設定"},{"url": "#設定の完了!!","title": "設定の完了!!"}]},{"url": "#記事を書く","title": "記事を書く"}]}}}]}}}
このように、heading 要素の url と title が node に追加されます。
のちほど、このデータを取得し表示するときに使います。
gatsby-remark-autolink-headers
このプラグインは、タイトルのホバーリンクのために使われます。
heding 要素に id が付与されるので、
タイトルそれぞれにアンカー付きのリンク(~~#anchor
)が存在します。
よって、目次内のリンクをクリックすることで、各タイトルに飛ぶことができるようになりそうです!
目次を表示
目次を表示していきましょう!
当ブログでの表示を説明しますので、参考に自分好みの目次を作成してください!
また、以下のライブラリを使用しています。
(どれもUIに関わるものなので必須ではありません。お好きなものをどうぞ!)
テンプレートファイル
ブログ記事のテンプレートファイルである、src/templates/blog-post/index.js
でTOC
とTOCDrawer
を import して表示しています。
import React from 'react';import { graphql } from 'gatsby';import { Box, Divider, Grid } from '@chakra-ui/core';import Layout from '../../components/layout';import SEO from '../../components/SEO';import Bio from '../../components/Bio';import ContentArticle from '../../components/ContentArticle';import PrevNextArticles from '../../components/PrevNextArticles';import SnsShare from '../../components/SnsShare';import { TOC, TOCDrawer } from '../../components/TOC';const BlogPostTemplate = ({ data, pageContext, location }) => {const { previous: prev, next } = pageContext;const mdx = data.mdx;const url = `${data.site.siteMetadata.siteUrl}${mdx.fields.slug}`;const title = mdx.frontmatter.title;const tocData = data.mdx.tableOfContents.items;return (<Layout location={location} position='relative'><SEOtitle={title}description={mdx.frontmatter.description || mdx.excerpt}cover={mdx.frontmatter.cover?.publicURL}isArticle/><Gridmx='auto'px='6'py='16'maxW='containers.xl'gap='5'gridTemplateColumns={['100%','100%','calc(100% - 200px) 200px','calc(100% - 300px) 300px',]}templateAreas={['"article"','"article"','"article aside"','"article aside"',]}><Box as='article' gridArea='article'><ContentArticle post={mdx} /><SnsShare url={url} title={title} mt='4' /><Divider mt='2' /><Bio mt='6' /><PrevNextArticles prev={prev} next={next} mt='10' /></Box><Box as='aside' gridArea='aside' position='relative'><TOCheadings={data.mdx.tableOfContents.items}position='sticky'top='0'd={['none', 'none', 'block', 'block']}/></Box></Grid><TOCDrawerheadings={data.mdx.tableOfContents.items}d={['inline-block', 'inline-block', 'none', 'none']}/></Layout>);};export default BlogPostTemplate;export const pageQuery = graphql`query BlogPostBySlug($slug: String!) {site {siteMetadata {siteUrl}}mdx(fields: { slug: { eq: $slug } }) {idexcerpt(pruneLength: 160)bodyfields {slug}frontmatter {titledate(formatString: "YYYY年MM月DD日")descriptiontagscover {publicURL}}tableOfContents}}`;
先述のtableOfContents
のitems
を
TOC
コンポーネントとTOCDrawer
コンポーネントに渡しています。
また、
TOC
コンポーネントは PC で表示、スマホでは非表示
d={['none', 'none', 'block', 'block']}
TOCDrawer
コンポーネントは PC で非表示、スマホでは表示
d={['inline-block', 'inline-block', 'none', 'none']}
となっています。
レスポンシブについては、Chakra UI のドキュメントを参照
TOC コンポーネント
このコンポーネントは目次の枠を用意しており、目次の中身は後述のHeadingList
コンポーネントに委譲しています。
import React from 'react';import { Box, Heading } from '@chakra-ui/core';const TOC = ({ headings, ...props }) => (<Boxp='3'boxShadow='0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04)'borderRadius='lg'{...props}><Heading as='h3' size='md' mt='4'>目次</Heading><Box overflowY='auto' maxH='70vh'><HeadingList headings={headings} /></Box></Box>);
HeadingList コンポーネント
このコンポーネントは目次の中身(画像の赤枠内)であり、以下の特徴があります。
- heading のタイトルが表示されている
- 各タイトルをクリックで、同じタイトルにスムーススクロールする
- 各タイトルは階層になっており、たとえば h2 の中に h3 があればネストして表示される
import React from 'react';import { List, ListItem } from '@chakra-ui/core';import { Link } from 'react-scroll';import { css } from '@emotion/core';const HeadingList = ({ headings, itemMy = '4', onDrawerClose, ...props }) => (<List mt='3' fontSize='sm' {...props}>{headings.map(heading => (<React.Fragment key={heading.title}><ListItem my={itemMy}><Linkto={heading.url.replace('#', '')}smooth={true}offset={-10}duration={800}onClick={onDrawerClose && onDrawerClose}css={css`cursor: pointer;&:hover,&.active {text-decoration: underline;opacity: 0.7;}`}>{heading.title}</Link></ListItem>{heading.items && (<HeadingListheadings={heading.items}itemMy='2'onDrawerClose={onDrawerClose}ml='3'mt='0'fontSize='xs'/>)}</React.Fragment>))}</List>);<List mt='3' fontSize='sm' {...props}>{headings.map(heading => (<React.Fragment key={heading.title}><ListItem my={itemMy}><Linkto={heading.url.replace('#', '')}smooth={true}offset={-10}duration={800}onClick={onDrawerClose && onDrawerClose}css={css`cursor: pointer;&:hover,&.active {text-decoration: underline;opacity: 0.7;}`}>{heading.title}</Link></ListItem>{heading.items && (<HeadingListheadings={heading.items}itemMy='2'onDrawerClose={onDrawerClose}ml='3'mt='0'fontSize='xs'/>)}</React.Fragment>))}</List>);
HeadingList
コンポーネント内で再帰的にHeadingList
コンポーネントをレンダーしていることに注目してください。
これは、heading.items
が true 相当のとき。つまり、さらに深い heading 要素があるときに再帰的にレンダーします。
たとえば以下のデータのように h2 の中に h3 があるとき、
{"data": {"allMdx": {"edges": [{"node": {"tableOfContents": {"items": [{"url": "#h2タイトル","title": "h2タイトル","items": [{"url": "#h3タイトルA","title": "h3タイトルA"},{"url": "#h3タイトルB","title": "h3タイトルB"},},]},}}]}}}
以下のようにul li をネストしたHTMLを生成します。
<ul class="css-15tyfp5"><li class="css-1hcp4tm"><a class="css-1tym40f-HeadingList">h2タイトル</a></li><ul class="css-18pzcey"><li class="css-1hvje3t"><a class="css-1tym40f-HeadingList">h3タイトルA</a></li><li class="css-1hvje3t"><a class="css-1tym40f-HeadingList">h3タイトルB</a></li></ul></ul></ul>
TOCDrawerコンポーネント
このコンポーネントは、スマホで右下のボタンをクリックすると目次を表示します。
import React from 'react';import {useDisclosure,Box,Drawer,DrawerBody,DrawerHeader,DrawerOverlay,DrawerContent,DrawerCloseButton,Flex,Icon,} from '@chakra-ui/core';import { FiChevronUp, FiChevronDown } from 'react-icons/fi';const TOCDrawer = ({ headings, ...props }) => {const { isOpen, onClose, onToggle } = useDisclosure();const btnRef = React.useRef();const iconStyles = {size: '6',transition: '0.2s ease 0s, transform 0.2s ease 0s',};return (<><Boxref={btnRef}position='fixed'bottom='12'right='5'color='blue.300'bg='gray.800'borderRadius='50%'shadow='0 0 20px rgba(0, 0, 0, 0.3)'zIndex='1401'onClick={onToggle}{...props}><Flex align='flex-start' overflow='hidden' h='64px'><Flex direction='column' alignSelf='center' px='5'><Iconas={FiChevronUp}transform={`translateY(${isOpen ? 15 : 4}px)`}{...iconStyles}/><Iconas={FiChevronDown}transform={`translateY(-${isOpen ? 15 : 4}px)`}{...iconStyles}/></Flex></Flex></Box><DrawerisOpen={isOpen}placement='right'size='md'onClose={onClose}finalFocusRef={btnRef}><DrawerOverlay /><DrawerContent><DrawerCloseButton /><DrawerHeader>目次</DrawerHeader><DrawerBody pb='24' overflowY='auto'><HeadingList headings={headings} onDrawerClose={onClose} /></DrawerBody></DrawerContent></Drawer></>);};
HeadingList
コンポーネントにDrawerを閉じる関数を渡しています。
これはリンクのonClick
に使われており、HeadingList
はpropsでonDrawerClose
を受け取っているとリンククリック時にDrawerを閉じます。
Chakra UIのDrawerコンポーネントを使用しています。
Drawerの動作の確認は簡素化したデモでご確認ください!
おわり
以上で目次の実装紹介はおわりです!
実際のコードのリンクも貼っておくのでぜひご活用ください!
- https://github.com/YuukiOkamoto/my-blog/blob/master/src/templates/blog-post/index.js
- https://github.com/YuukiOkamoto/my-blog/blob/master/src/components/TOC.js