ONOF
DecupCMSとAppRouterの愛称がよくない件
2024/04/01

普段仕事で使用しないような技術に触れたいと思い、NextJSでJamstackなブログを作ることにしました。このブログのことです。NextJS、AppRouter、DecupCMS(旧名、NetlifyCMS)でNetlifyでホスティングする形としたんですが、OGP画像の自動生成がうまいこといかなかったので、メモします。

CMSとは

CMS(Content Management System)は、ウェブサイトのコンテンツを管理、編集、公開するためのシステムです。これにより、HTMLやCSSなどの専門的な知識がなくても、テキスト、画像、動画などのウェブコンテンツを簡単に管理できます。また、複数のユーザーが同時にコンテンツを更新することも可能にします。国産CMSとしてMicroCMSが有名です。また、NotionをCMSとして活用する活用する方法もありますが、ブログをマークダウンで記載していきたかったのでDecupCMSを採用しました。

DecupCMSとは

DecupCMS(以前の名前はNetlifyCMS)は、静的サイトジェネレータと連携することにより、ウェブサイトのコンテンツを管理するためのオープンソースのコンテンツ管理システムです。これにより、マークダウン、JSON、YAML、TOMLなどの形式でコンテンツを直接編集できます。また、Gitリポジトリへのコミットとプッシュを自動化し、ウェブサイトの更新を容易にします。GitHubベースのCMSなので、Netlifyでホスティングすればかなり楽に仕組みを用意できます。

DecupCMSの導入方法

NextJSを使用して、Netlifyにホスティングする場合のDecupCMSの導入方法は以下の通りです。

  1. まず、GitHubに新しいリポジトリを作成します。このリポジトリは、あなたのウェブサイトのコードとコンテンツを保存する場所になります。
  2. 次に、NextJSを使用して新しいウェブサイトを作成します。これは、ターミナルで npx create-next-app your-app-name を実行することで行うことができます。
  3. ウェブサイトが作成されたら、DecupCMSを導入します。これは、npm install decapcms を実行することで行うことができます。
  4. DecupCMSの設定ファイル(通常は config.yml)をプロジェクトのルートディレクトリに作成します。このファイルには、あなたのGitHubリポジトリの詳細や、あなたが使用したいコンテンツタイプの定義を含めます。
  5. 最後に、Netlifyにログインし、新しく作成したGitHubリポジトリを指定して新しいサイトを作成します。Netlifyは自動的にあなたのサイトをビルドし、公開します。

以上がNextJSを使用して、Netlifyにホスティングする場合のDecupCMSの導入方法です。これにより、あなたはマークダウンで記事を書き、それを自動的にウェブサイトに反映させることができます。

NextJSのAppRouterでマークダウンを読み込む方法

NextJSのAppRouterを用いて、マークダウンファイルを読み込み、記事ページを作成する方法は以下の通りです。

  1. まず、マークダウンファイルを保存するディレクトリを作成します。たとえば、postsという名前のディレクトリを作成することができます。
  2. 次に、NextJSのAppRouterを設定します。pagesディレクトリ内に、[slug].jsという名前のファイルを作成します。このslugは、マークダウンファイルの名前(拡張子なし)に対応します。
  3. この[slug].jsファイル内で、getStaticPathsgetStaticProps関数を利用します。getStaticPaths関数は、全ての可能なslugのリストを返し、NextJSにどの記事ページを事前に生成するべきかを伝えます。getStaticProps関数は、特定のslugに対する記事データ(マークダウンファイルの内容)を取得します。
  4. getStaticProps関数内で、マークダウンファイルを読み込み、その内容を解析します。これには、fsモジュールとgray-matterライブラリ(マークダウンファイルのfront matterを解析するためのライブラリ)を使用します。
  5. 最後に、読み込んだ記事データをページコンポーネントのpropsとして渡します。ページコンポーネント内で、この記事データを用いて記事ページのレイアウトを作成します。

以上がNextJSのAppRouterでマークダウンを読み込む方法です。これにより、マークダウンファイルに基づく記事ページを動的に生成することが可能となります。これにより、記事の作成と更新が容易になり、コンテンツの管理が大幅に効率化します。

OGPとは

OGP(Open Graph Protocol)とは、ウェブページがソーシャルメディアなど他のプラットフォーム上でどのように展示されるかをコントロールするためのプロトコルです。OGPを使用すると、サイトのタイトル、説明、画像などを定義し、それらがソーシャルメディアの投稿でどのように表示されるかをカスタマイズすることができます。

NextJSのバージョン14以降でのOGP画像の動的生成

