はじめに
今回はNext.jsでApp Routerを指定した場合のMetadataの設定方法を紹介します。合わせて、OGP画像の生成方法も紹介します。
Metadataとは?
Metadataとは、Headタグの内容を定義するもので、pages router時代には<Head>を使って定義していたものです。
pages.tsxまたはlayout.tsxに以下のように定義します。
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 | const siteName = "サンプルWebSite"; const description = "OGPのサンプルWebSiteです。"; const url = "https://hoge.netlify.app/"; export const metadata: Metadata = { title: { default: siteName, template: `%s | ${siteName}`, }, description, openGraph: { title: siteName, description, url, siteName, locale: "ja_JP", type: "website", }, twitter: { card: "summary_large_image", title: siteName, description, site: "@hogehgoe", creator: "@hogehoge", }, metadataBase: new URL(process.env.URL ?? "http://localhost:3000"), }; |
基本的には、上位階層で指定したMetadataが下位階層にも適用されていきます。下位階層でこれらの値を変更したい場合は、同じようにmetadataを作成し、変更したい値のみ定義することで、上書きすることができます。
また、テンプレート機能を使用することで、下位階層から受けた値を埋め込むようなこともできます。
例えば、
子ページのタイトル | 全体のWebサイト名
といったタイトルを指定したい場合、上で設定しているように
template:
とすることで、実現できます。%s | ${siteName}
,
動的に設定するには
Metadataを動的にするには、
generateMetadata()
というfunctionをexportすれば、実現できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | const apiUrl = "https://jsonplaceholder.typicode.com/posts"; type Post = { userId: number; id: number; title: string; body: string; }; const getPost = async (id: string) => { const res = await fetch(new URL(`${apiUrl}/${id}/`), { cache: "no-cache" }); // return (await res.json()) as Post; }; export async function generateMetadata({ params }: { params: { id: string } }) { const res = await getPost(params.id); return { title: res.title }; } |
generateMetadataでのパラメータの取り方は、Pageと同じです。また、この中でもAPIをフェッチすることが出来るので、取得したデータをもとに、タイトルを生成することができます。
OGP画像
App Routerを使用した場合、page.tsxと同じ階層に
opengraph-image.(jpg|png|svg|ts|tsx)
を置いておくと、page.tsxがレンダリングされる際に、自動的にmetaタグ
og:image
が挿入されます。
(
jpg, png, svgの場合は、配置した画像がそのままOGP画像として使用されます。
今回は、「tsx」で動的に生成させるため、
opengraph-image.tsx
を作成します。
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 | import { ImageResponse } from "next/server"; import { getPost } from "@/lib/posts"; // Edge Functionとして動作する export const runtime = "edge"; export const revalidate = 0; // 数値の場合は秒数となる export const size = { width: 1200, height: 630, }; export const contentType = "image/png"; export default async function og({ params }: { params: { id: string } }) { const res = await getPost(params.id); return new ImageResponse( ( <div style={{ display: "flex", backgroundColor: "white", width: "100%", height: "100%", }} > <div style={{ width: "100px", height: "100%", backgroundColor: "#03a9f4" }} /> <div style={{ margin: "40px", fontSize: "40px", }} > {res.title} </div> <div style={{ position: "absolute", left: "140px", bottom: "40px", fontSize: "30px", }} > 日本語テキストです。 </div> </div> ), { ...size, }, ); } |
- runtime, revalidateは「Route Segment Config」の項目です。
- 今回は、netlify上でOGPを動的生成するため、runtimeは”edge” にしています。
- 参考: https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config
- レンダリングにsatoriを使用しているため、次のような制約があります。
- 子にDOM要素を含むdivは、
display: flex
もしくはdisplay: none
のいずれかである必要があります。 - 使用できるCSSに制約があります。
参考: https://github.com/vercel/satori#css
- 子にDOM要素を含むdivは、
- satoriを試せるPlaygroundで先にレイアウトを組むと楽です
この状態でページを開いてみると、以下のようにmetaタグが挿入されています。
このURLを開いてみると、生成された画像を確認することができます。
カスタムフォント
カスタムフォントを使用するには、大きく分けて次の2つの方法があります。
- next/fontのようなライブラリを使用してフォントファイルを内包する
- OGP画像生成時に、毎回Google Fontsからダウンロードする
前者のほうが、OGP画像を生成するコストが低くなりますが、フォントファイルを内包する必要があるため、ビルド後のバンドルサイズが大きくなってしまいます。
これは、Edge Functionを使用する場合に問題があります。
例えば、Vercel場合、Edge FunctionのCode sizeはhobbyで1MB、Enterpriseでも4MBとなっています。(netlifyは20MB)
特にVercelのEdge Fcuntionを使用する場合には、フォントをそのまま埋め込むのは無理があるため、必要な文字のみに絞る「サブセット化」が必要になります。(サブセットフォントメーカーなどを使用するようです。)
それに対し、後者は、ランタイムでフォントファイルをダウンロードしに行くため、効率が良いとは言えませんが、デプロイできない問題は解消できます。
この実装方法はいくつかありますが、こちらの記事が一番参考になります。
netlifyへデプロイ
今回はnetlifyにデプロイしてみます。
netlifyへデプロイすると、自動的にEdge Functionに登録され、実際にOGP画像が生成されます。
https://joyful-paletas-efc0cf.netlify.app/posts/1/
OGP確認ツール上でも、Metaタグに設定された内容や、OGP画像が反映されていることが確認できます。
さいごに
様々なサービスでメタタグやOGP画像が読み込まれることから、パブリックサイトではほぼ必須の実装になると思いました。