16. ブログ記事(中央カラム)の冒頭にindexをつける

Cursor

・ パソコンではすべて表示し、スマホでは折りたたんで表示する

・ パソコンでは右カラムに表示し、スクロールに追従する方式をとっているサイトもあるが、個人的には気が散るので嫌い。あと、ブログ記事に近い方がSEO的には良い

まず、TOCのComponentsをつくる

Next.js&Typescriptではコンポーネントはキャメルケースでファイル名を書き、それ以外はケバブケースでファイル名を書くのが一般的(Nextjs公式もそうなっている)

components/table-of-contents.tsx

import Link from "next/link";

type Heading = {
  id: string;
  text: string;
};

type TableOfContentsProps = {
  headings: Heading[];
};

export function TableOfContents({
  headings,
}: TableOfContentsProps) {
  if (headings.length === 0) {
    return null;
  }

  return (
    <nav className="rounded-xl border border-stone-200 bg-stone-50 p-5 dark:border-stone-700 dark:bg-stone-900">
      <h2 className="mb-4 text-lg font-semibold">
        Table of Contents
      </h2>

      <ul className="space-y-2">
        {headings.map((heading) => (
          <li key={heading.id}>
            <Link
              href={`#${heading.id}`}
              className="text-sm leading-6 text-stone-600 hover:text-stone-900 dark:text-stone-400 dark:hover:text-stone-100"
            >
              {heading.text}
            </Link>
          </li>
        ))}
      </ul>
    </nav>
  );
}

lib/posts.ts

MDXファイルを読み込み、MDXを解釈するファイル

ここで##というMarkdownに反応してH2を抽出する処理を行う

まずはPostの下あたりに次のコードを追加

export type Heading = {
  id: string;
  text: string;
};

そして、getPostsByCategory)0の下あたりに次のコードを追加

export function slugify(text: string): string {
  return text
    .toLowerCase()
    .replace(/[^\w\s-]/g, "")
    .replace(/\s+/g, "-");
}

export function getHeadings(content: string): Heading[] {
  const headings: Heading[] = [];
  const regex = /^## (.+)$/gm;

  let match;

  while ((match = regex.exec(content)) !== null) {
    const text = match[1].trim();

    const id = text
      .toLowerCase()
      .replace(/[^\w\s-]/g, "")
      .replace(/\s+/g, "-");

    headings.push({
      id,
      text,
    });
  }

  return headings;
}

これでたとえばMDXに

## What is Japanese Pension System?

本文。。。

## Who needs to join?

本文。。。

とあった場合、

What is Japanese Pension System?
Who needs to join?

H2だけが抽出される

app/posts/[slug]/page.tsx

変更前
import { formatDate, getAllPosts, getPostBySlug } from "@/lib/posts";


変更後
import {
  formatDate,
  getAllPosts,
  getHeadings,
  getPostBySlug,
} from "@/lib/posts";
import { TableOfContents } from "@/components/table-of-contents";
変更前
const post = getPostBySlug(slug);

変更後
const headings = getHeadings(post.content);
PostContentの下に追加
<TableOfContents headings={headings} />

残りをどうやったか忘れたけど、なんかできた。

触ったのは上記3つのファイルだけ。

見出しが日本語だとエラーが出るので、英語にしたらエラーがなおった(どうやら、libs/postのreplace(正規表現)の部分が英語しか考慮していない書き方だったらしく、日本語をすべて消去してしまうかららしい)

BACK