なつねこメモ

主にプログラミング関連のメモ帳 ♪(✿╹ヮ╹)ノ

書いてあるコードは自己責任でご自由にどうぞ。記事本文の無断転載は禁止です。

2022/02/20

contentlayer でらくらく Markdown ブログサイト構築

カテゴリー:TypeScriptNext.js

ここ最近は Unity の記事ばかりでしたが、今回は Node.js のお話と言うことで、最近お気に入りのライブラリー、 contentlayer について紹介します。
contentlayer は名前の通り、 Next.js などのコンテンツ周りを良い感じに取得してくれたり、コンパイルしてくれるライブラリです。
これを使うことで、簡単に Markdown などのドキュメントから静的サイトを構築することが出来ます。

ということで、まずは導入方法から。
いつも通り、 yarn 経由で導入します。

$ yarn add contentlayer next-contentlayer

導入したら、 contentlayer.config.ts という設定ファイルを作成します。
このブログの場合は、以下のような設定になります。

contentlayer.config.ts

import {
  ComputedFields,
  defineDocumentType,
  defineNestedType,
  makeSource,
} from "contentlayer/source-files";
import { remark } from "remark";
import strip from "strip-markdown";

const getSummarizedText = (markdown: string) => {
  const text = remark().use(strip).processSync(markdown).toString();
  const firstSentence = text.indexOf("。");
  if (firstSentence <= 120) {
    return text.substring(0, firstSentence + 1);
  }

  return text.substring(0, 120) + "...";
};

const computedFields: ComputedFields = {
  summary: {
    type: "string",
    resolve: (doc) => {
      return getSummarizedText(doc.body.raw);
    },
  },
};

const Article = defineDocumentType(() => ({
  name: "Article",
  filePathPattern: "**/*.md",
  contentType: "markdown",
  fields: {
    title: { type: "string", required: true },
    date: { type: "string", required: true },
    basename: { type: "string", required: true },
    categories: { type: "list", default: [], of: { type: "string" } },
  },
  computedFields,
}));

const Redirect = defineNestedType(() => ({
  name: "Redirect",
  fields: {
    from: { type: "string", required: true },
    to: { type: "string", required: true },
  },
}));

const Redirects = defineDocumentType(() => ({
  name: "Redirects",
  filePathPattern: "redirects.json",
  contentType: "data",
  fields: {
    redirects: { type: "list", default: [], of: Redirect },
  },
}));

const config = makeSource({
  contentDirPath: "contents",
  documentTypes: [Article, Redirects],
});

export default config;

defineDocumentType でドキュメントの形式、 front-matter などの型、その他フィールドを定義します。
こうすることによって、あとで contentlayer からデータを取得する際に、型が効くようになります。
computedFields を使うことで、 Markdown などのファイル名や内容から、動的に中身を生成することも可能です。
今回の場合は、 description 用の summary を動的に生成しています。

そして、 next.config.js に以下の設定を追記します。

next.config.js

const { withContentLayer } = require("next-contentlayer");

module.exports = withContentLayer()({
  // 元の Next.js の設定
  // 多分 latest (0.1.x) のバグ
  webpack: (config) => {
    config.resolve.alias = {
      ...config.resolve.alias,
      "contentlayer/generated": path.join(__dirname, ".contentlayer/generated"),
    };

    return config;
  },
});

これで準備完了です。
最後に、取得部分は以下のようにします。

import { allArticles, allRedirects } from "contentlayer/generated";
import type { Article as Entry } from "contentlayer/generated";

const getStaticPaths: GetStaticPaths<PathParams> = async () => {
  const paths = allArticles.map((w) => {
    const [year, month, day, slug] = w.basename.split("/");

    return {
      params: {
        slug: [year, month, day, slug] as [string, string, string, string],
      },
    };
  });

  return {
    paths,
    fallback: false,
  };
};

このように、 all${name に指定した名前} で簡単に中身を取得することが可能です。
ということで、 contentlayer の紹介でした。
ちなみに、このブログもこの記事が反映されたタイミングで、内部で fs を使って集めるのでは無く、 contentlayer を用いた生成に切り替わっています。