Twitter や Facebook など、SNS 上で Web サイトをシェアしたときに表示される OGP 画像。 あらかじめ静的ファイルとしてサーバーに配置されることも多い一方で、 画像をサーバー側で動的に生成することができれば、より自由度高く、視覚に訴える OGP 画像で PR することができます。
追記
- 2021-05-25: Vercel 内部で使用されている
@vercel/nft
の影響で playwright が利用できない状況になっています。playwrite-core support を追加した pull request がマージされているので、そのうち解決すると思われます。- https://github.com/vercel/nft/issues/211
- https://github.com/vercel/nft/releases/tag/0.12.2
Playwright
Playwrightは、Headless Chrome を Node.js から手軽に扱うことができるパッケージです。 Headless Chrome とは、Window を持たない Web ブラウザです。主に E2E テストの自動化に使用されていますが、 ここでは Playwright のスクリーンショット機能を利用して、OGP 画像の生成に利用します。
Next.js の API ルート
Vercel の場合、API ルートは Serverless Function として AWS Lambda にデプロイされます。
Lambda の実行環境には様々な制約があるため、AWS Lambda 環境でも動作するように最適化された playwright-aws-lambda
パッケージを利用します。
サンプルコード
ディレクトリの構成:
$ tree -L 2 --dirsfirst ./pages./pages├── api│ └── ogp.js├── posts│ └── [id].js└── index.js
OGP 画像生成の処理:
/* pages/api/ogp.js */import ReactDOM from "react-dom/server";import * as playwright from "playwright-aws-lambda";const styles = `html, body {height: 100%;display: grid;}h1 { margin: auto }`;const Content = (props) => (<html><head><style>{styles}</style></head><body><h1>{props.title}</h1></body></html>);export default async (req, res) => {// サイズの設定const viewport = { width: 1200, height: 630 };// ブラウザインスタンスの生成const browser = await playwright.launchChromium();const page = await browser.newPage({ viewport });// HTMLの生成const props = { title: "Hello OGP!" };const markup = ReactDOM.renderToStaticMarkup(<Content {...props} />);const html = `<!doctype html>${markup}`;// HTMLをセットして、ページの読み込み完了を待つawait page.setContent(html, { waitUntil: "domcontentloaded" });// スクリーンショットを取得するconst image = await page.screenshot({ type: "png" });await browser.close();// Vercel Edge Networkのキャッシュを利用するための設定res.setHeader("Cache-Control", "s-maxage=31536000, stale-while-revalidate");// Content Type を設定res.setHeader("Content-Type", "image/png");// レスポンスを返すres.end(image);};
ページごとの meta
タグに OGP 画像を設定:
/* pages/posts/[id].js */import Head from "next/head";const headers = { "X-API-KEY": process.env.CMS_API_KEY };export const getStaticPaths = async () => {const response = await fetch(process.env.CMS_API_URL, { headers });const { contents: posts } = await response.json();return {paths: posts.map((post) => `/posts/${post.id}`),fallback: false,};};export async function getStaticProps({ params }) {const blogPostUrl = [process.env.CMS_API_URL, params.id].join("/");const response = await fetch(blogPostUrl, { headers });const { title } = await response.json();const baseUrl = {production: "https://tdkn.dev",development: "http://localhost:3000",}[process.env.NODE_ENV];return {props: {title,// OGP画像は絶対URLで記述する必要がありますogImageUrl: `${baseUrl}/api/ogp?title=${title}`,},};}export default function BlogPost(props) {return (<div><Head><title>{props.title}</title><meta property="og:image" content={props.ogImageUrl} /></Head>{/* ... */}</div>);}
ポイント
- サーバーサイドでの画像生成は処理コストが高いので、アクセスされるたびに実行するのは非効率です。 そこで Vercel Edge Cache を活用し、一定期間はキャッシュからレスポンスするようにしています。