キッサ カタダ

テクノロジー

Remix製のブログサイトで目次を生成する方法

2024-08-22T18:40:36.470Zに公開
2024-09-08T12:59:04.017Z
#Remix
<h2 id="h8d027c8ed3">はじめに</h2><p>フルスクラッチでブログサイトを構築したものの、実装するハードルが高くて腰が重くなるものといえば…、そう、目次の生成ですね!(当社比)</p><p>私も新しい技術のキャッチアップとしてブログサイト作りがちなんですが、目次まで作ったことがなかったので作り方を調べるところから始まりました。</p><p>最初は自分の勉強も兼ねていたのでライブラリは使わずにフルスクラッチで実装しようともしたんですが、紆余曲折、かくかくしかじかあってライブラリを使うことにしました。</p><p>先人の知恵、発明していただいた車輪、最高です。</p><p>今回の記事では、実際に目次の生成機能を実装してみて色々と知見が貯まったので紹介しつつ、目次を生成する実装方法を解説します!</p><h2 id="h7064a78b13">ライブラリ選定</h2><p>ざーっと調べた感じだと、気になるライブラリは以下の二つでした。</p><p>最終的にTacBotを採用しました。</p><p><strong>TocBot</strong></p><p><a href="https://github.com/tscanlin/tocbot">https://github.com/tscanlin/tocbot</a></p><p>かの有名なZennを開発されたcatnoseさんも、Zennの目次表示に採用されているそうです。</p><p>スター数や最新コミット日時は申し分なく、Zennで運用された実績もあるため最有力候補、というか迷った末にこれに決めました。</p><p>メインブランチの命名がmasterだったのは、個人的なこだわりからするとマイナスポイントだったんですが、今回だけは気にしないことにします。</p><p><strong>Cheerio</strong></p><p><a href="https://github.com/cheeriojs/cheerio">https://github.com/cheeriojs/cheerio</a></p><p>microCMSの公式ドキュメントで紹介されていたため、こちらも信頼できそうです。</p><p>TocBotと同様にスター数や最新コミット、さらにはメインブランチもmainだったのですごく良さそうです。</p><p>ただ、jQueryライクな使い方がどうしても受け入れられず、一旦bun installしたもののTacBotに入れ直すことにしました…。</p><h2 id="he52fdbde3b">目次を生成する</h2><p>実装はドキュメント通りに進めました。</p><p>TacBotのインストールをしたら、<code>app/components/</code>に<code>toc.tsx</code>ファイルを作成して目次コンポーネントを作成していきます。</p><div data-filename="app/components/toc.tsx"><pre><code class="language-typescript">import { useEffect } from &apos;react&apos; import tocbot from &apos;tocbot&apos; export const Toc = () =&gt; { useEffect(() =&gt; { tocbot.init({ tocSelector: &apos;.toc&apos;, contentSelector: &apos;.article&apos;, headingSelector: &apos;h1, h2, h3&apos;, headingsOffset: 280, scrollSmoothOffset: -280, }) return () =&gt; tocbot.destroy() }, [tocbot]) return &lt;nav className=&apos;toc&apos; /&gt; }</code></pre></div><p>ライブラリの使い方は簡単で、レンダリング時に各オプションを私つつtocbotを初期化して目次のDOMを生成します。</p><p>どんなオプションを渡すことができるかは<a href="https://tscanlin.github.io/tocbot/#options" target="_blank" rel="noopener noreferrer nofollow">こちらのドキュメント</a>を参照いただければと思いますが、私が使ったものだけ紹介します。</p><p><strong>tocSelector</strong></p><p>目次をレンダリングする場所を指定します。今回の場合、tocクラスを付与したnavタグの直下にレンダリングされて欲しいので&quot;.toc&quot;を渡します。</p><p><strong>contentSelector</strong></p><p>目次を生成する際に参照するコンテンツが存在するクラスを指定します。記事データを表示しているdivタグにarticleクラスを付与しているため、&quot;.article&quot;を渡します。</p><p><strong>headingSelector</strong></p><p>目次に表示するheadingを渡します。私の記事の場合、h3タグまで作ることが多いので、&quot;h1, h2, h3&quot;を渡しています。</p><p><strong>headingsOffset, scrollSmoothOffset</strong></p><p>固定されたヘッダー等がある場合に指定すると、アンカーリンクを踏んだ時の移動位置や、アンカーリンクが有効判定となる位置を調整できます。</p><p></p><p>目次がレンダリングされることを確認したらスタイリングして完成です!簡単だ〜</p><h2 id="hce137f7854">ハマりポイント</h2><p>当ブログはPCとスマートフォンで目次の表示方法が異なります。</p><figure><img src="https://images.microcms-assets.io/assets/0b9c56e585404b659842183a9dfaf05a/b172d9a90ab8421086ace4c9a6105bb9/pc-toc.webp" alt="PCで目次を表示した場合のスクリーンショット" width="1200" height="630"><figcaption>PC表示</figcaption></figure><figure><img src="https://images.microcms-assets.io/assets/0b9c56e585404b659842183a9dfaf05a/a22ad97f48c64b7e850913ca9cbed794/sp-toc.webp" alt="スマートフォンで目次を表示した場合のスクリーンショット" width="389" height="843"><figcaption>SP表示</figcaption></figure><p>PCで表示されている目次をSPで表示した場合はCSSで非表示にしており、逆にSPで表示した際はPC用の目次は非表示になっています。</p><p>一見うまく動きそうですが、tocbotが目次を生成してくれるのは1回のみで、2回目以降は何も生成してくれませんでした。</p><p>useEffectの依存を見直してレンダーの発火タイミングを変えたり、レンダリングされたDOMを変数に格納して使おうとしたり、tocbot.refresh()を駆使してうまくやろうとしたんですが解決できず…。</p><p>結局、JavaScriptで画面幅を取得し、SPの場合とPCの場合で表示をだし分けたところ、tocbotのレンダリングは常に一回となり正常に目次が表示されるようになりました。</p><p>また、Remixで画面幅を取得する場合、useEffect内でないとwindows関数が使えないため注意が必要です。</p><p>画面がチラついたり可読性が低かったりするので改善したい気持ちでいっぱいですが、現状の当ブログの画面幅取得処理は以下のようになりました。</p><div data-filename="app/components/content-detail.tsx"><pre><code class="language-typescript">export const ContentDetail = ({ content }: Props) =&gt; { const [isSp, setIsSp] = useState(false) useEffect(() =&gt; { const flag = window.innerWidth &lt; 1024 setIsSp(flag) }, []) return ( &lt;div className=&apos;container mx-auto&apos;&gt; {/* 色々記述 */} {isSp ? ( &lt;Popover&gt; &lt;PopoverTrigger asChild className=&apos;fixed bottom-6 right-6&apos;&gt; &lt;Button variant=&apos;outline&apos; className=&apos;z-10&apos;&gt; 目次 &lt;/Button&gt; &lt;/PopoverTrigger&gt; &lt;PopoverContent&gt; &lt;Toc /&gt; &lt;/PopoverContent&gt; &lt;/Popover&gt; ) : ( &lt;div&gt; &lt;Introduce /&gt; &lt;div className=&apos;border rounded p-5 space-y-4 sticky top-6&apos;&gt; &lt;p className=&apos;font-bold&apos;&gt;目次&lt;/p&gt; &lt;Toc /&gt; &lt;/div&gt; &lt;/div&gt; )} &lt;/div&gt; ) }</code></pre></div><p>色々とハマったり力技で押し切ったりしましたが、一旦実装できました!お疲れ様です!</p><h2 id="h9be0c3393d">おわりに</h2><p>実装は丸半日くらいかけちゃいましたが、最終的にはそれなりに満足のいくものができてよかったです。</p><p>よりシームレスに、画面のチラつきなく目次を表示できるようにしたいので、それはまた改善しつつ、そのうちフルスクラッチで目次生成するのにも挑戦してみようかと思います!</p><h2 id="h44e51f96ce">参考資料</h2><p><a href="https://tscanlin.github.io/tocbot/#get-started">https://tscanlin.github.io/tocbot/#get-started</a></p>
RK

カタダ リョウタ

キッサカタダマスター兼フロントエンド寄りのソフトウェアエンジニア。

多趣味に生きてます。

RK

カタダ リョウタ

キッサカタダマスター兼フロントエンド寄りのソフトウェアエンジニア。

多趣味に生きてます。

目次