はじめに
React Router v7とCloudflare PagesでOGPタグの設定とOGP画像生成をやってみたいと思います。
OGPタグの設定
全体で共通の設定
アプリケーション全体で共通の設定をするには、root.tsxのレイアウトコンポーネントに定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | export function Layout({ children }: { children: React.ReactNode }) { return ( <html lang="ja"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> {/* OGP:サイト名 */} <meta property="og:site_name" content="プロフィール帳" /> {/* X向けの設定 */} <meta property="twitter:card " content="summary_large_image" /> <meta property="twitter:site" content="@a_kaizaki" /> <Meta /> <Links /> </head> <body> {children} <ScrollRestoration /> <Scripts /> </body> </html> ); } |
meta()関数による動的な設定
React Router v7で、ルートモジュールごとに
meta()
関数を定義することで、タイトルタグやメタタグを指定できます。ここで指定した内容は先ほどのレイアウトコンポーネントにある
<Meta />
に入ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | import type { Route } from "../../.react-router/types/app/routes/+types/profile"; import { profiles } from "~/db"; export async function loader({ params }: Route.LoaderArgs) { // meta・コンポーネントで使用するデータを取得 return profiles.find((profile) => profile.id === params.id); } export function meta({ data, location }: Route.MetaArgs) { // loaderで取得したデータを元にmeta情報を生成 if (!data) { return { title: "Not Found", }; } const title = `${data.name}さんのプロフィール`; const description = `${data.name}さんのプロフィールをチェックしよう`; console.log("location.href", location); return [ { title, }, { name: "description", content: description }, { property: "og:title", content: title }, { property: "og:description", content: description }, { property: "og:type", content: "profile" }, { property: "og:url", content: `https://example.pages.dev${location.pathname}`, }, ]; } export default function Profile({ params, loaderData }: Route.ComponentProps) { return ( <div> <h1>プロフィール詳細</h1> <p>ID: {params.id}</p> {loaderData ? ( <> <p>名前: {loaderData.name}</p> <p>プロフィール: {loaderData.description}</p> </> ) : ( <p>Not Found</p> )} </div> ); } |
基本的には、Remix v2の時と似たような考え方ですが、React Router v7で導入された「型安全なルーティング」を活用しています。
meta()
関数のPropsの型として使っているのは、このルートモジュールを元に自動生成された
Route.MetaArgs
です。
今回は、この中に含まれている
data
を使っています。これは、
loader()
の返り値の型になっています。
meta()
関数の戻り値は配列ですが、その1つひとつのオブジェクトは、以下のようなキーを指定することができます。
- title
タイトルタグを指定するためのもの - name
メタタグのname
を指定するためのもの。(descriptionやkeywordなどを指定する) - property
メタタグのproperty
を指定するためのもの。主に、OGPのプロパティを指定するために使用する。 - content
メタタグのcontent
を指定するためのもの。
これで、以下のようなメタタグが出力されるようになりました。
1 2 3 4 5 6 7 8 9 10 11 | <meta charSet="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <meta property="og:site_name" content="プロフィール帳"/> <meta property="twitter:card " content="summary_large_image"/> <meta property="twitter:site" content="@a_kaizaki"/> <title>山田太郎さんのプロフィール</title> <meta name="description" content="山田太郎さんのプロフィールをチェックしよう"/> <meta property="og:title" content="山田太郎さんのプロフィール"/> <meta property="og:description" content="山田太郎さんのプロフィールをチェックしよう"/> <meta property="og:type" content="profile"/> <meta property="og:url" content="https://example.pages.dev/profiles/taro"/> |
OGP画像の生成
ここからは、React Router v7でOGP画像を生成し、メタタグに挿入するところまでをやってみます。
Remix時代のこちらの記事を参考にしながら、実装しました。
@vercel/ogの導入
OGPの生成には @vercel/og を使いますが、Cloudflare Pagesで動かすためにラッパーを使います。
1 | pnpm add @cloudflare/pages-plugin-vercel-og |
middlwareの実装
OGPを生成するには、Cloudflare PagesのFunctions機能を使います。
今回は、各ページごとに異なるOGPを生成するため、Cloudflare PagesのMiddleware機能を使います。
functions/profiles/_middleware.tsx
を作成します。
このパスに作成することで、
profiles/
配下にアクセスされた場合のみに実行することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | import vercelOGPagesPlugin from "@cloudflare/pages-plugin-vercel-og"; import { profiles } from "~/db"; export const onRequest = vercelOGPagesPlugin({ imagePathSuffix: "/og-image.png", // ファイル名を定義 component: ({ pathname }) => { const paths = pathname.split("/"); // パスの最後の要素を取得 const slug = paths[paths.length - 1]; // データを取得 const profile = profiles.find((profile) => profile.id === slug); return ( <div style={{ display: "flex", backgroundColor: "white", width: "100%", height: "100%", }} > <div style={{ width: "100px", height: "100%", backgroundColor: "#03a9f4" }} /> <div style={{ margin: "40px", fontSize: "40px", }} > {/* 1つのノードにまとめないとCloudflareではエラーが出る */} {/* https://github.com/vercel/next.js/discussions/52657#discussioncomment-6442423 */} {`@${slug} / ${profile?.name}`} </div> <div style={{ position: "absolute", left: "140px", bottom: "40px", fontSize: "30px", }} > {profile?.description} </div> </div> ); }, options: { width: 1200, height: 630, }, autoInject: { // メタタグを自動で挿入 openGraph: true, }, }); |
OGP画像のレイアウトは以前作ったものをそのまま使ってみました。
実際に動作させるには、ローカル環境で
pnpm run build && wrangler pages dev
を実行するか、実際にデプロイする必要があります。
これで、以下の画像が生成されました。
メタタグにも以下のように挿入されています。
1 2 3 | <meta property="og:image" content="http://localhost:8788/profiles/taro/og-image.png" /> <meta property="og:image:height" content="630" /> <meta property="og:image:width" content="1200" /> |
OGPチェッカーで確認
最後に、実際にデプロイしてOGPチェッカーで内容を確認してみます。
問題なく認識されていますね!
さいごに
React Router v7関連の情報はまだ少ないので、これからも調査して情報発信していきたいと思います!