isirmtブログ
タグシリーズ投稿
お気に入りのタグ

ブログを書いている人

isirmt

ホームページ

GitHub
github_user_icon
isirmt
Software & Web
@isirmt

(管理者用)ダッシュボード

© isirmtBuild with NextjsBlogWithGitPAT  (535e811)
  1. App Router で動的にOGP画像を作りたい【Next.js】
  2. Next.js14 の og:image
  3. 動的に画像を生成する
  4. Dynamic Routingでの書き方
  5. 発生する問題
  6. 回避策
  7. おわりに
共有
2024
8
16

App Router で動的にOGP画像を作りたい【Next.js】

コメント: 0件
Next.js
App Router
SEO

Next.js14 の og:image

Metadata型のJSON形式にopenGraphというプロパティがあります。

例
openGraph: {
  title,
  description,
  url,
  siteName,
  images: {
    url: imageURL,
    width: 1200,
    height: 630
  },
  type,
}

imagesに画像を取得できるURLを当てはめると自動的に<head />へ挿入されます。

例えば、

images: `${process.env.NEXT_PUBLIC_URL}/open-graph.png`

とすれば、appディレクトリの直下のopen-graph.pngを参照してくれます。

動的に画像を生成する

Next.js App Routerではopengraph-image.(png | jpg | ...)というファイルをpage.tsxと同じディレクトリに配置するだけで自動で<head />へ追加されます。その際に、MetadataのmetadataBaseプロパティが参照され、metadataBase/(opengraph-img)があるディレクトリ/opengraph-image.pngのように形作られます。

また画像ファイルではなくJSX / TSX ファイルを置くこともできます。

LINKMetadata Files: open...API Reference f...https://nextjs.org/docs/app/api-refe...thumb

opengraph-image.tsx
import { ImageResponse } from 'next/og'
 
export const runtime = 'edge'
 
export const alt = 'TITLE'
export const size = {
  width: 1200,
  height: 630,
}
 
export const contentType = 'image/png'
 
export default async function Image() { 
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        TITLE
      </div>
    ),
    {
      ...size,
    }
  )
}

公式ドキュメントから少し抜粋しました。

JSX記法で画像を構成し、200ステータスレスポンスでog:imageとして利用できるようになります。

サンプルではCSSをstyleプロパティで記述していますが、全て対応しているわけではありません。

LINKGitHub - vercel/sato...Enlightened lib...https://github.com/vercel/satori#css...thumb

ここに記載のプロパティのみが有効で、それ以外を宣言すると500エラーレスポンスを吐き出します。

Dynamic Routingでの書き方

公式ドキュメントにある通り、page.tsxと同じ書き方で実現できます。

export default function Image({ params }: { params: { slug: string } }) {
  // ...
  console.log(params.slug);
}

発生する問題

ここまでは良かったのです。。。例えば、掲載中のシリーズ一覧を表示するページがあり、特定のシリーズをクリックするとシリーズの詳細が表示される...の場合は

.
└── app/
    ├── series/
    │   ├── [slug]/
    │   │   ├── page.tsx
    │   │   └── opengraph-image.tsx
    │   └── page.tsx
    ├── page.tsx
    └── opengraph-image.tsx

/series/page.tsxと/series/[slug]/page.tsx上述のページを分けて表示できますし、params.slugでお好みのOGP画像を作ることができます。

では、こちらはどうでしょうか。

.
└── app/
    ├── post/
    │   ├── [...slug]/
    │   │   ├── page.tsx
    │   │   └── opengraph-image.tsx
    │   └── page.tsx
    └── page.tsx

[slug]から[...slug]に変わりました。型でいうと、

[slug]
{ params: { slug: string } }
[...slug]
{ params: { slug: string[] } }

このように変わります。/post/nextjs/ogpというパスが与えられた場合、

paams.slug = ["nextjs", "ogp"];

と格納されます。これをCatch-all セグメントと呼びます。

では、同じようにImageResponseを作ろうとすると...

export default function Image({ params }: { params: { slug: string[] } }) {
  // ...
  console.log(params.slug.join('/'));
}

と書けますが実は使えないです。 私も「これ」を作るにあたって、同じ問題に遭いました。

調べると Issue でも取り上げられていました。

簡単に言うと、Catch-all下にサブルートがあるから実現できないよ!ということです。

サブルートは、/以外の/postや/series/aboutといったトップページ以外のディレクトリで指されるページです。今回の例ではopengraph-image.tsxとpage.tsxがparams.slugを使うサブルートとして共存するからではないでしょうか。

but all the possible paths including the request url paths to og image are caught by the catch-all routes.

OGP画像へのパスを含む全てのパスがCatch-allされるため、論理的にアクセスが成り立たなくなるようですね。

Issue の方ではslugを利用していなかったのでRoute-Groupsを作ってOGP画像を切り替えたらどうか、という話でつながっていきます。

回避策

Page Routerの時と同じ方法で解決しましょう。

thumb

このURLで上の画像が取得できます。即ち、API RouteとDynamic Routingを組み合わせた手法です。

/src/app/api/ogp-posts/[...slug]/route.ts
import { NextRequest } from "next/server";
import { ImageResponse } from "next/og";
import { getPost } from "@/lib/getPosts";

export async function GET(req: NextRequest, context: { params: { slug: string[] } }) {
  const slug = decodeURIComponent(context.params.slug.join('/'));
  const { data } = await getPost(`${process.env.GIT_POSTS_DIR!}/${slug}.md`);

  return new ImageResponse(<div style={{
    width: "100%",
    height: "100%",
    backgroundImage: `url(${process.env.NEXT_PUBLIC_URL}/ogp_back.png)`,
    justifyContent: "center",
    display: "flex",
    alignItems: "center",
    flexDirection: "column",
    fontSize: "50px"
  }}>
    <div style={{
      width: "70%",
      lineHeight: "1.2",
      fontWeight: "bold"
    }}>{data.title}</div>
  </div>,
    {
      width: 1200,
      height: 630,
      status: 200
    }
  )
}

API を簡単に用意できるのは本当に助かります。context引数は現状paramsだけを持っているようです。

おわりに

Catch-all について仕様が理解できた気がします。

opengraph-imageは静的な場合に非常に使えそうですが、動的の場合はまだまだAPI Routeの便利さに頼ることになりそうです。

他の改善方法等があるなら、今後試してみたいですね。

コメント

自動更新
コメントはまだありません
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.
LINKRouting: Dynamic Rou...Dynamic Routes ...https://nextjs.org/docs/pages/buildi...thumb
LINKError: Catch-all mus...Verify canary r...https://github.com/vercel/next.js/is...thumb
LINKError: Catch-all mus...Verify canary r...https://github.com/vercel/next.js/is...thumb
LINKRouting: Route Group...Route Groups ca...https://nextjs.org/docs/app/building...thumb
LINKhttps://blog.isirmt....https://blog.isirmt.com/api/ogp-post...
LINKFile-system conventi...API reference f...https://nextjs.org/docs/app/api-refe...thumb