Next.jsのSSGでビルド時にogpを生成しようとしてつまずいたこと。CloudFrontでのつまずきも

2024/12/02 投稿

はじめに

Next.jsのSSGで作成したこのブログにogpを追加しようとしてつまずいたので、その解決方法を紹介します。また、CloudFrontでのつまずきも合わせて紹介します。

「そんなのあたりまえだろ」と思うこともあるかもしれませんが、私のような初心者にとってはつまずきどころだったので、解決方法を共有します。

一年前の挑戦

1年前にも同じようなことをやろうとして、結果的にうまくいきませんでした。以下の記事曰く、next.js側の問題だったそうなのであきらめていました。

今回の挑戦

一年たった今なら、解決していると信じて再挑戦しました。

ogpの作り方

基本的には公式の解説を参考にすればいいです。ogpを作りたいページに対応するpage.tsxと同じところにopengraph-image.tsxを作ります。そして、next/ogを使ってogpを生成します。

export const dynamic = "force-static"; が必要

これについては、以下のようにエラーで教えてくれたので、opengraph-image.tsxに追加しました。
⨯ Error: export const dynamic = "force-static"/export const revalidate not configured on route "/opengraph-image" with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export

[...slug] といったルーティングでは使いずらい

Error: Catch-all must be the last part of the URL.
このようなエラーが出たので調べると、このissueで詳しく説明されていました。
app/ ├── opengraph-image.tsx ├── [...slug]
こんな感じで[...slug]のサブルートにopengraph-image.tsxを置けないらしく、例えば次のようにして回避できるようです。
app/ ├── (shared)/ │ ├── opengraph-image.tsx ├── [...slug]
ただ、今回はルーティングが単純なので[...slug][categoryId]/[postId]のように変更してしまいました。

画像がうまくはれない

imgタグのsrcに直接リンクを書いたり、Static Importを使ったりしても、うまくいきませんでした。最終的にfsで画像を読み込み、Base64にエンコードして埋め込む方法で成功しました。以下に最終的なコードを示します。
import { ImageResponse } from 'next/og'; import fs from 'fs'; import path from 'path'; import { getCategory, getPost } from "@/app/main"; import { category } from '@/app/type'; export const dynamic = "force-static"; export const size = { width: 1200, height: 630, }; export const contentType = 'image/png'; export const generateStaticParams = async () => { const categories: category[] = await getCategory(); return categories.flatMap((category) => category.posts.map((post) => ({ categoryId: post.slug[0], postId: post.slug[1], })) ); }; export default async function ImageOG({ params }: { params: { categoryId: string, postId: string } }) { const iconPath = path.join(process.cwd(), 'public/images/kitatai.png'); if (!fs.existsSync(iconPath)) { throw new Error('Icon image not found'); } const iconBuffer = fs.readFileSync(iconPath); const post = await getPost(params.categoryId, params.postId); if (!post || !post.frontMatter) { throw new Error(`Post not found for categoryId: ${params.categoryId}, postId: ${params.postId}`); } return new ImageResponse( ( <div style={{ fontSize: 64, backgroundColor: 'white', width: '100%', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', // 垂直方向に中央揃え alignItems: 'center', // 水平方向に中央揃え position: 'relative', fontFamily: 'Arial, sans-serif', overflow: 'hidden', color: 'black', }} > {/* タイトル */} <div style={{ fontWeight: 'bold', fontSize: '72px', textAlign: 'center', lineHeight: '1.2', maxWidth: '80%', // 横幅を調整 wordWrap: 'break-word', // 必要に応じて単語を分割 overflowWrap: 'break-word', // 長い単語も折り返す whiteSpace: 'normal', // 折り返しを有効に }} > {post.frontMatter.title} </div> {/* 下部右にアイコンと「TAICHI KITAJIMA」 */} <div style={{ position: 'absolute', bottom: '20px', right: '20px', display: 'flex', alignItems: 'center', }} > <img src={`data:image/png;base64,${iconBuffer.toString('base64')}`} alt="Icon" style={{ width: '80px', height: '80px', marginRight: '10px', borderRadius: '50%', boxShadow: '0 4px 10px rgba(0, 0, 0, 0.2)', }} /> <span style={{ fontFamily: 'monospace', fontWeight: 'bold', fontSize: '48px', }} > TAICHI KITAJIMA </span> </div> </div> ), { ...size, } ); }

こんな感じになりました。

ogpのサンプル

CloudFrontでのつまずき

CloudFrontでindex.htmlを省略しても目的のページを表示できるように以下の解説に従って関数を設定していました。
async function handler(event) { const request = event.request; const uri = request.uri; // Check whether the URI is missing a file name. if (uri.endsWith('/')) { request.uri += 'index.html'; } // Check whether the URI is missing a file extension. else if (!uri.includes('.')) { request.uri += '/index.html'; } return request; }
この方法だと拡張子がない場合、index.htmlを追加することになります。しかし、以上の方法で生成したogpはopengraph-imageという名前になり拡張子がないです。今回は例外として、opengraph-imageが含まれる場合はindex.htmlを追加しないようにしました。
function handler(event) { var request = event.request; var uri = request.uri; // Check whether the URI is missing a file name. if (uri.endsWith('/')) { request.uri += 'index.html'; } // Check whether the URI is missing a file extension. else if (!uri.includes('.') && !uri.includes('opengraph-image')) { request.uri += '/index.html'; } return request; }

まとめ

Next.jsのSSGでogpを動的生成している記事が少なかったので、今回の記事を書きました。また、CloudFrontでのミスも合わせて紹介しました。この記事が同じような問題に悩む人の助けになれば幸いです。