PROMARI JOURNAL

iframe から、丸型の共有ボタンと「いいね」へ。Promari SNS Share を公開しました。

作成: / 公開: / 内容更新:

執筆:Takaomi Murasaki / 掲載・運営:プロマリ

丸い共有ボタンの設計を、用語のポップアップと身近なたとえでたどります。表示・保存・計測・配信の内容を省略せず、四層の責任と設計判断を詳しく解説。全5ページ、連載予定は16章。

PASS IT ON

ひとつの発見を、次の会話へ。

PHPとJavaScript、同じ定義を二度直しますか

TOMLは設定を表す形式です。配列や節を使ってサービスの並び、表示、設置場所、補助チャネル、計測の契約をまとめます。PHPのサービス定義からロゴやURLの情報を取り出し、ブラウザ側の生成物へ渡すことで、PHPとTypeScriptの両方で、同じ定義を手作業で直す量を減らしています。片方だけ直して、もう片方を忘れる。そんな行き違いを減らしたいわけです。生成処理もプログラムなので、元の記述形式が変われば抽出できなくなる可能性があります。生成できたことと、正しく生成されたことは別に検証します。

設定を直したら、次は書き出しです。–check正本と生成物が一致するかを確かめます。生成物だけを直すと、次の生成で元に戻ってしまいます。反対に正本だけを直すと、読者へ届くコードは古いまま。どちらも画面を眺めるだけでは見落としそうですよね。そこで、レビューでは両方の差分を見ます。自動生成に任せる部分と、私たちが確かめる部分を決めておくわけです。

設定をTOMLへ寄せたのは、PHPとJavaScriptで共有先の順番が食い違うのを避けたかったからです。実行環境ごとに必要な形式は違っても、変更の入口は一つにする。生成物も配布に必要ですが、手では直さない。生成器の変更と設定の変更を同じ差分で追えるようにしておくと、どこから差が生まれたかを確認できます。 設計判断:設定の正本を一つにする「設定は合っているのに画面が古い」というときも、正本・生成・配信のどこで止まったかを順に見ればよくなります。

設定生成はビルド時、生成物の利用は実行時です。ここを分けたぶん、読者のブラウザへPHPの定義を解析する処理は持ち込みません。一方で、正本だけ更新して生成を忘れると、正常に動く古いコードが配られます。クラッシュしないぶん見落としやすいんですよね。だから生成器の成功だけではなく、再生成した結果とコミット済みの成果物の一致まで確認します。--checkは、その取り違えをレビュー前に見つけるための入口です。

設定にも型があります。trueと文字列の”true”、数値と文字列、配列と単一の値は同じではありません。知らないキーを黙って無視すると、書いた人は効いたつもりになりがちです。生成時に許可する項目と値の範囲を確かめることは、ブラウザで試す前に誤りを見つける入口になります。エラーメッセージも「設定が変」だけでなく、どの項目が期待と違うか分かると修正しやすいですね。

ちなみに、設定をいくつか重ねて使うなら、どちらを優先するかも決めておきたいところです。配列を丸ごと置き換えるのか、項目ごとに混ぜるのか。Deep Mergeという言葉を使っていても、その規則は実装ごとに違い得ます。とくに共有先の順番は意味を持つので、勝手に足し合わせてよいとは限りません。同じ入力から同じ出力ができるか、二回生成して差分が出ないかも確かめます。

設定が壊れた朝、画面はどう振る舞うべきか

fail-closedを考えるときは、何を止めるかまで決めます。設定の検証が失敗したら生成を止める、APIで認可できなければ変更を拒否する。一方、共有機能に問題があっても記事本文は読めるようにする。すべてを同じエラー処理へ寄せるより、守る条件ごとに、失敗の影響を閉じるほうが扱いやすいんです。

設定のコメントについても、触れておきます。半年後に article_top = false だけを見たら、「冒頭には出したくないのかな」と思いませんか。ところが今回の理由は逆で、冒頭にはテーマから出すので、自動挿入だけ止めたいのです。同じfalseでも、理由を知らないと受け取り方が変わってしまいますよね。私は、この判断をコメントに残しておきたいと思っています。使える設定と、このサイトで選んだ設定を分けておけば、他のサイトへ持っていくときにも考え直せます。

