Help us understand the problem. What is going on with this article?

Next.js + MDXで作成した静的サイトのレイアウトをカスタマイズする

Next.js + MDXでブログを構築した際、
レイアウトをカスタマイズするのに試行錯誤したのでその結果を備忘録として残しておく。

サンプルコードはこちら

MDXで記述したページに共通のヘッダやフッタを配置したい

ブログの各記事に、
記事のメタ情報(タイトル、作成日、カテゴリ ...)を記載したヘッダや
関連記事へのリンクを含むフッタなどの共通コンポーネントを配置することを考える。

一番シンプルな方法は各 MDX ファイル内に共通コンポーネントの呼出しを記述する方法。
けれど、すべての MDX ファイルに以下のような記述をするのは DRY 原則に反するのでやりたくない。

import CommonHeader from "@/components/CommonHeader";
import CommonHeader from "@/components/CommonFooter";

<CommonHeader />

ここに本文を書く

<CommonFooter />

そこで、このブログでは以下のような方法で実装した。各記事のパスは /blog/{year}/{month}/{post_id} とする。

  • Next.js の Dynamic Routes を利用し、
    共通コンポーネントを記述したページ pages/blog/[...id].tsx を作成する。

  • babel-plugin-import-glob-array
    用いて MDX ファイルを一括インポートし、各 MDX ファイルのパス情報を配列形式で取得する。

  • 取得したパス情報をもとに SSG 用のパス情報を生成する。また、
    Dynamic Import 機能で
    MDX ファイルをパスごとに取得して、コンテンツとして埋め込む。

実装の一部を以下に抜粋する。詳細はサンプルコードを参照。

FrontMatter を用いたメタ情報の取り扱いについては
公式のサンプルコードもある。

// [...id].tsx
import React from "react";
import { GetStaticPaths, GetStaticProps } from "next";
import dynamic from "next/dynamic";
import mdxUtil from "@/lib/mdx-util";
import BlogLayout from "@/layouts/BlogLayout";

interface Props {
  resourceId: string;
  frontMatter: FrontMatter;
}

const Post: React.FC<Props> = (props: Props) => {
  const { resourceId, frontMatter } = props;
  // MDX ファイルを dynamic import してコンテンツとして埋め込む
  const MDX = dynamic(() => import(`@/posts/${resourceId}.mdx`));
  return (
    <BlogLayout frontMatter={frontMatter}>
      <MDX />
    </BlogLayout>
  );
};

export const getStaticPaths: GetStaticPaths = async () => {
  // babel-plugin-import-glob-array でパス情報を取得、パスリストを生成
  const posts = await mdxUtil.getPosts();
  const paths = posts.map((post) => {
    return {
      params: { id: post.resourceId.split("/") },
    };
  });
  return { paths, fallback: false };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const resourceId = (params.id as string[]).join("/");
  // 各パスに対応する MDX ファイルのメタ情報とパス情報を Props として渡す
  const post = await mdxUtil.getPostByResourcePath(resourceId);
  return {
    props: {
      resourceId,
      frontMatter: post.frontMatter,
    },
  };
};

export default Post;

MDX 本文中の各コンポーネントに独自のスタイルを適用したい

MDXPrivider を用いることで、
MDX 本文中の各要素をカスタムコンポーネントと対応づけることができる。
あとはカスタムコンポーネントに好きなスタイルを適用すればよい。

import React from "react";
import { MDXProvider } from "@mdx-js/react";
import Link from "next/link";
import Paragraph from "@/components/Paragraph";

interface Props {
  frontMatter: FrontMatter;
  children: React.ReactNode;
}

const BlogLayout: React.FC<Props> = (props: Props) => {
  const { children, frontMatter } = props;

  // MDX 中の要素をカスタムコンポーネントに対応づける
  const state = {
    p: Paragraph,
  };

  return (
    <>
      <h1>{frontMatter.title}</h1>
      <span>Created at {frontMatter.date}</span>
      <MDXProvider components={state}>{children}</MDXProvider>
      <Link href="/">
        <a>Go back to home.</a>
      </Link>
    </>
  );
};

export default BlogLayout;

感想

Next.js の Dynamic Routes と MDX を併用する実装はもう少しきれいにできないだろうか。

babel-plugin-import-glob-array を使ったり dynamic import を使ったり、
冗長な感じがして気になってはいるが、今のところ他にうまい方法が思いついていない。

uu64
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away