🤮

Playwrightを使用してOGP画像を生成する際に詰まった話

2021/12/16に公開

Playwrightを使用したOGP画像の生成は、よく使われているcanvasを使用した方法よりも、個人的にかなり開発体験が良く、おすすめの手法です。
ですが、現状Playwrightを使用したOGP画像の生成の情報は少なく開発が難航したので、その際に詰まった部分とどう解決したのかをまとめました!

環境

Next.jsのプロジェクトをOGP画像を生成するOGPサーバーとしてvercelにデプロイして使用しています。
その環境以外では試していないので、動作は異なる可能性があります。

"next": "12.0.7",
"playwright-aws-lambda": "^0.7.0",
"playwright-core": "^1.17.1",

Playwrightとは

https://playwright.dev/

PlayWright とはnode.jsでChrome、Safari、Firefoxなどのブラウザを操作するためのライブラリです。同じようなライブラリではPuppeteerがあります。
PlayWrightは主にE2Eテストで使用されることの多いようですが、今回はPlayWrightのスクリーンショット機能を使ってOGP用の画像を生成します。

PlaywrightでOGP画像を生成する方法

ざっくりと流れとしては、

  1. OGP画像にしたいcomponentを作成する
  2. そのcomponentをReactDOMServer.renderToStaticMarkupでhtmlに変換する
  3. そのhtmlをヘッドレスブラウザにセットする
  4. ブラウザのスクリーンショットを取る

これような流れで、PlaywrightでOGP画像を生成することが出来ます。
詳しい方法は、以下の記事に書いてあるので、ぜひご参照ください。開発の際にはとても参考になりました!
https://zenn.dev/tdkn/articles/c52a0cc7bea561

Playwrightを使用して詰まったところ

画像の読み込み

component内の画像の部分が生成されたOGP用の画像には表示されないことがありました。
これは簡単なことで、スクリーンショットを取るタイミングが画像が読み込まれる前に行われていることが原因のようでした!

具体的には、コードが以下のようになっていました。

await page.setContent(html, { waitUntil: "domcontentloaded" });

setContentは、ページにhtmlをレンダリングするメソッドです。
https://playwright.dev/docs/api/class-page#page-set-content

setContentの引数のwaitUntilは、setContentの操作が終了したとみなすタイミングを調整します。最初に設定していたdomcontentloadedの場合は、DOMContentLoadedイベントが発生した時点で、操作が終了したものとみなします。

DOMContentLoadedイベントはブラウザがHTMLを完全に読み込んだ際に発生するイベントで、画像のような外部リソースはまだ読み込まれていない可能性があるので、今回のOGP画像の生成の用途には適していません。

代わりに以下のコードに修正しました。

await page.setContent(html, { waitUntil: "load" });

loadイベントはブラウザがすべてのリソースを読み込んだ際に発生するイベントなので、これで画像が表示されるようになりました!!

日本語対応

ローカルで開発してるときは気づかないのですが、vercelにデプロイしたら日本語の部分が表示されていませんでした。
これは、vercelが内部で使用しているAWS Lambdaの実行環境が日本語に対応していないことが原因のようです。
解決策としては日本語を表示するための日本語フォントがないことが問題なので、日本語フォントを入れることです。

具体的には、以下のコードのようにcssの@importを使用してwebfontを読み込みます。

const css = `
  @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap');
  html,
  body {
    margin: 0;
    padding: 0;
  }
 ...省略
  `;

return (
    <html>
      <style dangerouslySetInnerHTML={{ __html: css }} />
      <body>
       ...省略
      </body>
    </html>
  );

webfontではなくダウンロードしたフォントファイルを使用してもいいのですが、一つ大きな問題があります。
それは、vercelのServerlessFunctionの最大サイズが50MBなので、日本語フォントのようなサイズの大きいファイルは制限を超えてしまう可能性があることです。webfontは読み込みに時間がかかるので、サイズが超えないのであればローカルにフォントファイルを設置するのもありだと思います。

絵文字対応

日本語と同じく絵文字も対応する必要がありました。
結論としては日本語の時と同じように解決したのですが、試行錯誤したのでその過程も共有します。

日本語の時のように絵文字もフォントを入れて解決しようと思い、絵文字フォントを探しました。中々使えそうなフォントはなかったですが、noto-emojiを発見しましたので、このフォントファイルをダウンロードして設置しました。
https://github.com/googlefonts/noto-emoji

が、先ほど書いたように絵文字フォントファイルはサイズが大きかったので、vercelの制限を超えてしまいました、、、。

その後、使用しているライブラリのplaywright-aws-lambdaのReadMeを読んでみると、

Loading additional fonts
If you need custom font support by e.g. emojicons in your browser, you have to load it by using the loadFont(url: string) function before you launch the browser.

await loadFont(
  'https://raw.githack.com/googlei18n/noto-emoji/master/fonts/NotoColorEmoji.ttf'
);

https://github.com/JupiterOne/playwright-aws-lambda

と記載されており、これだっ!!と思い、使用してみたのですが以下のようなエラーがでて結局使いませんでした。解決策が分かる方がおられましたら、ぜひ教えてください!!

unhandledRejection: Error: ENOENT: no such file or directory, mkdir 'C:\tmp\fonts'

上記のコードの中でhttps://raw.githack.com/googlei18n/noto-emoji/master/fonts/NotoColorEmoji.ttfが使われており、これをcssから読み込めばいいと気づき、以下のように記述することで絵文字を表示することに成功しました!!

@font-face {
    font-family: 'NotoColorEmoji';
    src: url('https://raw.githack.com/googlei18n/noto-emoji/master/fonts/NotoColorEmoji.ttf') format('truetype');
    }
.wrapper {
    font-family: 'Noto Sans JP', 'NotoColorEmoji', sans-serif;
  }

これにて、長きに渡ったOGP生成との激闘は幕を閉じました。

まとめ

情報が少なく詰まりどころが多かったけど、Playwrightを使用してのOGP画像の生成は最高!!!!

Discussion