いいねの初期取得は、実装上の制約も残るところです。初期化中は操作を無効にしますが、取得に失敗した場合、現在の接続コードは0の仮表示とエラー案内を出し、ボタンから再試行できる状態に戻します。再操作では取得を先に試しますが、この0はサーバーで確定した0件ではありません。未取得を「—」などで別表示にする案は、連載で比較したい改善点です。実装済みの挙動と、目指したい表示を分けて扱います。

これは、失敗を全部無視することとも違います。画面では本文を読めるようにし、運用側では共有部品が読み込めなかったことを調べられるようにする。利用者向けの案内と、開発者が原因を探す情報では、必要な粒度が違います。エラーの詳細をそのまま画面へ大量に出すより、読者には次にできる行動を伝え、調査の情報は適した場所へ残します。

コメントの役割も、ここで効いてきます。false を「無効」と説明するだけなら、コードから読めます。でも「テーマが冒頭へ置くため、自動挿入は無効」と残せば、後の人は理由を保ったまま変更できます。残したいのは、コードの日本語訳より、その判断を変えてよい条件です。

少し先のことも想像してみましょう。半年後、この設定を見直すとしたら、何がきっかけになるでしょうか。テーマからの呼び出しをやめた、固定バーを導入した、記事の種類が増えた。そうした条件を考えておくと、設定が長く残っても「昔からこうだから」で維持せずに済みます。設定は一度決めて終わりではなく、運用の判断を記録する場所でもあります。

いいねを1回押したのに、数字が揃わない?

「押した」と「保存できた」を分けて考える

さて、次はハートのボタンです。押すと数字が一つ増える、見慣れた「いいね」ですね。ただ、画面で数字を増やすことと、その結果を保存できることは別なんです。まず、いいねを使うときは like 属性を付けます。ただし、属性を付けただけでは保存先は作られません。部品の初期状態は操作無効で、サイト側がsetLikeState()へ渡すbusyで操作可否を制御します。初期取得に失敗した後の再試行も、接続コード側の担当です。操作すると promari-sns-share-like イベントが発火し、detail.liked に希望する状態が入ります。保存後に setLikeState() へ確定値を返すのが連携の契約です。

JavaScriptいいねの連携API(初期状態と確定状態の反映例)
await customElements.whenDefined('promari-sns-share');
const element = document.querySelector('promari-sns-share[like]');

// サイトの保存APIから取得した値を渡す。以下の値は説明用。
element.setLikeState({ liked: false, count: 0, busy: false });

// 保存要求はこのイベントで受け取る。
element.addEventListener('promari-sns-share-like', (event) => {
  const requested = event.detail.liked;
  // サイト側で requested を保存し、成功・失敗の結果を setLikeState() に返す。
  // この抜粋はAPIの契約を示すもので、保存処理そのものは含まない。
});

当サイトでは、共有部品の外側にあるWordPress側のAPIが保存を担当します。現在の実装は、ログイン中ならサーバーが確定した会員ID、ゲストなら署名付きCookieに基づく匿名識別子と記事IDを使います。クライアントから送られたユーザーIDで所有者を決めることはしません。冒頭と末尾は同じ確定値を表示し、再読み込みでも取り直します。会員とゲストで識別の範囲が違うため、件数をそのまま実人数とは読み替えないことも、表示と運用の契約に含めています。

さて、ここからは「いいね」の中を見ていきます。たとえば冒頭で押したら「1」になったのに、読み終えたところでは「0」のまま。そんな画面では、もう一度押したくなりますよね。これは今回の設計で避けたい場面の一つです。操作前の状態、読者が希望した状態、サーバーが保存した確定状態を分けて扱います。単に数字を1増やすのではなく、保存先へ希望するlikedの状態を渡し、返ってきた確定値を表示する。先に画面を変える場合も、失敗したら確定状態へ戻す約束が必要になります。

同じ記事に冒頭と末尾の2つの要素がある場合、それぞれが独立して件数を増減させると表示が食い違います。同じ確定値を両方へ渡し、保存中の操作も調整します。通信失敗を0件と解釈して上書きしないこと、遅れて返った古い応答で新しい状態を戻さないことも、非同期処理を設計するときに検討する条件です。連載では成功する一本道だけでなく、応答順序と再試行も分けて扱います。

