Blog

renderMdxを追加してMDX描画呼び出しを単一化した(Blog/Works/MdxContent)

2026.01.25

compileMDXの呼び出しを lib/render-mdx.ts に集約し、Blog/Works/MdxContentのMDX描画を単一ソース化。mdxOptions/components は lib/mdx.ts のまま共通利用。

renderMdxを追加してMDX描画呼び出しを単一化した(Blog/Works/MdxContent)

やったこと(要点)#

  • compileMDX の呼び出しを lib/render-mdx.ts に集約して renderMdx(source) を作成
  • Blog / Works / components/mdx/MdxContent.tsxMDX描画呼び出しを統一
  • lib/mdx.tsmdxOptions / mdxComponents をそのまま再利用(挙動の差分をゼロに)

背景#

MDXの描画は最終的に compileMDX に行き着くため、各ページで個別に呼び出すと、

  • 微妙な options/components の差分が生まれる
  • 変更時に「片方だけ更新漏れ」が起きる

という事故が起きやすい。

そこで 呼び出し自体renderMdx() にまとめて、描画処理の差分を消した。

実装(最終形)#

1) renderMdx を追加(compileMDX呼び出しの単一ソース)#

code block
ts
/* lib/render-mdx.ts */
import "server-only";
 
import { compileMDX } from "next-mdx-remote/rsc";
import type { ReactNode } from "react";
 
import { mdxComponents, mdxOptions } from "@/lib/mdx";
 
/**
 * ✅ MDX描画呼び出しの単一ソース
 * - Blog / Works / 任意のMDX描画で共通利用
 */
export async function renderMdx(source: string): Promise<ReactNode> {
  const { content } = await compileMDX({
    source,
    components: mdxComponents,
    options: {
      mdxOptions,
    },
  });
 
  return content;
}

2) MdxContent も renderMdx 経由に統一#

code block
tsx
/* components/mdx/MdxContent.tsx */
import { renderMdx } from "@/lib/render-mdx";
 
type Props = {
  source: string;
};
 
export default async function MdxContent({ source }: Props) {
  const content = await renderMdx(source);
  return <>{content}</>;
}

3) Blogページ:renderMdxで描画#

code block
tsx
/* app/blog/[slug]/page.tsx */
import { notFound } from "next/navigation";
import type { Metadata } from "next";
 
import { getPostBySlug, getAllPosts } from "@/lib/posts";
import { formatDate } from "@/lib/formatDate";
import { renderMdx } from "@/lib/render-mdx";
 
type Props = {
  params: Promise<{ slug: string }>;
};
 
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((p) => ({ slug: p.slug }));
}
 
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
 
  const post = await getPostBySlug(slug);
  if (!post) return { title: "Not Found | My Site" };
 
  return {
    title: `${post.meta.title} | My Site`,
    description: post.meta.description,
  };
}
 
export default async function BlogPostPage({ params }: Props) {
  const { slug } = await params;
 
  const post = await getPostBySlug(slug);
  if (!post) notFound();
 
  const content = await renderMdx(post.content);
 
  return (
    <main className="container py-14">
      <header className="max-w-3xl">
        <p className="text-xs tracking-[0.22em] uppercase text-muted">Blog</p>
 
        <h1 className="mt-3 text-3xl font-semibold tracking-tight">
          {post.meta.title}
        </h1>
 
        <p className="mt-3 text-xs tracking-[0.16em] text-muted">
          {formatDate(post.meta.date)}
        </p>
 
        <p className="mt-4 text-sm leading-relaxed text-muted">
          {post.meta.description}
        </p>
      </header>
 
      <article className="prose prose-invert mt-10 max-w-none">{content}</article>
    </main>
  );
}

4) Worksページ:renderMdxで描画#

code block
tsx
/* app/works/[slug]/page.tsx */
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
 
import { getAllWorks, getWorkBySlug } from "@/lib/works";
import { renderMdx } from "@/lib/render-mdx";
 
type PageProps = {
  params: Promise<{ slug: string }>;
};
 
export async function generateStaticParams(): Promise<Array<{ slug: string }>> {
  const items = await getAllWorks();
  return items.map((w) => ({ slug: w.slug }));
}
 
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params;
 
  const work = await getWorkBySlug(slug);
  if (!work) {
    return {
      title: "Not Found | Works | My Site",
      description: "指定された作品が見つかりませんでした。",
    };
  }
 
  return {
    title: `${work.meta.title} | Works | My Site`,
    description: work.meta.summary,
  };
}
 
export default async function WorkDetailPage({ params }: PageProps) {
  const { slug } = await params;
 
  const work = await getWorkBySlug(slug);
  if (!work) notFound();
 
  const content = await renderMdx(work.content);
 
  return (
    <main className="container py-14">
      <header className="flex items-end justify-between gap-6">
        <div>
          <p className="text-xs tracking-[0.22em] uppercase text-muted">
            Portfolio
          </p>
 
          <h1 className="mt-3 text-3xl font-semibold tracking-tight">
            {work.meta.title}
          </h1>
 
          <p className="mt-4 max-w-2xl text-sm leading-relaxed text-muted">
            {work.meta.summary}
          </p>
        </div>
 
        <Link
          href="/works"
          className="text-xs tracking-[0.22em] uppercase text-muted hover:text-foreground"
        >
          Back
        </Link>
      </header>
 
      <div className="mt-10 hairline" />
 
      {(work.meta.href || work.meta.repo || work.meta.note) && (
        <div className="mt-6 space-y-2">
          {(work.meta.href || work.meta.repo) && (
            <div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs tracking-[0.16em] text-muted">
              {work.meta.href && (
                <a
                  href={work.meta.href}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="underline underline-offset-4"
                >
                  Open site ↗
                </a>
              )}
              {work.meta.repo && (
                <a
                  href={work.meta.repo}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="underline underline-offset-4"
                >
                  Repository ↗
                </a>
              )}
            </div>
          )}
 
          {work.meta.note && (
            <p className="text-xs leading-relaxed text-muted">{work.meta.note}</p>
          )}
        </div>
      )}
 
      {/* ★ iPhoneがライトでも暗背景なら読めるよう、常に prose-invert */}
      <article className="prose prose-invert mt-10 max-w-none">{content}</article>
    </main>
  );
}

コマンドログ#

code block
bash
git status -sb
git add -A
git commit -m "refactor: renderMdxを追加してMDX描画呼び出しを単一化"
git push

参照コミット#

  • 62ce8f9 — refactor: renderMdxを追加してMDX描画呼び出しを単一化