【Dawn】ブログ記事内で目次(TOC)を自動生成する方法

はじめに

Shopifyでサイトを制作・運用するうえで、ブログはSEO対策やコンテンツマーケティングに重要な機能です。
ただ記事を書くのではなく、ユーザーにとって読みやすく、検索エンジンにも評価されやすい構成にすることが成果に繋がります。

その鍵となるのが「TOC(Table of Contents:目次)」です。
適切なTOCの導入により、読みやすさの向上はもちろん、ページ内リンクの最適化や検索結果での表示強化も期待できます。

そこで今回は、JavaScriptを実装し、目次機能が標準で備わっていないShopifyでも簡単に&自動で目次を生成する方法をご紹介します。
ブログ活用を本格化したい方はぜひご参考ください!

実装方法

1. 目次生成用のJavaScriptファイルを作成

コード編集画面から新規アセット「table-of-contents.js」を作成し、下記コードを貼り付けます。
今回はブログ記事内のh2タグとh3タグを自動で取得する仕組みにしています。

document.addEventListener('DOMContentLoaded', () => {
  const articleBodyItemProp = 'articleBody';
  const tableOfContentsItemProp = 'tableOfContents';

  const getArticleBody = () => {
    return document.querySelector('.article-template__content');
  };

  const getHeadings = () => {
    const articleBody = getArticleBody();
    if (!articleBody) return null;

    return articleBody.querySelectorAll(['h2', 'h3']);
  };

  const existsTableOfContents = () => {
    return !!document.querySelector(`[itemprop="${tableOfContentsItemProp}"]`);
  };

  const generateSlug = (text) => {
    return text
      .toLowerCase()
      .trim()
      .replace(/[^\wぁ-んァ-ン一-龥ー]+/g, '-') // 日本語と英数字以外の文字をハイフンに
      .replace(/-+/g, '-')                      // 連続ハイフンを1個に
      .replace(/^-|-$/g, '');                   // 先頭・末尾のハイフンを削除
  };  

  const generateAnchor = (id, text) => {
    const anchor = document.createElement('a');
    anchor.style.display = 'block';
    anchor.style.width = '100%';
    anchor.style.padding = 0;
    anchor.style.margin = 0;
    anchor.style.lineHeight = '21px';
    anchor.style.fontSize = '14px';
    anchor.style.color = '#6f7372';
    const content = document.createTextNode(text);
    anchor.appendChild(content);
    anchor.setAttribute('href', `#${id}`);
    return anchor;
  };

  const generateListItem = (id, text, isNest = false) => {
    const listItem = document.createElement('li');
    listItem.style.padding = '8px 0';
    listItem.style.margin = 0;
    listItem.style.listStyleType = 'none';
    if (isNest) {
      listItem.style.paddingLeft = '24px';
    }
    const anchor = generateAnchor(id, text);
    listItem.appendChild(anchor);
    return listItem;
  };

  const generateList = (headings) => {
    if (!headings) return;

    const list = document.createElement('ul');
    list.style.padding = 0;
    list.style.margin = '16px 0 0';
    list.style.listStyle = 'none';
    headings.forEach((heading) => {
      const slug = generateSlug(heading.textContent);
      const text = heading.textContent.trim();
      const listItem = generateListItem(slug, text, heading.tagName === 'H3');
      list.appendChild(listItem);
    });
    return list;
  };

  const generateTitle = () => {
    const title = document.createElement('h2');
    title.style.fontSize = '14px';
    title.style.fontWeight = '700';
    title.style.margin = 0;
    title.style.color = 'rgb(111, 115, 114)';
    title.style.lineHeight = 1.5;
    title.style.letterSpacing = '0.04em';
    const content = document.createTextNode('目次');
    title.appendChild(content);
    return title;
  };

  const generateTableOfContents = (headings) => {
    if (!headings) return;

    const tableOfContents = document.createElement('nav');
    tableOfContents.setAttribute('itemprop', tableOfContentsItemProp);
    tableOfContents.style.padding = '16px';
    tableOfContents.style.margin = '36px 0';
    tableOfContents.style.backgroundColor = '#f7f9f9';
    const title = generateTitle();
    tableOfContents.appendChild(title);
    const list = generateList(headings);
    tableOfContents.appendChild(list);
    return tableOfContents;
  };

  const addIdToHeading = () => {
    const articleBody = getArticleBody();
    const allHeadings = articleBody?.querySelectorAll('h2, h3');
    if (!articleBody || !allHeadings) return;
  
    allHeadings.forEach((heading) => {
      const slug = generateSlug(heading.textContent);
      heading.id = slug;
    });
  };

  const addTableOfContents = () => {
    const articleBody = getArticleBody();
    const headings = getHeadings();
    if (!articleBody || !headings) return;

    const tableOfContents = generateTableOfContents(headings);
    const firstHeading = headings[0];
    if (!tableOfContents || !firstHeading) return;
    if (existsTableOfContents()) return;

    articleBody.insertBefore(tableOfContents, firstHeading);
  };

  // 実行
  addIdToHeading();
  addTableOfContents();
});

2. 「main-article.liquid」で上記ファイルを読み込む

「main-article.liquid」のブログコンテンツを表示させている箇所を下記のように変更し、作成したJavaScriptファイルを読み込みます。

{%- when 'content' -%}
  <div
    class="article-template__content page-width page-width--narrow rte{% if settings.animations_reveal_on_scroll %} scroll-trigger animate--slide-in{% endif %}"
    {{ block.shopify_attributes }}
  >
    {{ article.content }}
    <script src="{{ 'table-of-contents.js' | asset_url }}" defer="defer"></script>
  </div>

これにより以下のようにブログコンテンツの上部に目次が表示され、見出しをクリックすると対応する箇所までスクロールされるようになります。

本ブログでは、記事冒頭にある導入部分の後に目次が表示されるようにしており、さらにクリックした際スムーズにページがスクロールされるようカスタマイズしています。
目次のスタイルもJavaScript内で調整が可能ですので、サイトデザインに合わせてアレンジしてみてくださいね。

まとめ

いかがでしたか?

TOCの導入は、サイトのUX向上やマーケティング施策に必要不可欠です。
特に長文コンテンツにおいては、ユーザーが必要な情報へと効率的にアクセスできるナビゲーションが求められます。
TOCはその導線となり、ユーザーにとって使いやすい構成になっていることが、離脱率軽減やサイトの評価を上げることにも役立ちます。

取得するhタグの変更やスクロールの挙動のカスタマイズなど、さらなる応用も可能です。
ご不明な点やご相談がある方はお気軽にお問い合わせください!

売上最大化に向けて戦略立案から構築・運用までワンストップでサポート

EC運用でお悩みの方はお任せください!

お問い合わせはこちら
一覧に戻る