非同期処理で厄介なのは、要求順と応答順が一致しないことです。trueを保存した後にfalseを保存しても、最初の応答が遅れて届く可能性があります。ボタンを無効化するだけで、別タブや別端末の操作まで直列化できるわけではありません。今回のUIで調整する範囲と、保存側で守る範囲を分けて考えます。連載では、リクエストの世代管理で古い応答を捨てる方法と、サーバー上の更新順序をどう扱うかを別々に掘り下げます。

状態をいくつかのbooleanで持つと、loading=trueなのに操作可能、といった組み合わせが生まれます。連載では、現状の実装を起点に状態遷移を整理します。未取得を確定件数の0と別表示にする、保存中なら同じ操作欄の再送を抑える、失敗なら直前の確定状態を維持する。判定をイベントハンドラーごとに書くより、許す遷移を明確にしたほうがテストしやすいんです。型で不正な組み合わせを減らす案も、ここで検討できます。

楽観的更新は、返事を待つ前に画面へ希望する状態を見せる方法です。反応が速く感じられる一方で、失敗したときに戻す処理や、古い応答をどう扱うかが必要になります。サーバーの確定後に表示する方法にも、待ち時間を伝える工夫が要ります。どちらが常に正しいというより、操作の性質と、失敗した場合の負担に合わせて選びます。

記事の上では3件なのに、下まで読んだら2件。これでは、どちらが正しいのか迷ってしまいますよね。今回のように冒頭と末尾へ置くなら、件数をそれぞれ別に決めないようにします。サーバーが返した同じ状態を両方へ配る。保存中の操作も記事単位で調整する。画面の下までスクロールしたとき、初めて食い違いに気付くようでは困りますよね。テストでは、片方を操作してもう片方を見るだけでなく、再読み込みした後にも同じ結果を取得できるか確かめます。

同じリクエストが二度届いても、1件にできるか

ここで少し、「同じ人のいいね」をどう見分けるのかにも触れておきましょう。その目印がいつまで残るかによって、件数の意味も変わってきます。会員IDなら同じアカウントとして別端末から扱えますが、ゲストCookieを消せば同じ人だとは判断できません。ログインしたからといって、過去の匿名履歴を全部その会員へ推測で結び付けるのも避けています。現在の連携では、明示的ないいね操作で同じブラウザの票を整理する場面と、過去の操作履歴を保持する場面を分けています。現在の票と、誰として操作した履歴かは、同じデータではないんです。

冪等性は、同じ要求を繰り返しても結果が余分に変化しない性質です。同じブラウザーと記事に対してliked=trueを再送しても、同じ1件として保存される必要があります。同時要求に対しては保存側の一意性や更新方法も関係し、フロントのボタンを無効にするだけでは保証できません。入力の型、許可する記事、トークン、保存、応答、キャッシュまでをAPIの一つの契約として考えます。

APIへ送るのは「反転して」ではなくliked=trueliked=falseという希望状態です。応答を受け取れず再送しても、同じ状態へ落ち着く契約にしたいからです。ただし、この形にしただけでは競合は消えません。存在確認と追加の間へ別の要求が入れば、実装次第で重複します。リクエストの意味と、保存処理の排他制御を揃えて初めて再送に耐えられる。ここはフロントだけ眺めていても分からないところです。

現在の保存処理では、記事と所有者から作るキーを基準にロックを取り、状態の変更と操作履歴の追加をトランザクションでまとめています。同じ希望状態の再送では、操作履歴も増やしません。ゲストの票を会員の票へ整理する場合も、単純に足して二票へしない。もちろん、ロックには待ち時間や競合時の失敗があるので、取れなかったときまで成功扱いにはしません。このロックが揃えるのは同じ保存キーへの処理で、ゲストと会員のようにキーが変わる要求まで一括で直列化するものではありません。トランザクションが効くストレージエンジンを使うことも前提です。連載では、この境界をまたぐ競合も検証対象にします。

