WebサイトやWebアプリで、プレビューに表示した画像を、テキストと一緒にXへシェアできるようにしたいことはありませんか。
今回は、シェアボタンから画像とテキストをまとめて渡す方法を、JavaScriptのコード付きで紹介します。
Web Share APIのnavigator.share()に、画像のFileと本文を渡します。 生成した画像を端末に保存して選び直す手間を省き、OSの共有メニューへ渡せます。
共有メニューでXを選ぶと、対応する環境では画像と本文をXへ渡せます。Next.jsやSSRは不要で、ブラウザのJavaScriptだけで実装できます。
// pngBlobは作成済みのPNG。共有ボタンのクリック処理内で実行します。
const file = new File([pngBlob], 'result.png', {
type: 'image/png',
});
await navigator.share({
files: [file],
text: '12345点でした!\nhttps://yuua-drop.push.tokyo/',
});
これは共有処理の中心部分です。実際には画像を先に作り、画像付き共有への対応を確認してから呼び出します。
実装手順
1. 共有画像をPNGのBlobに変換する
CanvasやHTMLの画像化処理で、共有する画像を作ります。Canvasの場合は、描画を終えたcanvasからtoBlob()でPNGを取り出します。
// canvasは描画済みのCanvasです。
const pngBlob = await new Promise((resolve, reject) => {
canvas.toBlob(blob => {
if (blob) resolve(blob);
else reject(new Error('PNGの生成に失敗しました'));
}, 'image/png');
});
Blobは画像データ本体です。冒頭のコードのようにFileへ変換すると、ファイル名と形式を付けて共有できます。Canvasの画像書き出しの仕様
2. 共有ボタンを押す前に画像を作っておく
プレビューを開いた時点で画像を生成し、BlobやFileを保持しておきます。画像ができたらプレビューを表示して、共有ボタンを有効にします。
navigator.share()には、クリックやタップによる一時的な操作権限が必要です。画像生成や通信を長く待つと、その権限が切れる場合があります。共有ボタンでは、準備済みのFileを取り出してすぐに渡します。WebKitによる操作権限の解説
3. 画像付き共有への対応を確認する
const canShareImage =
typeof navigator.share === 'function' &&
typeof navigator.canShare === 'function' &&
navigator.canShare({ files: [file] });
navigator.shareがあっても、画像ファイルを共有できるとは限りません。実際に渡すFileを指定して、navigator.canShare()で確認します。web.devの実装例
対応していれば、共有ボタンのクリック処理から冒頭のnavigator.share()を呼びます。非対応の場合は、プレビュー画像の保存と、Xの投稿画面へのリンクを案内します。
Xの投稿リンクとの違い
| 方法 | 画像の渡し方 | ユーザーの操作 |
|---|---|---|
| Web Share API | 画像ファイルと本文をOSへ渡す | 共有メニューでXを選ぶ |
| XのWeb Intent | 本文とURLを渡す。画像の添付処理は含まない | 保存した画像を手動で添付する |
今回の方法は、Xの投稿リンクに画像添付用のパラメータを付けるものではありません。OSの共有機能を通じてアプリへ渡しています。URLのプレビューに表示されるカード画像と、投稿に添付する画像ファイルも別のものです。Web Intentによる本文とURLの共有
ゆうあどろっぷでの実装
ゆうあどろっぷでは、ゲームオーバー後の共有モーダルで次の処理を行っています。
- モーダルを開くと、1080×1350pxのCanvasに背景・キャラクター・スコアを描く。
- PNGのBlobを作り、メモリに保持する。
URL.createObjectURL()でプレビュー画像を表示する。- 「シェアする」を押したら、保持していたBlobをFileに変換して、本文と一緒に共有する。
HTMLの表示領域を撮影しているのではなく、共有画像をCanvasに描いています。「ピーク時の盤面」も、記録した座標・レベル・角度から描き直しています。完成画像をサーバーで生成・アップロードする処理は、この共有経路にはありません。

