やったこと(要点)#
compileMDXの呼び出しをlib/render-mdx.tsに集約して renderMdx(source) を作成- Blog / Works /
components/mdx/MdxContent.tsxの MDX描画呼び出しを統一 lib/mdx.tsの mdxOptions / 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
bashgit status -sb
git add -A
git commit -m "refactor: renderMdxを追加してMDX描画呼び出しを単一化"
git push参照コミット#
62ce8f9— refactor: renderMdxを追加してMDX描画呼び出しを単一化
