koudenpaのブログ

趣味のブログです。株式会社はてなでWebアプリケーションエンジニアをやっています。職業柄IT関連の記事が多いと思います。

Contentlayer で Next.js の SSG 向けデータを管理する体験はなかなかよい

タイトルで全てが完結している記事です。

↓これ。

github.com

Next.jsのようなフレームワークで静的なサイトのコンテンツを生成する際には、元となるデータ(よくあるのはお知らせやブログ記事)を何かしらの形で管理しなくてはならない。

どうするのがいいのか? 適当に検索してヒットしたのがContentlayerだったので使ってみて「いい感じだった」ラッキーというわけです。

「データの管理: 所定のディレクトリにMarkdownファイルを置けばそれが個別のページにレンダリングされる」のような感じ。

まだベータ版なのだけれど十分に使って行けるかと思う*1

この辺の体験が特によかった。

  • 導入が簡単
  • TypeScriptなら定義したデータに型が付くし検証もされる
  • カスタムコンポーネント追加が簡単

導入が簡単

↓の通りに導入、

Next.js – Contentlayer

後はお好みの形式のデータを定義して、指定したディレクトリにMarkdownファイルを配置してビルドすればよい。

contentlayer.dev

簡単!

// Pagesコンポーネントからはこんな風に参照できる
import { useMDXComponent } from "next-contentlayer/hooks";
import { allNews } from "contentlayer/generated";
// 色々割愛

// 静的ページの生成用データを提供する
export async function generateStaticParams() {
  return allNews.map((doc) => ({
    slug: doc.slug,
  }));
}

// データ固有のメタデータ作る
export async function generateMetadata({
  params: { slug },
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const doc = allNews.find((doc) => doc.slug === slug);
  if (!doc) notFound();

  return buildPageMetadata({ title: doc.title, coverUrl: doc.coverUrl });
}

// ページ本体
export default function NewsPage({
  params: { slug },
}: {
  params: { slug: string };
}) {
  const doc = allNews.find((doc) => doc.slug === slug);
  if (!doc) notFound();
  const MDXContent = useMDXComponent(doc.body.code);

  return (
    <MDXContent components={mdxComponents} />
  );
}

TypeScriptなら定義したデータに型が付くし検証もされる

僕は機械に凡ミスを指摘してもらわないと生きるのが辛い。

Contentlayer+TypeScriptなら定義したデータの通りの型が生成される。

そのデータを参照するコンポーネントを書いているときにはバキバキに補完が効くし、requiredなフィールドが定義されてないとかはビルド時に警告が出てCIが落ちる。

最高か?

ただし課題はある。

github.com

1.0では対応されるよ->1年以上経過、なので気の長い話ではあるが。

Issueで触れられている computed fields も便利な機能で、定義したデータ内容から導出される値の計算を実装できる。普通にいわゆるModelクラスを作れてしまう。便利。

こんな感じで使っている。

const computedFields: ComputedFields = {
  slug: {
    type: "string",
    resolve: toSlug,
  },
};

export const News = defineDocumentType(() => ({
  name: "News",
  filePathPattern: "news/**/*.mdx",
  contentType: "mdx",
  fields: {
    title: { type: "string", required: true },
    category: { type: "string", required: true },
    date: { type: "date", required: true },
  },
  computedFields: {
    ...computedFields,
    coverUrl: {
      type: "string",
      resolve: (doc) => `/images/news/${toSlug(doc)}/cover.webp`,
    },
  },
}));

カスタムコンポーネント追加が簡単

Markdownは元々電子メールで多少はいい感じに見えるドキュメントを書くための形式なので簡素。そのため、サイトのコンテンツを記述するに当たってはかゆいところに手が届かない場面がある。

が、カスタムコンポーネントの組み込みが簡単なので、ちょっと特別なコンテンツを見せたいときにReactでコンポーネントを書けば書けてしまう。

contentlayer.dev

const mdxComponents: MDXComponents = {
  // アイコン置けるようにしたり
  Icon: ({ type, size, color }) => (
    <Icon type={type} size={size} color={color} />
  ),
  // ちょっと変わった見た目のコンポーネント置けるようにしたり
  BorderedList: ({ title, children }) => (
    <ArticleBorderedList title={title}>{children}</ArticleBorderedList>
  ),
};

export default mdxComponents;

簡単便利。

そんな感じ!


まぁ、目線はシステムをDevOpsしているエンジニアのものではある。

mdxファイルを書く非エンジニアからの評判は? というとボチボチといったところ。

GitHub組み込みのVSCodeを使ってもらって、ベースブランチへのマージでプレビュー環境にデプロイしているけれど、完璧なUXなWordPressとかに比べたら劣る体験であるのは否めない。

ローカルマシンでnext devできる人が運用するならコンテンツを管理運用する体験もよいかと思う。

*1:ただし破壊的な変更があっても泣かないこと