普段仕事で使用しないような技術に触れたいと思い、NextJSでJamstackなブログを作ることにしました。このブログのことです。NextJS、AppRouter、DecupCMS(旧名、NetlifyCMS)でNetlifyでホスティングする形としたんですが、OGP画像の自動生成がうまいこといかなかったので、メモします。
CMS(Content Management System)は、ウェブサイトのコンテンツを管理、編集、公開するためのシステムです。これにより、HTMLやCSSなどの専門的な知識がなくても、テキスト、画像、動画などのウェブコンテンツを簡単に管理できます。また、複数のユーザーが同時にコンテンツを更新することも可能にします。国産CMSとしてMicroCMSが有名です。また、NotionをCMSとして活用する活用する方法もありますが、ブログをマークダウンで記載していきたかったのでDecupCMSを採用しました。
DecupCMS(以前の名前はNetlifyCMS)は、静的サイトジェネレータと連携することにより、ウェブサイトのコンテンツを管理するためのオープンソースのコンテンツ管理システムです。これにより、マークダウン、JSON、YAML、TOMLなどの形式でコンテンツを直接編集できます。また、Gitリポジトリへのコミットとプッシュを自動化し、ウェブサイトの更新を容易にします。GitHubベースのCMSなので、Netlifyでホスティングすればかなり楽に仕組みを用意できます。
1<https://decapcms.org/> 2
NextJSを使用して、Netlifyにホスティングする場合のDecupCMSの導入方法は以下の通りです。
npx create-next-app your-app-name
を実行することで行うことができます。npm install decapcms
を実行することで行うことができます。config.yml
)をプロジェクトのルートディレクトリに作成します。このファイルには、あなたのGitHubリポジトリの詳細や、あなたが使用したいコンテンツタイプの定義を含めます。以上がNextJSを使用して、Netlifyにホスティングする場合のDecupCMSの導入方法です。これにより、あなたはマークダウンで記事を書き、それを自動的にウェブサイトに反映させることができます。
NextJSのAppRouterを用いて、マークダウンファイルを読み込み、記事ページを作成する方法は以下の通りです。
posts
という名前のディレクトリを作成することができます。pages
ディレクトリ内に、[slug].js
という名前のファイルを作成します。このslug
は、マークダウンファイルの名前(拡張子なし)に対応します。[slug].js
ファイル内で、getStaticPaths
とgetStaticProps
関数を利用します。getStaticPaths
関数は、全ての可能なslug
のリストを返し、NextJSにどの記事ページを事前に生成するべきかを伝えます。getStaticProps
関数は、特定のslug
に対する記事データ(マークダウンファイルの内容)を取得します。getStaticProps
関数内で、マークダウンファイルを読み込み、その内容を解析します。これには、fs
モジュールとgray-matter
ライブラリ(マークダウンファイルのfront matterを解析するためのライブラリ)を使用します。以上がNextJSのAppRouterでマークダウンを読み込む方法です。これにより、マークダウンファイルに基づく記事ページを動的に生成することが可能となります。これにより、記事の作成と更新が容易になり、コンテンツの管理が大幅に効率化します。
OGP(Open Graph Protocol)とは、ウェブページがソーシャルメディアなど他のプラットフォーム上でどのように展示されるかをコントロールするためのプロトコルです。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} 55
1<https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image> 2
1<https://nextjs.org/docs/app/api-reference/functions/image-response> 2
1<https://zenn.dev/temasaguru/articles/2968736b5a2f41> 2
ここからが本題になります。上記は非常に楽なのでぜひ使いたかったですが、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 6 7import { Buffer } from 'node:buffer'; 8 9 10export const runtim = "edge"; 11 12 13export const writeOgpImage = async (title: string, slag: string) => { 14 const path = `./public/img/${slag}`; //ここのファイル名をメタタグに設定する 15 if(!fs.existsSync(path)) fs.mkdirSync(path); 16 17 18 const image = await generateOgpImage(title); 19 20 21 await fs.promises.writeFile(`${path}/ogp.png`, image); 22} 23 24 25export const generateOgpImage = async (title: string) => { 26 const fontMedium = fs.readFileSync("public/font/NotoSansJP-Regular.ttf"); 27 const fontBold = fs.readFileSync("public/font/NotoSansJP-Bold.ttf"); 28 29 30 const svg = await satori( 31 <OgpImageTemplate> //ここでOGP画像のDONを返却してます 32 {title} 33 </OgpImageTemplate>, 34 { 35 width: 1200, 36 height: 630, 37 fonts: [ 38 { 39 name: "Noto Sans JP", 40 data: fontMedium, 41 style: "normal", 42 weight: 500, 43 }, 44 { 45 name: "Noto Sans JP", 46 data: fontBold, 47 style: "normal", 48 weight: 700, 49 }, 50 ], 51 }, 52 ); 53 return await sharp(Buffer.from(svg)).png().toBuffer(); 54} 55
こちらはDecupCMSにあまり関係ないですが、メタタグの設定をする際にadobeフォントを使用していると、TwitterOGP画像の設定に失敗するみたいです。NextJsではおとなしくGoogleフォントを使用したほうがいいですね。
ということで、まあやりたかったことは実現できたので最低限はクリアできてるかなと思いながらも、またも力業的な感じで納めてしまいました。私の悪い癖です。本来はどこの処理がどう動いているからここで、こうするとうまくいかない、という根拠まで調べるべきなんですが、そこまでは至らずいったんここで終わっておきました。
DecupCMSで結構使ってしまったので、DecupCMSを生かしつつ回避する方法を探りきれいに対応できるよう模索したいと思います。NotionAPIあたりを使ってタイトルだけAPIで取れたりしないかな。追加で、NotionからDecupの投稿ページに簡単に転記できたりすると私の運用が楽になってよさそう。
- Next.jsの記事 -