Tech blog

記事の目次を作ろう

記事の目次を作ろうと思います。

ただ、むずいのは、microCMSが返してくれる記事情報が素のHTMLだということです。表示するぶんにはとっても便利なんですが、記事の中身を解析して別の情報を作るには向いていないなーと思います。

ともかく、このHTMLから記事の見出し要素を抽出し、目次として構成できればうまくいきそうですが、どうやって抽出するか考えなければいけませんね。

ブラウザー上でHTMLを分析する

まあ、適当なHTMLエレメントのinnerHTMLにHTMLテキストをぶちこめば、適切に解析してHTMLエレメントにしてくれます。さらにクエリセレクターを使えばどこにどんなHTMLがあるかは取得できます。(たぶん、適切なやり方ではないのだと思いますが・・・)

なので、解析は、以下の方針とします。

  • divエレメントにHTMLをぶっこむ
    • divはdocument.createElementで生成して、documentにはappendしない(appendすると描画されちゃうので)
  • h1 - h3までをクエリセレクターで探す
  • h1, h2, h3に親子関係を持たせる(h1の下にあるh2, h3はh1の子とする。h2の下にあるh3はh2の子とする)

解析する処理を書いた

こんな感じです。もうちょっとうまく書ける気がするんですが、こういうのを綺麗に書くのがまだまだ苦手です。

解説はコメントで書いておきます。

実装

export type TOC = {
  tagName: string;
  text: string;
  id: string;
  children: TOC[];
}

function createTableOfContents(html: string) {
  const div = document.createElement("div"); // どこにも描画されないdivを用意する
  div.innerHTML = html; // ブラウザにhtmlをパースさせる
  const headings = [...div.childNodes] // h1, h2, h3のエレメントを探す
    .filter(it => headingTags.includes(it.nodeName.toLowerCase()))
    .map(it => it as HTMLElement)
    .map(it => ({ tagName: it.nodeName.toLowerCase(), text: it.textContent!, children: [], id: it.id }))
  return headings.reduce((p, it) => {
    // h1のとき
    if (it.tagName === "h1" || p.length === 0) {
      p.push(it);
      return p;
    }
    // h2のとき
    // 最後尾のh1のchildrenとして追加する(これで入れ子状態が作れる)
    const children = p[p.length - 1].children;
    if (it.tagName === "h2" || children.length === 0) {
      children.push(it);
      return p;
    }
    // h3のときもh2とおなじ
    const grandChildren = children[children.length - 1].children;
    if (it.tagName === "h3") {
      grandChildren.push(it)
      return p;
    }
    return p;
  }, [] as TOC[]);
}

入れ子になったものを表示する

これは、とくに問題なくて、ulとliを入れ子にするだけですね。上で作った入れ子構造があるので、比較的簡単に実装できます。

スタイルをいい感じにする

どういうデザインにするか

zennを見ると、左にコンテンツ、右に目次が表示されます。これを真似ることにしました。

flex-itemの幅が言うことを聞かなくてハマる

左にコンテンツ、右に目次を表示するということは、flexで横並びにするのが簡単なアイデアですね。

とおもって、display: flexをあてて、アイテムそれぞれに対し、flex: 10みたいな感じで、領域における割合を10:3で設定したんですが、なぜかコンテンツ側が画面幅を飛び出してしまう問題が発生しました。いろいろ調べた結果、こちらの記事(Flex アイテムがコンテナからはみ出してしまう時の対処方法)が参考になりました。

要は、アイテムにmin-width: 0をつけるだけなんですが、なぜか解決するという。

文章が長いコンテンツの場合、コンテンツの幅(物理)が広いともとらえられるので、勝手に広がってしまうみたいです。その辺は上記記事を読んでいただければと思います。

スマホ対応も忘れずに

スマホで見た場合は、記事の上部分に表示することにしました。このブログはno class frameworkを使っているので、なるべくスタイルを当てたくないのですが、ある程度はやむなしと考えています。(セマンティックなHTMLから離れていきそうで嫌なんですが・・・)

拙作のCSS in JSライブラリならメディアクエリも利用できるので、バッチリです。

スマホ対応の場合には若干HTMLの構造を変えなければいけないのですが(表示の順番が変わるので)、flex-directionでreverseを使ってみました。HTMLを見たときと表示されたものが逆になるので、けっこう違和感はありますが、うまく実装できました。

目次をクリックしたら飛ばす

heading要素にidをつけておくと、#hogeというリンクに飛んだ時にその要素まで飛ばすことができる機能があります。microCMSは記事のheading要素にランダムなidを付与してくれているので、上で解析して階層化する際にどんなidが付与されているかも記録しておきます。

あとは、目次を表示する際にリンクをつけてあげればよいです。簡単。

まとめ

microCMSで入稿した記事に目次をつける流れを書きました。

次は、記事にカテゴリでもつけてみようかと思います。

以上です。よろしくお願いします。