Cookieの属性は、目的を分けて見ます。HttpOnlyはJavaScriptからの直接読み取りを制限し、SecureはHTTPSでの送信、SameSiteは別サイトからの要求にCookieを添える条件に関わります。これらを付けても、所有者の識別と認可が自動で完成するわけではありません。署名の検証、リクエスト元の確認、対象記事の公開条件、レート制限はそれぞれ別の担当です。Cookieを持っていることだけで任意の記事への操作を許さないようにします。

保存で一つにしたいのは、「票が変わった」という事実と、その操作履歴です。途中で失敗して票だけ増えたり、履歴だけ残ったりすると、後の集計が説明できません。同じ所有者と記事を更新する要求の順番をそろえ、関連する変更を一緒に確定し、失敗したら一緒に取り消す。その境界までが保存の設計です。

WordPressのnonceは使い捨ての認証証明ではなく、一定期間内のリクエスト検証に使う値です。再送防止や認可の代わりにはなりません。現在の共通APIでは、署名Cookieの読者トークンをnonceのactionへ含め、ログイン状態はWordPress側で確認します。Origin、対象記事の公開状態、入力の型、停止中アカウントかどうかも別々に検査します。トークンの検証が通ることと、その操作を許してよいことを混同しないようにします。

考えてみる:タイムアウトしたら、もう一度「+1」でよい?

タイムアウトだけでは、保存に失敗したかどうか分かりません。サーバーでは保存できていて、応答だけが届かなかった可能性もあります。そのまま「+1」を再送すれば2件になるかもしれません。同じブラウザー・同じ記事の「liked=true」を何度受けても同じ結果にする設計が、ここで効きます。これが冪等性を必要とする具体的な理由です。 設計ポイント:再送しても増えすぎない保存

読み取りと変更は、APIの入口でも分けています。GETで状態を取得しただけでは、過去の匿名票を会員へ付け替えません。変更はPOSTで明示してもらう。返すデータはlikedと件数などの確定状態で、DOMの操作方法は含めません。これなら保存先を変えるときも、接続コードが同じ契約へ変換すれば表示部品を保てます。HTTPメソッドを分けるだけでなく、読み取りで何を変更しないかまで決めておくのがポイントです。

HTTPステータスは、その返事をどう受け取るかの手掛かりです。存在しない記事、許可できない要求、サーバー側の障害は、同じ「0件」ではありません。fetchは、HTTPのエラー応答でもPromise自体が解決する場合があるため、通信が返ったことと正常な状態コードだったことを分けて確認します。さらにJSONとして読めるか、項目が契約どおりかを確かめます。

キャッシュにも気を配ります。全員に同じ記事本文を配るキャッシュと、ブラウザーごとにlikedが違う応答では条件が異なります。ある方の状態を他の方へ返してしまえば、件数が合っていても誤った表示です。Cache-Controlなどの応答ヘッダーを見て、途中の仕組みが何を保存してよいかを決めます。速くする工夫は、誰にとって同じデータなのかを確認してから加えたいですね。

追跡用の記録では、現在のいいね件数と操作回数を分けています。付ける・取り消すを繰り返せば、現在の票が0でも操作履歴は残ります。コメントも記事IDと会員ID、または匿名識別子に結び付きます。管理者の集計ではその違いを確認できるようにし、公開画面へIPなどを出すことはしません。履歴を持てば何でも分析できるというわけではなく、過去に取っていなかった所有者を後から復元することもできません。この限界は残したまま扱います。

再送のテストでは、同じ所有者と記事にtrueを二回送り、票も履歴も一回分で止まるかを確認します。次にfalseを二回送り、取消が一回だけ残るかを見る。会員の別セッション、ゲストからの切り替え、他人のユーザーIDを送った場合も並べます。数字だけ一致していても、所有者や履歴がずれていたら集計は信用できません。テストデータはローカルに閉じ、本番の読者の反応へ混ぜない形で確かめています。

WordPressのnonceが保証する範囲は、公式のnonce解説を、fetchのHTTP応答の扱いはMDNのFetchガイドを確認できます。

その「成功」は、どこまで成功したのか

コピーできた? 共有できた? ブラウザの返事を待つ

