やったこと(要点)#
compileMDXとMDXRemoteの mdxOptions / components を単一ソース化lib/mdx.tsを新規作成して MDX描画設定の集約ポイントを作成- 型エラー(readonly配列 / SerializeOptions未export)を回避するため、型を
@mdx-js/mdxのCompileOptionsベースで定義
背景#
MDXの描画設定が以下2箇所に分散していると、rehype/remark の差分が出た瞬間に挙動がズレて事故りやすい。
components/mdx/MdxContent.tsx(MDXRemote)app/blog/[slug]/page.tsx(compileMDX)
そこで、設定を lib/mdx.ts に集約して、どこで描画しても同じMDX挙動になるようにした。
つまずきポイント(重要)#
as constを付けると、配列がreadonly推論されてPluggable[]と合わなくなるnext-mdx-remote/rscにはSerializeOptionsが export されていない
→ なので、@mdx-js/mdx の CompileOptions から型を作るのが安全。
実装(最終形)#
1) 集約ファイルを追加#
code block
ts/* lib/mdx.ts */
import type { CompileOptions } from "@mdx-js/mdx";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import { getMDXComponents } from "@/mdx-components";
export type MdxOptions = Omit<
CompileOptions,
"outputFormat" | "providerImportSource"
> & {
useDynamicImport?: boolean;
};
export const mdxOptions: MdxOptions = {
remarkPlugins: [remarkGfm],
rehypePlugins: [
rehypeSlug,
[
rehypeAutolinkHeadings,
{
behavior: "append",
properties: {
className: ["heading-anchor"],
"aria-label": "見出しへのリンク",
},
content: { type: "text", value: "#" },
},
],
],
};
export const mdxComponents = getMDXComponents({});2) MDXRemote 側を集約設定へ差し替え#
code block
tsx/* components/mdx/MdxContent.tsx */
import { MDXRemote } from "next-mdx-remote/rsc";
import { mdxComponents, mdxOptions } from "@/lib/mdx";
type Props = {
source: string;
};
export default function MdxContent({ source }: Props) {
return (
<MDXRemote
source={source}
options={{
mdxOptions,
}}
components={mdxComponents}
/>
);
}3) compileMDX 側を集約設定へ差し替え#
code block
tsx/* app/blog/[slug]/page.tsx */
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { compileMDX } from "next-mdx-remote/rsc";
import { getPostBySlug, getAllPosts } from "@/lib/posts";
import { formatDate } from "@/lib/formatDate";
import { mdxComponents, mdxOptions } from "@/lib/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 compileMDX({
source: post.content,
components: mdxComponents,
options: {
mdxOptions,
},
});
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>
);
}コマンドログ#
code block
bashgit status -sb
git add -A
git commit -m "fix: mdxOptions型定義を@mdx-js/mdxベースに修正"
git push参照コミット#
a0d3fa9— fix: mdxOptions型定義を@mdx-js/mdxベースに修正
