2023.07.20最終更新日:
2023.07.20
はじめに
今回は、react-markdown
というライブラリを使って、マークダウン文字列から目次 (TableOfContents)を作成する方法を紹介します。
言語はTypeScriptを使用し、jsライブラリにReactを用いて実装していきます。
対象とする読者
- マークダウンで記事本文を作成するブログサイト等をReact(or Next.js)で開発している人。
- Reactを使って目次をサクッと作りたい人。
- Reactを使ったシンプルな目次の実装方法が知りたい人。
今回説明しないこと
- ライブラリの詳しい使用方法(記事閲覧後、すぐに実装し始められることを本記事の一つの目的としているため。)
実装
それでは早速実装していきましょう。
以下の使用で実装していきます。
- 記事本文のh1, h2, h3タグのみ抽出して、目次を生成。
- それぞれの見出しに内部リンクでスクロールするようにする。
1. ライブラリのインストール
今回はreact-markdown
というライブラリを使って実装していきますので、インストールしていきましょう。
Githubのスター数も多く、メンテナンスも定期的に行われているようなので問題なく使えるライブラリになります。
GitHub - remarkjs/react-markdown: Markdown component for ReactMarkdown component for React. Contribute to remarkjs/react-markdown development by creating an accougithub.com
※お使いのパッケージマネージャーに合わせて、コマンドを変えて行ってください。
今回は、yarn
を使用します。
bashyarn add react-markdown
2. 目次(TableOfContents)コンポーネントを作成する
次に目次コンポーネントを作成していきます。
現状の実装内容としては、Propsでマークダウンの文字列を受け取って、それらを全てDOMに変換しているだけです。
tsx// TableOfContents/index.tsx import React from "react"; import ReactMarkdown from "react-markdown"; type Props = { markdownContent: string; }; const TableOfContents: React.FC<Props> = ({ markdownContent }) => { return ( <nav> <span>目次</span> <ReactMarkdown>{markdownContent}</ReactMarkdown> </nav> ) }; export default TableOfContents;
使用する側は以下のようになります。
tsx// App.tsx import "./App.css"; import TableOfContents from "./TableOfContents"; import ReactMarkdown from "react-markdown"; const MARKDOWN_CONTENT = ` # Heading1 本文が入ります。本文が入ります。本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。 ## Heading2 本文が入ります。本文が入ります。本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。 本文が入ります。本文が入ります。本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。 ### Heading 3 本文が入ります。本文が入ります。本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。 本文が入ります。本文が入ります。本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。本文が入ります。本文が入ります。 本文が入ります。本文が入ります。 `; function App() { return ( <div style={{ display: "flex" }}> <div> {/* 作成したTableOfContentsコンポーネント */} <TableOfContents markdownContent={MARKDOWN_CONTENT} /> </div> <div> <span>記事本文</span> <ReactMarkdown>{MARKDOWN_CONTENT}</ReactMarkdown> </div> </div> ); } export default App;
プレビュー
3. 目次コンポーネントを完成させる
続いて、先程作成した目次コンポーネントを完成させていきましょう。
完成形
tsx// TableOfContents/index.tsx import React from "react"; import ReactMarkdown from "react-markdown"; import { LinkItem } from "./LinkItem"; type Props = { markdownContent: string; }; const TableOfContents: React.FC<Props> = ({ markdownContent }) => { return ( <nav> <span>目次</span> <ReactMarkdown allowedElements={["h1", "h2", "h3"]} components={{ h1: LinkItem, h2: LinkItem, h3: LinkItem }} > {markdownContent} </ReactMarkdown> </nav> ); }; export default TableOfContents;
構造としては、
allowedElements
:目次に表示したい要素を限定する。components
:取得したh1, h2, h3の要素をそれぞれリンクとして表示する。
<LinkItem />
コンポーネントの実装内容は以下の通りです。
(以下ではa
タグを使用していますが、Next.js等で実装を行う場合は適宜<Link />
コンポーネント等を使用してください。)
tsx// TableOfContents/LinkItem/index.tsx import React from "react"; type Props = { level: number; children: React.ReactNode & React.ReactNode[]; }; const getHeadingLevel = (level: number) => { switch (level) { case 1: { return 0; } case 2: { return 12; } case 3: { return 24; } } }; export const LinkItem: React.FC<Props> = ({ level, children }) => { // HeadingのLevelに合わせて、左側のスペーシングを変えています。 const leftSpacing = getHeadingLevel(level); return ( <div style={{ paddingLeft: leftSpacing }}> <a href={`#${children.toString()}`}>{children}</a> </div> ); };
これで目次自体の作成は完了しました。
最後に、記事本文に#id
を付与して、目次のリンクをクリックした際にスクロールできるようにしましょう。
4. 記事本文を完成させる
記事本文のHeadタグにidを付与していきます。
idとしては、各Headタグの文字列を付与します。
まずは、idを付与した<Heading />
コンポーネントを実装していきます。
tsx// Heading/index.tsx import React from "react"; type Props = { level: number; children: React.ReactNode & React.ReactNode[]; }; // 見出しにIDを付与するためのMarkdownコンポーネント export const Heading: React.FC<Props> = ({ level, children }) => { const id = children.toString(); switch (level) { case 1: { return <h1 id={id}>{children}</h1>; } case 2: { return <h2 id={id}>{children}</h2>; } case 3: { return <h3 id={id}>{children}</h3>; } } return <h1 id={id}>{children}</h1>; };
では、実際に使っていきましょう。
(※記事本文のマークダウン文字列(MARKDOWN_CONTENT
)が肥大化してきたので、別ファイルに切り出しています。)
tsx// App.tsx import "./App.css"; import TableOfContents from "./TableOfContents"; import ReactMarkdown from "react-markdown"; import { MARKDOWN_CONTENT } from "./markdownContents"; import { Heading } from "./Heading"; function App() { return ( <div style={{ display: "flex", gap: 20 }}> <div style={{ width: 180 }}> {/* 作成したTableOfContentsコンポーネント */} <TableOfContents markdownContent={MARKDOWN_CONTENT} /> </div> <div> <span>記事本文</span> {/* IDを持ったHeadingコンポーネントを割り当てる */} <ReactMarkdown components={{ h1: Heading, h2: Heading, h3: Heading }}> {MARKDOWN_CONTENT} </ReactMarkdown> </div> </div> ); } export default App;
これで、記事本文と目次の完成です!!
UIを確認してみましょう。
さいごに
ここまで読んでいただきありがとうございます。
完成したソースコードを載せておきますので、ご利用ください。
GitHub - koutaro0205/react-table-of-contents: ブログ記事用の参考ソースコードブログ記事用の参考ソースコード. Contribute to koutaro0205/react-table-of-contents development by creating an accoungithub.com
See you !!