「コピーしました」と出たのに、貼り付けると前のURL。コピーできたと思っているだけに、これは困りますよね。小さな案内文ですが、いつ成功と判断するかが大事になってきます。それでは、ブラウザAPIからどんな返事が来たら、この案内を出せるのかを見ていきましょう。

「コピー」は記事URLをクリップボードへ渡します。「共有」は対応端末でWeb Share APIを使い、OSの共有メニューを開きます。Web Share APIがない端末では、丸型表示の「共有」から追加の共有先を開けます。「その他」はキーボードで開閉でき、Escapeで閉じられます。スマートフォンでは配置とメニューの位置を調整し、画面からはみ出さないようにしています。

コピーのボタンも、押せば必ず成功するとは限りません。Clipboard APIは非同期の操作で、Promiseを通して成功や失敗が返ります。呼び出せたことと、コピーできたことは別なんです。安全なコンテキスト、権限、ユーザー操作との関係など、ブラウザ側の条件によって拒否されることもあります。まだ結果が分からないうちに「コピーしました」と伝えると、貼り付け先で何も出ずに困りますよね。操作が終わってから、その結果を表示するように考えます。

スマートフォンで共有先が一覧になる、あの画面も使えます。Web Share APIは、URLなどを端末の共有画面へ渡す機能です。ただ、SNSへの投稿が完了したことまでは分かりません。使える機能かどうかを確かめ、ユーザーの操作から呼び出し、キャンセルと障害も必要に応じて分けます。どの端末にも同じ共有先が入っているわけではないので、通常の共有リンクやコピーも残しておきます。手元の端末で、どの選択肢が出るか見てみるのも面白いですよ。

返るPromiseの意味はMDNのNavigator.share()にもまとまっています。端末によって解決のタイミングが違う点まで含め、投稿完了の通知には使わないようにします。

Promiseが返ったこと、解決したこと、SNSへの投稿が完了したことは別です。1.1.0ではコピーの成功案内をawaitの後に出し、拒否された場合はURLを表示して手動コピーへ退避します。一方、共有操作の計測イベントは失敗やキャンセルを区別せず通知します。現在のイベントを「共有成功」と読んではいけないわけです。成功・拒否・キャンセルを別の結果として公開する案は、互換性も含めて連載で検討します。

Web Share APIの拒否を、1.1.0の実装ではcatchで受け止めています。キャンセルと障害は分類していませんし、共有先での投稿結果も取得していません。分類を加えるなら、AbortErrorを必ず利用者のキャンセルだと断定しないことも必要です。共有先が存在しない場合などにも同じ例外が返ることがあります。エラー名をそのまま業務上の結果にせず、APIが保証している範囲で画面と計測を設計します。

ユーザー操作との近さも重要です。ブラウザは、ページを開いただけで勝手に共有画面を出したり、無関係なタイミングでクリップボードを書き換えたりされないよう、機能ごとに条件を設けています。ボタンのクリックから長い別処理を挟むと、操作に基づく呼び出しとして認められなくなる場合もあります。「メソッドが存在するか」だけでなく、「いま呼べるか」があるわけです。

Progressive Enhancementは、利用できる機能に応じて体験を加えていく考え方です。共有APIが使える環境では便利な入口を出し、使えない環境にも通常のリンクやコピーなど別の道を残す。何もかも全端末で同じに見せることより、目的へたどり着けることを優先します。スマートフォンとPCで選択肢が違っても、それだけで不具合とは限りません。

共有のクリックは、SNSへの投稿完了ではない

さて、ボタンが動いたら、どれくらい使われたかも気になってきます。ただ、数字を眺める前に、何を数えているのかを少し確認しておきましょう。ここを取り違えると、使われ方まで違って見えてしまうんです。共有クリックは promari-sns-share CustomEventでページ側に伝わります。Shadow DOM内のリンクを、ページ側の a[data-share] だけで監視する方式ではありません。当サイトでは接続コードがイベントを pm-analytics へ渡し、promari-observabilityの同意管理を通してGA4へ送ります。同意前は保留、拒否時は破棄します。

