vercel/og-imageをNetlify Functionsで動かせるように改造する
vercel/og-imageについて
Vercelが公開しているOG画像生成用のServerless Functionの例。
当然Vercelにデプロイする前提で作られているので、そのままではNetlifyにデプロイできません。
なんでNetlify Functionsにするかと言うと、Hobbyプランだと組織のリポジトリを使うことができず不便だからです。
メインコントリビューターのSteven氏が「Netlify Functionsに書き換える必要あるけど、試してないわ」と言ってそのままCloseされているissue。
ということで試してみましょう。
リファクタリング
netlify.tomlを用意
[build]
command = "yarn build"
publish = "public"
[functions]
directory = "api"
external_node_modules = ["chrome-aws-lambda"]
chrome-aws-lambda
を外部モジュールとして指定する必要があります。
また、Netlify FunctionsはTypeScriptをサポートしているため、api/dist
を指定する必要はありません。
esbuild: method that leverages esbuild to bundle functions, resulting in shorter bundling times and smaller artifacts. Currently available as an opt-in beta for JavaScript functions. TypeScript functions always use esbuild.
デプロイが割と速いなと思ったら、esbuildが使われているそうです。
ローカルでデバッグできるように
npm i -g netlify-cli
netlify login
Netlify CLIを使います。
"scripts": {
- "build": "tsc -p api/tsconfig.json && tsc -p web/tsconfig.json"
+ "build": "tsc -p api/tsconfig.json --noEmit && tsc -p web/tsconfig.json",
+ "dev": "yarn build && netlify dev --port 3003",
+ "dev:html": "OG_HTML_DEBUG=1 yarn dev"
},
API部分のビルドは--noEmit
で型チェックだけにします。 先述した通り向こうがesbuildを使ってくれるからです。
echo .netlify >> .gitignore
.netlify/functions-serve/index
ができるため除外します。
追加パッケージを用意してindex.tsを書き換え
yarn add -D @netlify/functions
-import { IncomingMessage, ServerResponse } from 'http';
import { parseRequest } from './_lib/parser';
import { getScreenshot } from './_lib/chromium';
import { getHtml } from './_lib/template';
+import { Handler } from '@netlify/functions';
+import { Buffer } from 'buffer';
-const isDev = !process.env.AWS_REGION;
const isHtmlDebug = process.env.OG_HTML_DEBUG === '1';
-export default async function handler(req: IncomingMessage, res: ServerResponse) {
+export const handler: Handler = async (event, ctx) => {
+ const isDev = !ctx.clientContext?.custom?.netlify;
try {
- const parsedReq = parseRequest(req);
+ const parsedReq = parseRequest(event);
const html = getHtml(parsedReq, isHtmlDebug);
if (isHtmlDebug) {
- res.setHeader('Content-Type', 'text/html');
- res.end(html);
- return;
+ return {
+ statusCode: 200,
+ headers: {
+ 'Content-Type': 'text/html',
+ 'Cache-Control': '',
+ },
+ body: html,
+ };
}
const { fileType } = parsedReq;
const file = await getScreenshot(html, fileType, isDev);
- res.statusCode = 200;
- res.setHeader('Content-Type', `image/${fileType}`);
- res.setHeader('Cache-Control', `public, immutable, no-transform, s-maxage=31536000, max-age=31536000`);
- res.end(file);
+ return {
+ statusCode: 200,
+ headers: {
+ 'Content-Type': `image/${fileType}`,
+ 'Cache-Control': `public, immutable, no-transform, s-maxage=31536000, max-age=31536000`,
+ },
+ body: Buffer.from(file).toString('base64'),
+ isBase64Encoded: true,
+ };
} catch (e) {
- res.statusCode = 500;
- res.setHeader('Content-Type', 'text/html');
- res.end('<h1>Internal Error</h1><p>Sorry, there was a problem</p>');
+ return {
+ statusCode: 500,
+ headers: {
+ 'Content-Type': 'text/html',
+ 'Cache-Control': '',
+ },
+ body: '<h1>Internal Error</h1><p>Sorry, there was a problem</p>',
+ };
console.error(e);
}
};
AWS_REGION
でプロダクション判定していましたが、Netlify CLIがLambdaをエミュレートする際にAWS CLIのリージョン設定を拾うらしく、私はap-northeast-1
が勝手に入ってしまいました。
Netlify側が設定する環境変数はこちらで解説されているんですが、LambdaではNODE_ENV
すら定義されていないらしく、ctx.clientContext?.custom?.netlify
を使うことでやっとこさ本番の判定ができました。
パーサー部分の書き換え
-import { IncomingMessage } from 'http';
+import { Event } from '@netlify/functions/dist/function/event';
import { parse } from 'url';
import { ParsedRequest, Theme } from './types';
-export function parseRequest(req: IncomingMessage) {
+export function parseRequest(req: Event) {
console.log('HTTP ' + req.url);
- const { pathname, query } = parse(req.url || '/', true);
+ const { pathname, query } = parse(req.path.replace('/.netlify/functions/index', '') || '/', true);
/.netlify/functions/index/
が付いてしまったので応急処置。あと、NetlifyのHandler
に合わせた型定義に変えています。
web部分の書き換え
-url.pathname = `${encodeURIComponent(text)}.${fileType}`;
+url.pathname = `/.netlify/functions/index/${encodeURIComponent(text)}.${fileType}`;
これも応急処置。Web部分は実使用だと使わないかも。私はまるごと消しています。
動作確認
3:33:12 AM: ────────────────────────────────────────────────────────────────
3:33:12 AM: 1. build.command from netlify.toml
3:33:12 AM: ────────────────────────────────────────────────────────────────
3:33:12 AM:
3:33:12 AM: $ yarn build
3:33:12 AM: yarn run v1.22.10
3:33:12 AM: $ tsc -p api/tsconfig.json --noEmit
3:33:18 AM: Done in 6.16s.
3:33:18 AM:
3:33:18 AM: (build.command completed in 6.4s)
3:33:18 AM:
3:33:18 AM: ────────────────────────────────────────────────────────────────
3:33:18 AM: 2. Functions bundling
3:33:18 AM: ────────────────────────────────────────────────────────────────
3:33:18 AM:
3:33:18 AM: Packaging Functions from api directory:
3:33:18 AM: - index.ts
3:33:18 AM:
3:33:24 AM:
3:33:24 AM: (Functions bundling completed in 5.8s)
3:33:24 AM:
3:33:24 AM: ────────────────────────────────────────────────────────────────
3:33:24 AM: 3. Deploy site
3:33:24 AM: ────────────────────────────────────────────────────────────────
3:33:24 AM:
3:33:24 AM: Starting to deploy site from 'og-image/public'
3:33:24 AM: Creating deploy tree
3:33:24 AM: Creating deploy upload records
3:33:24 AM: 0 new files to upload
3:33:24 AM: 1 new functions to upload
3:33:40 AM: Site deploy was successfully initiated
3:33:40 AM:
3:33:40 AM: (Deploy site completed in 16.3s)
3:33:40 AM:
Webを消した場合のログはこんな感じになります。
環境 | レスポンス |
---|---|
Netlify | ![]() |
Vercel | ![]() |
Netlifyの方はcontent-length
が追加されないようですね。
strict-transport-security
の設定にはnetlify.tomlの編集が必要らしいですが、長くなるので割愛。
注意
リクエスト回数の制限が違う
2022年2月10日現在、Netlify FunctionsのStarter(無料)におけるリクエスト回数上限は125k per site /month ($25+ when exceeded)です。
で、Vercelはプランに関係なく「Unlimited Serverless Function Invocations」らしいです。太っ腹ですね。
URLが長くなってしまう
Vercelにはなかった/.netlify/functions/index
が付いてこうなるのですが、何かしら経由して短縮させるといった方法を取らないと使いづらいですね。
参考
Discussion