React でアプリ開発をしている中で、URLを共有したときのプレビューをちゃんと出したくなりました。
今回のアプリでは、グループURLを LINE などで共有する想定があります。
URLを共有したときに、タイトルや画像がそれっぽく出てほしい。
そこで、まずは固定の OGP を index.html に入れました。
この記事では、SPA の OGP をどう考えたか、今の暫定対応と将来の動的対応をまとめます。
React Router と Hono API で URL 共有型の投稿アプリを作る基本部分は別記事で扱っているため、ここでは共有プレビューと画像の置き場所の話に絞ります。
SPA と OGP の相性問題
React SPA は、基本的に index.html を1つ返して、ブラウザ上でルーティングします。
つまり、以下のURLがあっても、サーバーから返るHTMLは同じです。
/g/example-name
/g/another-group
/login
ブラウザで見る分には React Router が画面を切り替えてくれます。
しかし、SNS や LINE のプレビュー bot は、JavaScript の実行結果ではなく、最初に返ってきたHTMLの meta タグを見ることが多いです。
そのため、SPA で URL ごとに違う OGP を出したい場合は注意が必要です。
今回まずやったこと
今回はまだ固定グループが中心なので、まず index.html に固定 OGP を入れました。
実際のコードです。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>同級生近況ノート</title>
<meta
name="description"
content="久しぶりに会う前に、今どこで何をしているかをゆるく共有する場所です。"
/>
<meta property="og:type" content="website" />
<meta
property="og:url"
content="https://kinkyo-note.workers.dev/g/group-name"
/>
<meta property="og:title" content="同級生近況ノート" />
<meta
property="og:description"
content="久しぶりに会う前に、今どこで何をしているかをゆるく共有する場所です。"
/>
<meta
property="og:image"
content="https://kinkyo-note.workers.dev/g/group-name.jpg"
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="同級生近況ノート" />
<meta
name="twitter:description"
content="久しぶりに会う前に、今どこで何をしているかをゆるく共有する場所です。"
/>
<meta
name="twitter:image"
content="https://kinkyo-note.workers.dev/g/group-name.jpg"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
これで、group-name を共有したときに以下が出る想定です。
- タイトル:
同級生近況ノート - 説明文:
久しぶりに会う前に... - 画像:
/group-name.jpg
固定 OGP のメリット
固定 OGP のメリットは、とにかく簡単なことです。
- Vite の
index.htmlに書くだけ - 追加のAPI実装がいらない
- 画像も
public/group-name.jpgに置けば返せる - まず共有時の見た目を整えられる
今回のように、最初の対象グループが固定なら十分です。
固定 OGP の問題
ただし、この方法には明確な問題があります。
SPA 全体で同じ index.html を返すため、別のグループでも同じ OGP が出ます。
たとえば将来こういうURLが増えたとします。
/g/example2-name
/g/tokyo-members
/g/company-alumni
index.html に固定で書いている限り、どのURLを共有しても同じ内容になります。
title: 同級生近況ノート
image: group-name.jpg
これは、複数グループ対応を本番機能として出すなら困ります。
なぜ今回は固定でよしとしたか
今回の段階では、以下の判断をしました。
- まだグループ作成機能は本格実装前
- 共有したい主要URLは
/g/group-name - まず LINE などで共有したときの見た目を整えたい
- 動的OGPはあとで Workers 側に寄せられる
将来は Workers で動的に差し替える
複数グループに対応するなら、最終的には Workers 側で HTML を返すときに OGP を差し替えるのが自然です。
イメージはこうです。
GET /g/:slug
-> D1 から group を取得
-> index.html を読み込む
-> og:title / og:description / og:image を group ごとに差し替える
-> HTML を返す
たとえば、概念的にはこういう処理です。
app.get('/g/:slug', async (c) => {
const slug = c.req.param('slug')
const group = await findGroupBySlug(slug)
const html = await getAssetIndexHtml(c)
return c.html(
html
.replace('__OG_TITLE__', escapeHtml(group.name))
.replace('__OG_DESCRIPTION__', escapeHtml(group.description))
.replace('__OG_IMAGE__', group.ogImageUrl),
)
})
この方式なら、React Router を使った SPA のままでも、bot に返すHTMLは URL ごとに変えられます。
動的OGPにするなら画像の置き場所も考える
今回、グループ画像は public 配下に置いています。
固定画像ならこれで問題ありません。
ただ、将来ユーザーがグループを作ったり、投稿者アイコンをアップロードしたりするなら、画像をどこかに永続保存する必要があります。
候補としては Cloudflare R2 です。
R2
- group icon
- user avatar
- future uploaded images
注意点として、ブラウザの localStorage に保存した画像は、他人の端末から見えません。
つまり、OGP画像や共有されるアイコンに使うには不向きです。
共有される画像は、bot や他ユーザーがアクセスできるURLに置く必要があります。
まとめ
React SPA の OGP は、最初に返る index.html に依存します。
今回の判断はこうです。
- 固定画像なら
index.htmlに OGP を直書きでよい - 将来は Workers で
/g/:slugごとに HTML を差し替える - 共有される画像は
localStorageではなく、R2 など外部から見える場所に置く