数えているのは共有操作であって、コピーの成功やSNSでの投稿完了ではありません。1.1.0のイベントには成功・失敗・キャンセルを区別する項目がなく、Web Shareの拒否後も同じ操作イベントが通知されます。いいねの確定件数とも別の指標です。プラグイン本体はGA4へ直接送らず、サイト側の計測レイヤーへ渡します。数値を読むときは、この観測範囲へ戻るようにしています。

Shadow DOM内の要素を、ページ全体のCSSセレクターで直接検索する設計にはしません。共有部品は操作を意味のあるCustomEventとして外へ通知し、サイト側がその契約を受け取ります。イベントが境界を越えるかはdispatchする要素やbubblescomposedの設定に関わるため、名前が同じだけでは連携を保証できません。イベント名、detail、送出元、成功やキャンセルの意味をセットで確認します。

アクセス解析を許可しない方にも、共有ボタンは使っていただきたいですよね。同意の状態は計測側が見て、送信するかどうかを決めます。こうして分けておけば、表示する部品へGA4固有の処理まで詰め込まずに済みます。集まった数値は、どの場所でどの操作が使われたかを考える材料です。クリック数をSNSの投稿数や閲覧人数と読み替えないよう、運用するときにもイベントの意味を揃えておきます。

CustomEventは、部品の内部DOMを外側へ公開せず、起きた操作を伝えるために使っています。イベント名だけでなく、detailの項目と値域、発火するタイミングも契約です。内部のボタンを置き換えてもこの契約を保てば、計測側のセレクターを修正せずに済みます。ただし、同じ操作から二回発火すれば計測も二重になります。再描画後のリスナー登録や、ホスト側の接続処理まで含めて一回性を確認します。

bubblesはイベントが祖先へ伝わるか、composedはShadow DOMの境界を越えられるかに関わる設定です。ただし、どの要素からdispatchしたかも重要です。ホスト要素から出す場合と内部要素から出す場合を、同じ図で済ませないようにします。イベント名を見つけたら、送出する場所と受け取る場所を線でつないでみると、連携の経路が理解しやすくなります。

今回の送出元を具体的に追うと、共有の通知はcustomEventTrackerが受け取ったホスト要素からdispatchしています。いいねの要求も、独自要素自身から出します。内部ボタンの生のクリックをそのまま外部へ公開する構成ではありません。そのため、内部のHTMLを変えても、イベント名とdetailの意味を保てば、サイト側の受け取り方を保ちやすくなります。

経路の詳細は、DOM仕様のイベント送出composedの解説、共有結果の限界はWeb Share仕様で確認できます。

計測の名前にも約束が必要です。「share_success」という言葉を見て、営業担当の方は投稿完了だと思い、実装した人は共有画面を開けたことだと思っているかもしれません。同じ数字を見ながら違う話をしてしまいます。計測は、数を集める前に「何を一回と数えるか」を決める仕事です。成功・開始・キャンセルを区別するなら、その定義を使う人にも説明します。

UTMは、共有したリンクから戻ってきた訪問の流入元などを区別するためのパラメーターです。共有ボタンを押した出来事を記録するイベントとは別の入口になります。二つの数が一致しなくても、それだけで実装ミスとは言えません。共有しても誰も開かないこともあれば、一つのリンクを複数の方が開くこともあるからです。数字が合わないからおかしい、と決める前に、それぞれ何を数えているのかへ戻ってみる。地味ですが、大事な確認ですね。

COMMENTS
コメント

このページ(3ページ目)の感想・質問・設計へのコメント

PASS IT ON

この気づきを、誰かにも。
THANK YOU FOR READING.すべての記事へ ↗

FROM INSIGHT TO IMPACT

「できたらいいな」を、
動く仕組みに。

記事で見つけたヒントを、あなたの事業へ。
新しいサービスも、手間のかかる業務も。
いまの課題から、つくるべきものを一緒に考えます。

開発・AI・研修の実績を見る

まだ、仕様書はいりません。

「何から始める?」から、ご一緒に。

課題がまとまっていなくても大丈夫。テーマを選ぶと相談文をご用意します。
連絡先の必須入力は、お名前とメールアドレスだけ。

まずは課題の整理から相談する

ご相談後の流れ

  1. 01 内容を確認
  2. 02 メールでご連絡
  3. 03 課題・進め方をご相談
送信だけで契約やお申込みが確定することはありません。