React Native for WebでカスタムOGP
株式会社パルケの手を動かすCTO、みつるです。
パルケでは、ビジネスチャットツールのパルケトークを提供しています。
パルケトークで、プロフィールや一部の公開メッセージなどのURLをシェアした時にその内容に応じたカスタムOGPを表示したいという要件が発生しました。
パルケトークはExpoのReact Native製で、Web版はReact Native for Webで提供しています。
WebアプリはSPAとしてビルドされますので、そのままではページの内容に即したOGP情報を出力する事ができません。
そこで、先人たちの知見を参考にさせていただき、パルケトークでカスタムOGPを実現しました。
次のZennの記事は大変参考になりました。
実現方法の概要
vercel/ogで、表示する情報を組み合わせたOG画像を動的に生成します。
Cloudflare WorkersのKVにOGPに必要な情報を格納しておきます。
Cloudflare Workersで対象のルートのリクエストの場合に、KVから取得した情報を元にOGPのmetaタグの値を更新します。
各実装の説明
vercel/og
URLより、OG画像を自動で生成するEdge Functionを作成し、Vercelにデプロイします。
キャッシュが効くので、たくさんのリクエストが発生しても、Edge Functionの実行回数は抑えられるようです。
pages/api/og.tsx
import { ImageResponse } from '@vercel/og';
import { NextRequest } from 'next/server';
export const config = {
runtime: 'edge',
};
- vercel/ogライブラリのImageResponseを利用すると、JSXから画像を生成して戻せるようになります。
- runtimeは
edge
を指定します。
export default async function handler(req: NextRequest) {
const { searchParams } = req.nextUrl;
const title = searchParams.get('title');
const contents = searchParams.get('contents');
const photoUrl = searchParams.get('photoUrl');
// 途中省略
return new ImageResponse(
(
<div
style={{
background: '#009B9B',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
{/* 内容省略。OG情報を生成するためのコンテンツ */}
</div>
),
{
width: 1200,
height: 600,
}
);
}
- ハンドラーでは、searchParamsからOG画像生成に必要な情報を取得します。
- そのsearchParamsの情報を使って、JSXで出力する内容を作成し、ImageResponseで画像として返します。
- あとは、Vercelにデプロイすれば、URLが発行されてそのURLから呼び出せるようになります。
Cloudflare Workers KV
アプリでOG情報を表示したいコンテンツが作成、更新された時に、KVにその情報を送信して更新します。
先に、Cloudflareのコンソール上で、KVの名前空間を作成しておきます。
preview用とproduction用を作成しておくと運用しやすくて良いと思います。
REST APIでのKVの更新
KVの情報更新は、REST APIで更新するのが一番手っ取り早いと思います。
バックエンドサーバーで、対象のデータの追加・更新があった時に、REST APIを呼び出して変更を反映します。
const keyName = `/pr-profile/${id}`;
const value: OgpInfo = {
url: 'some url',
type: 'article',
title: 'some title',
description: 'some description',
image: 'image url' // vercel/ogで画像を生成するためのURL
};
const baseUrl = `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_NAME_ACCOUNT_ID}/storage/kv/namespaces/${process.env.CLOUDFLARE_KV_NAME_SPACE_ID}`
const url = `${baseUrl}/values/${encodeURIComponent(keyName)}`;
const body = JSON.stringify(value);
- KVのキー
keyName
は、OG情報を表示するURLのpathnameと同じにしておくと便利です。 - keyNameは、
encodeURIComponent
でencodeしておきます。 - KVの値
value
は、オブジェクトをJSON.stringify
で文字列化しておきます。 - baseUrlは、CloudflareのAPIのエンドポイントとなります。
const res = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiToken}`,
},
body,
})
- fetchのPUTでKVのキーバリューを更新します。キーが存在しなければ作成されて、存在すれば更新されます。
- 保持期間などをクエリパラメータで指定することもできるようです。
- Authrizationには、マイ プロフィールのAPIトークンで取得したTokenをセットします。
これで、カスタムOGPをセットする準備が整いました。
Cloudflare Workers
Cloudflare Workersを利用することで、戻り値のHTMLを強制的に書き換える事ができます。
この機能を使って、HTMLの<head>
タグの中の<meta>
タグのOG情報を更新します。
前提
Cloudflareに対象のサイトが登録されていて、プロキシされている事が前提となります。
プロキシされている事で、Cloudflare Workersで処理が挟めるようになります。
Routeの割り当て
Workersのトリガーのルートで、特定のサブドメイン、またはパスに該当する場合に起動するように指定できます。
これを使って、カスタムOGPを追加したいルートを絞り込んで、必要以上にWorkerが実行されるのを防ぐ事ができます。
Workerの実装
src/index.ts
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// まずはオリジナルのHTMLを取得
const response = await fetch(request);
const url = new URL(request.url);
// KVから、URLのpathnameをキーにOG情報を取得
const value = await env.PR_OGP_KV.get(url.pathname);
if (value) {
try {
// 文字列をオブジェクトに変換
const jsonParsed = JSON.parse(value);
// zodのスキーマを使ってバリデーション
const parsed = OgpInfoSchema.parse(jsonParsed);
// CloudflareのHTMLRewriterを使ってmetaタグを変換
const transformed = new HTMLRewriter()
.on('meta', new ElementHandler(parsed))
.transform(response);
return transformed;
} catch (e) {
// JSONのparseやzodのチェックでエラーになったらオリジナルをそのまま返す
return response;
}
} else {
// OG情報が取得できなければ、オリジナルをそのまま返す
return response;
}
},
};
- 先にオリジナルのHTMLを取得して、KVからOGP情報が取得できればmetaタグを置換します。
- HTMLの変換には、CloudflareのHTMLRewriterモジュールを使用します。
class ElementHandler {
og_url: string | undefined;
og_type: string | undefined;
og_title: string | undefined;
og_description: string | undefined;
og_image: string | undefined;
// SearchParamsからOGP情報を取得
constructor(parsed: OgpInfo) {
this.og_url = parsed.url;
this.og_type = parsed.type;
this.og_title = parsed.title;
this.og_description = parsed.description;
this.og_image = parsed.image;
}
// metaエレメントのOGのコンテンツをセットする
element(element: Element) {
// An incoming element, such as `div`
console.log(`Incoming element: ${element.tagName}`);
if (element.hasAttribute('property')) {
const property = element.getAttribute('property');
switch (property) {
case 'og:title':
this.og_title && element.setAttribute('content', this.og_title);
break;
case 'og:url':
this.og_url && element.setAttribute('content', this.og_url);
break;
case 'og:type':
this.og_type && element.setAttribute('content', this.og_type);
break;
case 'og:description':
this.og_description && element.setAttribute('content', this.og_description);
break;
case 'og:image':
this.og_image && element.setAttribute('content', this.og_image);
default:
break;
}
}
}
}
- metaタグを変換するElementHandlerのClassを定義して、HTMLRewriterに渡します。
これでHTMLのOGP情報が更新されるようになりました。
環境設定
wrangler.toml
main = "src/index.ts"
[env.staging]
name = "cloudflare-add-ogp-staging"
vars = { ENVIRONMENT = "staging" }
kv_namespaces = [
{ binding = "PR_OGP_KV", id = "xxx" }
]
[env.production]
name = "cloudflare-add-ogp-prod"
vars = { ENVIRONMENT = "production" }
kv_namespaces = [
{ binding = "PR_OGP_KV", id = "xxx" },
]
- ステージング環境と本番環境を別々でデプロイできるようにしておき、それぞれで利用するKVの指定しておきます。これで、それぞれの環境とKVをマッピングできます。
- 実装できたら、wranglerを使ってデプロイします
# ステージング環境のデプロイ
wrangler publish --env staging
# 本番環境のデプロイ
wrangler publish --env production
Cloudflare Workersはデプロイが早いのがいいですね。
React Native for Web
ベースとなるindex.html
に、OGPの初期値をセットしておきます。
web/index.html
<head>
<meta property="og:url" />
<meta property="og:type" />
<meta property="og:title" />
<meta property="og:description" />
<meta property="og:site_name" />
<meta property="og:image" />
</head>
まとめ
以上、 React Native for WebのようなSPAでも、Cloudflare Workersとvercel/ogを使って動的にOGPを表示する方法を記載しました。
React Native for Webに限らず、他のSPAでも応用可能かと思います。
ただ、複数のサービスにまたがっており、対応箇所も多いため、SSRに比べると遥かに複雑だと思います。
SEO対応が重視されるようなサービスであれば、最初からNext.jsやRemix.runのようなSSRのフレームワークを利用したいですね。
最後に
株式会社パルケでは、無料で誰でも簡単に使えるミーティングアプリ、チャットアプリを運用しています。
興味がありましたらぜひ利用してみてください。
無料でずっと話せるミーティングアプリ パルケミート
とにかく簡単につながる無料ビジネスチャット パルケトーク
また、React・React Nativeを使ったアプリ開発のご依頼も承っております。
お仕事のご相談は気軽にTwitterのDMからご相談ください。
Discussion