🎭

Vercel + Next.js + PlaywrightでOGP画像を自動生成する

2021/02/24に公開

Twitter や Facebook など、SNS 上で Web サイトをシェアしたときに表示される OGP 画像。
あらかじめ静的ファイルとしてサーバーに配置されることも多い一方で、画像をサーバー側で動的に生成することができれば、より自由度高く、視覚に訴える OGP 画像で PR することができます。

追記

OGP画像とは?

SNSなどでURLをシェアした際に表示される下記のような画像です。

処理の流れ

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>
  );
}

ポイント

  • 好きなデザインの画像を作れる。HTMLとCSSでコーディングしたものを画像としてレンダリングしているだけなので自由度が高いです。
  • サーバーサイドでの画像生成は処理コストが高く、アクセスされるたびに実行するのは非効率なので Vercel Edge Cache を活用し、一定期間はキャッシュからレスポンスするようにしている。

より高度な実例

手前味噌になりますが、個人サイト (https://tdkn.dev) でもOGP画像を使っており、画像の中でWebフォントを利用するための工夫があったりするので興味のあるかたは GitHub でソースコードを公開しているので覗いてみてください。

Discussion