AppRouterのMETAタグ設定機能の登場により、OGPの設定が非常に楽になっています。index.tsxの横にopengraph-image.tsxを作成、そちらにコードを記載することにより、OGP画像の自動生成も可能です。画像生成にはImageResponseを使用しており、そちらのベースはsatoriになってるみたいです。下記は公式のページのサンプルですが、見ていただければ大体わかるのではと思います。

1import { ImageResponse } from 'next/og'
2 
3// Route segment config
4export const runtime = 'edge'
5 
6// Image metadata
7export const alt = 'About Acme'
8export const size = {
9  width: 1200,
10  height: 630,
11}
12 
13export const contentType = 'image/png'
14 
15// Image generation
16export default async function Image() {
17  // Font
18  const interSemiBold = fetch(
19    new URL('./Inter-SemiBold.ttf', import.meta.url)
20  ).then((res) => res.arrayBuffer())
21 
22  return new ImageResponse(
23    (
24      // ImageResponse JSX element
25      <div
26        style={{
27          fontSize: 128,
28          background: 'white',
29          width: '100%',
30          height: '100%',
31          display: 'flex',
32          alignItems: 'center',
33          justifyContent: 'center',
34        }}
35      >
36        About Acme
37      </div>
38    ),
39    // ImageResponse options
40    {
41      // For convenience, we can re-use the exported opengraph-image
42      // size config to also set the ImageResponse's width and height.
43      ...size,
44      fonts: [
45        {
46          name: 'Inter',
47          data: await interSemiBold,
48          style: 'normal',
49          weight: 400,
50        },
51      ],
52    }
53  )
54}

本題

ここからが本題になります。上記は非常に楽なのでぜひ使いたかったですが、DecupCMSだと単純な方法では使えませんでした。細かく追ってないので少々自信がない部分もあるんですが、opengraph-image.tsxが実行されるのはメタタグを書き込むこともあり、ページが生成された後のようでslagを渡された後、同名のマークダウンファイルにアクセスができないようでした。マークダウンファイルにアクセスができないと、OGP画像に乗せるブログタイトルが取得できず、slag文字列をOGP画像として使用することになり、なんともおかしな感じになります。

なお、マークダウンファイルにアクセスできないのが原因なので、MicroCMSやNotionAPIなどを利用する方法は大丈夫のようです。

回避策

回避策としては、ページを生成する際にsatoriとsharpを使用して画像を書き出しそれをOGPとして生成する方法をとりました。

1import satori from "satori";
2import sharp from "sharp";
3import fs from "fs";
4import OgpImageTemplate from "@/app/_components/OgpImageTemplate";
5
6import { Buffer } from 'node:buffer';
7
8export const runtim = "edge";
9
10export const writeOgpImage = async (title: string, slag: string) => {
11    const path = `./public/img/${slag}`; //ここのファイル名をメタタグに設定する
12    if(!fs.existsSync(path)) fs.mkdirSync(path);
13
14    const image = await generateOgpImage(title);
15
16    await fs.promises.writeFile(`${path}/ogp.png`, image);
17}
18
19export const generateOgpImage = async (title: string) => {
20    const fontMedium = fs.readFileSync("public/font/NotoSansJP-Regular.ttf");
21    const fontBold = fs.readFileSync("public/font/NotoSansJP-Bold.ttf");
22
23    const svg = await satori(
24            <OgpImageTemplate> //ここでOGP画像のDONを返却してます
25                {title}
26            </OgpImageTemplate>,
27        {
28            width: 1200,
29            height: 630,
30            fonts: [
31                {
32                    name: "Noto Sans JP",
33                    data: fontMedium,
34                    style: "normal",
35                    weight: 500,
36                },
37                {
38                    name: "Noto Sans JP",
39                    data: fontBold,
40                    style: "normal",
41                    weight: 700,
42                },
43            ],
44        },
45    );
46    return await sharp(Buffer.from(svg)).png().toBuffer();
47}

その他

こちらはDecupCMSにあまり関係ないですが、メタタグの設定をする際にadobeフォントを使用していると、TwitterOGP画像の設定に失敗するみたいです。NextJsではおとなしくGoogleフォントを使用したほうがいいですね。

まとめ

ということで、まあやりたかったことは実現できたので最低限はクリアできてるかなと思いながらも、またも力業的な感じで納めてしまいました。私の悪い癖です。本来はどこの処理がどう動いているからここで、こうするとうまくいかない、という根拠まで調べるべきなんですが、そこまでは至らずいったんここで終わっておきました。

DecupCMSで結構使ってしまったので、DecupCMSを生かしつつ回避する方法を探りきれいに対応できるよう模索したいと思います。NotionAPIあたりを使ってタイトルだけAPIで取れたりしないかな。追加で、NotionからDecupの投稿ページに簡単に転記できたりすると私の運用が楽になってよさそう。