主な実装箇所はlib/share/shareCard.tsとcomponents/game/ResultTicket/SharePreviewModal.tsxです。モーダルで画像を作り、共有処理ではそのBlobを使い回しています。
Next.jsで使う場合
App Routerでは、共有UIを'use client'のClient Componentにします。Canvasの生成はuseEffectや操作イベント内で行い、navigator.share()は共有ボタンのクリックから呼びます。サーバーでの描画中やファイルの先頭では、documentやnavigatorにアクセスしないようにします。Next.jsのClient Componentの説明
動作確認と注意点
- HTTPSまたはlocalhostで動かします。スマートフォンからPCのLAN内IPへHTTPでアクセスする方法では、安全なコンテキストの条件を満たしません。
canShare()がtrueでも、Xが共有先に出ることや、画像と本文の両方を受け取ることは保証されません。対象の端末・ブラウザ・アプリで確認します。AbortErrorは、キャンセルや共有先がない場合に返ります。そのあとにダウンロードやX起動を自動で続けないようにします。share()の成功は、OSや共有先への受け渡しを意味します。Xへの投稿完了として扱わないようにします。
共有先ごとに受け取る項目や扱いは異なります。Web Share APIの仕様を確認し、非対応時の画像保存・手動添付も用意しておきます。
外部ドメインの画像をCanvasに描く場合は、配信元のCORS許可も必要です。許可がないと、toBlob()での書き出しがSecurityErrorになる場合があります。Canvasのセキュリティに関する仕様
以下のサンプルは、macOSのBraveでPNG生成、画像付き共有メニューの表示、共有を閉じた後の中止処理を確認しました。Xでの受け取りは環境によって異なるため、利用対象の実機でも確認してください。
動くサンプル
以下をshare-demo.htmlとして保存し、HTTPSまたはlocalhostの開発サーバーで開いてください。「プレビューを作る」→「画像とテキストを共有」の順に操作します。
HTML・JavaScriptのサンプル全体を表示
<!doctype html>
<html lang="ja">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>画像とテキストの共有サンプル</title>
<style>
body { font: 16px/1.7 system-ui; max-width: 640px; margin: 32px auto; padding: 16px; }
img { display: block; max-width: 100%; height: auto; margin: 16px 0; }
img[hidden], a[hidden] { display: none; }
button, a { margin: 8px 12px 8px 0; }
</style>
<h1>画像とテキストを共有する</h1>
<button id="prepare" type="button">プレビューを作る</button>
<img id="preview" alt="共有するスコア画像" hidden>
<button id="share" type="button" disabled>画像とテキストを共有</button>
<a id="save" download="result.png" hidden>画像を保存</a>
<a id="intent" target="_blank" rel="noopener noreferrer">Xの投稿画面を開く(本文のみ)</a>
<p id="status" role="status">先にプレビューを作ってください。</p>
<script type="module">
const prepare = document.querySelector('#prepare');
const preview = document.querySelector('#preview');
const share = document.querySelector('#share');
const save = document.querySelector('#save');
const status = document.querySelector('#status');
const pageUrl = 'https://yuua-drop.push.tokyo/';
const text = '12345点でした! #ゆうあどろっぷ';
const params = new URLSearchParams({ text, url: pageUrl });
document.querySelector('#intent').href = `https://x.com/intent/tweet?${params}`;
let file = null;
let objectUrl = null;
prepare.addEventListener('click', async () => {
prepare.disabled = share.disabled = true;
status.textContent = '画像を作成しています。';
try {
const canvas = document.createElement('canvas');
canvas.width = 1080;
canvas.height = 1350;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Canvas 2D unavailable');
ctx.fillStyle = '#27134f';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#fff';
ctx.font = 'bold 72px sans-serif';
ctx.fillText('SCORE 12345', 80, 220);
const blob = await new Promise((resolve, reject) => {
canvas.toBlob(b => b ? resolve(b) : reject(new Error('PNG failed')), 'image/png');
});
file = new File([blob], 'result.png', { type: 'image/png' });
if (objectUrl) URL.revokeObjectURL(objectUrl);
objectUrl = URL.createObjectURL(blob);
preview.src = save.href = objectUrl;
preview.hidden = save.hidden = false;
canvas.width = canvas.height = 0;
share.disabled = false;
status.textContent = '画像を確認して、共有ボタンを押してください。';
} catch (error) {
console.error(error);
status.textContent = '画像を作成できませんでした。もう一度お試しください。';
} finally {
prepare.disabled = false;
}
});
share.addEventListener('click', async () => {
if (!file) return;
share.disabled = prepare.disabled = true;
try {
if (!navigator.share || !navigator.canShare?.({ files: [file] })) {
status.textContent = '画像付き共有に対応していません。画像を保存して、投稿に添付してください。';
return;
}
// このクリック処理では、share()の前に画像生成や通信をしない。
await navigator.share({ files: [file], text: `${text}\n${pageUrl}` });
status.textContent = '共有先で投稿内容を確認してください。';
} catch (error) {
if (error.name === 'AbortError') {
status.textContent = '共有が中止されたか、共有先がありませんでした。';
return;
}
console.error(error);
status.textContent = '共有できませんでした。画像を保存して、投稿に添付できます。';
} finally {
share.disabled = prepare.disabled = false;
}
});
window.addEventListener('pagehide', () => {
if (objectUrl) URL.revokeObjectURL(objectUrl);
});
window.addEventListener('pageshow', event => {
if (event.persisted && file) {
objectUrl = URL.createObjectURL(file);
preview.src = save.href = objectUrl;
}
});
</script>
</html>
この方法を使っているゆうあどろっぷは、キャラクターを落として合体・進化させる、個人制作のブラウザパズルゲームです。非公式ファンゲームとして公開しています。ゲームオーバー後の共有モーダルで、実際の動きを確認できます。

