PROMARI JOURNAL

そういえば、書く場所へは共有できなかった。Qiita・Zenn・note・Medium・アメブロへの共有ボタンを足しました

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

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

記事の下の共有欄に、X や LINE はあるのに、読んだ人が自分の記事で紹介する Qiita・Zenn・note・Medium・アメブロへの入口がありませんでした。note は公式の共有入口へ、入口の無い4つは「題名と URL をコピーして投稿画面を開く」新しい動きで足しました。4.2.0 では、貼った先でそのまま記事のカードになるよう、共有先ごとの書き方をそろ…

PASS IT ON

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

入口の無い4つには、新しい動きを足した

ここからが、今回の本題です。Qiita・Zenn・Medium・アメブロには、外のページを共有する共有の入口がありません。note のように入口の URL へ記事の情報を付けて開く、というやり方が使えないのです。では、この4つの共有先のボタンは、押したら何をすればよいのでしょうか。

このプラグインがこれまで持っていた動きは、3種類でした。入口の URL を開く open、記事の URL をコピーする copy、スマホなどの端末に備わった共有の画面を呼び出す native です。入口の無い4つは、どれにもうまく当てはまりません。そこで、4つの案を並べて比べました。

図3-1 入口の無い4つに、どの道を選ぶか
図3-1 入口の無い4つに、どの道を選ぶか。現在のしくみ。4つの案を比べて、「題名と URL をコピーしてから、投稿画面を開く」を選びました。これを compose という新しい動きとして足しました。図3-1 入口の無い4つに、どの道を選ぶか。現在のしくみ。4つの案を比べて、「題名と URL をコピーしてから、投稿画面を開く」を選びました。これを compose という新しい動きとして足しました。

1つ目の案は、投稿画面を開くだけにすることです。open の入口の代わりに、投稿画面の URL を書いておけば、コードを1行も変えずに足せます。けれど、開いた投稿画面は空っぽで、記事の題名も URL もありません。読者は元のタブへ戻って URL をコピーし、また投稿画面へ戻って貼ることになります。入口を置いた意味がほとんどありません。

2つ目の案は、URL をコピーするだけにすることです。窓を開かないので、ブラウザに止められる心配もありません。けれど、それなら Qiita のボタンも Zenn のボタンも、すでにある「URLをコピー」のボタンと同じ動きになります。4つのボタンを並べる意味がなくなってしまいます。

3つ目の案は、投稿画面の URL に、題名や本文を付けて開くことです。GitHub の Issue のように、URL の後ろに ?title=… と書けば、題名が入った画面を開けるサービスもあります。けれど、4.1.0 を作ったときに確かめた範囲では、4つのサービスは、どれもそうした項目を読みませんでした。読まれない項目を付けて開くと、見た目にはちゃんと動いているのに、画面には何も入っていない、という失敗になります。エラーが出ないぶん、いちばん気づきにくい失敗です。実はこのあと、アメブロの投稿画面だけは、ログインした状態で確かめ直すと、題名と本文の項目を読むことが分かりました。その話は、この先の「コピーする文」の節でします。

そして4つ目の案が、今回選んだ「題名と URL をコピーしてから、投稿画面を開く」です。ボタンを1回押すと、記事の題名と URL がクリップボードに入り、同時にそのサービスの投稿画面が新しいタブで開きます。読者は、開いた画面に貼り付けるだけで、紹介の記事を書き始められます。この動きに、compose(書く、組み立てる)という名前を付けました。

操作の種類を増やすのは、共有先を1つ足すのとは重さの違う判断です。共有先は「データのカード」を足すだけで済みますが、操作の種類はプログラムの動きそのものを変えるからです。本編の連載で残した ADR-0002 にも、「クリックで何をするかの種類を増やす必要が出たら、別の判断として扱う」と書いていました。そこで今回の判断は、新しいADR(ADR-0003)として残しています。上の4つの案と、捨てた理由も、そこに書きました。

compose を使う共有先のカードは、note よりもさらに短くなります。Qiita の例を見てみましょう。

TOMLdestinations/qiita.toml(4.1.0 の実物・アイコンの行を除く)
# Qiita's draft editor. Qiita has no entry point for sharing an external page, so the title and URL are copied first.
# Icon: simple-icons 16.33.0 (qiita.svg), CC0 1.0. The logo remains a trademark of its owner.
key = 'qiita'
label = 'Qiita'
brand_color = '#55C500'
action = 'compose'
endpoint = 'https://qiita.com/drafts/new'

action が ‘compose’ になり、endpoint には共有の入口ではなく、投稿画面の URL を書きます。そして、[params] の表がありません。compose は、投稿画面の URL に何も付けないからです。付けても読まれないことは、3つ目の案のところで確かめたとおりです。4.1.0 では、Zenn・Medium・アメブロのカードも同じ形で、それぞれ8行でした(アイコンの行を含む)。4.2.0 で、この下に下書きの形を書く2行が加わり、アメブロは open へ移りました。それも、この先の節で見ます。

1回押すと、2つのことが同時に始まる

それでは、compose のボタンを押したとき、画面の裏で何が起きるのかを見ていきます。

図3.1-1 1回押すと、2つのことが同時に始まる
図3.1-1 1回押すと、2つのことが同時に始まる。現在のしくみ。押した瞬間に、紙挟みへ題名と URL を挟み、同時に投稿画面を新しいタブで開きます。読者は貼り付けるだけで書き始められます。図3.1-1 1回押すと、2つのことが同時に始まる。現在のしくみ。押した瞬間に、紙挟みへ題名と URL を挟み、同時に投稿画面を新しいタブで開きます。読者は貼り付けるだけで書き始められます。

押した瞬間に始まることは2つです。1つ目は、クリップボードへの書き込みです。書き込む中身は、4.1.0 では、記事の題名と、改行と、記事の URL の3つを並べた文でした。4.2.0 からは、共有先ごとに「そのサービスで記事のカードになる書き方」の文を書き込みます。URL には、ほかの共有先と同じようにUTMが付き、utm_source には qiita のような共有先の名前が入ります。2つ目は、投稿画面を新しいタブで開くことです。そして、2つのことが始まったあと、コピーがうまくいったかどうかを、画面の中の小さな知らせで伝えます。

書き込む文を決めているのは、ドメイン層(判断や決まりごとを置く層)の共有先の値オブジェクト ShareDestination にある、composeDraft という小さな関数です。2ページ目で見た、共有 URL を組み立てる shareUrl のすぐ隣に置いています。

TypeScriptweb/src/domain/model/ShareDestination.ts(コピーする文を決める関数・4.2.0)
composeDraft(request: ShareRequest): string {
  return this.action === ShareAction.Compose ? this.#render(request) : '';
}

#render(request: ShareRequest): string {
  return this.#draft
    ? DraftTemplate.render(this.#draft, LinkCardPolicy.bound(request))
    : DraftTemplate.render(DraftTemplate.COMPOSE_DEFAULT, request);
}

compose 以外の共有先なら空の文を返し、compose なら下書きを作って返します。下書きの形は、共有先のカードに書いたひな形(draft)で決まり、書いていなければ 4.1.0 と同じ「題名+改行+URL」です。ひな形へ記事の値を差し込むのは DraftTemplate、題名や説明を上限の長さで切るのは LinkCardPolicy という、ドメイン層の小さな2つのサービスが受け持ちます。共有 URL の組み立てと同じく、共有先が自分の操作に合わせて答えるので、呼ぶ側は操作の種類で分岐せずに済みます。とても短い関数ですが、「何をコピーするか」という決まりを、画面やブラウザの都合から切り離して1か所に置いているのがポイントです。4.2.0 で文の形をがらりと変えたときも、この関数を呼ぶ側は1行も直していません。ブラウザがなくても試験できるので、題名が空のときに下書きの先頭へ空行が残らないことも、試験で確かめています。

実際にクリップボードへ書いて、タブを開くのは、ユースケース(利用者の操作を受けて、手順を進める層)の HandleShareClickUseCase です。compose の部分は、次の数行です。

TypeScriptweb/src/application/HandleShareClickUseCase.ts(compose を受け持つ部分)
case 'compose': {
  preventDefault();
  // Popup blockers honour window.open only in the click's synchronous turn, so both effects start
  // before the first await. The clipboard write starts first; a synchronous throw becomes a rejection
  // so the editor still opens. A failed copy is reported, not retried with a prompt: the user is
  // already looking at the new tab (ADR-0003).
  const writing = new Promise<void>((resolve) => { resolve(this.#gateways.clipboard.write(button.draft)); });
  this.#gateways.newTab.open(button.href);
  return writing.then(() => 'composed' as const, () => 'compose-copy-failed' as const);
}

上から読んでいきます。2行目の preventDefault は、ボタンを押したときのブラウザのふつうの動きを止める指示です。3〜6行目のコメントには、なぜこの順番なのかを英語で書いています。7行目でクリップボードへの書き込みを始め、8行目で投稿画面のタブを開きます。9行目は、書き込みが終わったら、成功なら ‘composed’、失敗なら ‘compose-copy-failed’ という結果を返す、という意味です。この結果を見て、画面に出す知らせの文言が決まります。

ここで注目してほしいのは、7行目と8行目のあいだに、待つための書き方(await)が1つも無いことです。クリップボードへの書き込みは時間のかかる処理なので、ふつうなら「書き込みが終わるのを待ってから、次へ進む」と書きたくなります。けれど、ここではあえて待たずに、書き込みを始めたらすぐにタブを開いています。次の節で、その理由を見ていきます。

もう1つ、ユースケースの中にブラウザの機能の名前(navigator.clipboard や window.open)が出てこないことにも注目してください。this.#gateways.clipboard と this.#gateways.newTab は、ドメイン層に置いた「約束(インターフェース)」で、ブラウザの本物の機能は、別の層(infrastructure)がその約束に合わせて実装しています。今回、新しいタブを開くための約束 NewTabGateway を足し、infrastructure の BrowserNewTab が window.open で実装しました。小窓を開く既存の約束とは分けています。その理由は5ページ目の見直しの節でお話しします。この分け方のおかげで、試験では本物のブラウザの代わりに、呼ばれた順番を記録するだけの代役を渡して、「コピー→タブ」の順に呼ばれることを確かめられます。本編の連載2.5「DIP――ブラウザの機能を、ポートの向こうへ置く」で読んだ作りが、ここでもそのまま効いています。

コピーする文は、貼る先のカードの書き方に合わせる

4.1.0 の compose がコピーした文は、「題名+改行+URL」という、とても素朴な形でした。技術ブログの共有なら、Markdown(文字に記号を添えて見出しやリンクを表す書き方)のリンクの形、[題名](URL) にしたほうが気が利いているのでは、と思うかもしれません。実は、その案も考えました。

コピーする文の形の案(4.1.0 を作ったときの比較)
案貼った結果(Markdown で書く場所)貼った結果(見たまま書く場所)
[題名](URL)題名がリンクになる記号の [ ] ( ) がそのまま残る
題名+改行+URL1行目が文、2行目が URL(URL の行はカードになることもある)1行目が文、2行目が URL(多くはリンクになる)
URL だけURL の1行だけURL の1行だけ

4つのサービスのうち、Qiita と Zenn は、Markdown で書く場所です。一方、Medium とアメブロは、画面に見えているとおりに書いていく場所(見たまま書くエディタ)が基本です。Markdown のリンクの形でコピーすると、Qiita と Zenn ではきれいなリンクになりますが、Medium とアメブロでは、[ や ( の記号が、そのまま文の中に残ってしまいます。

4つとも同じ1つの文をコピーする作りなので、どの書く場所に貼っても崩れない形を選ぶ必要がありました。題名と URL を別の行に分けるだけなら、Markdown の場所でも見たまま書く場所でも、そのまま読める文になります。URL を独立した行にしておけば、URL だけの行をカードやリンクに変えてくれる機能にも拾ってもらえます。題名が空のときに URL だけにするのも、同じ考え方です。

共有先ごとにコピーする文の形を変える、という案もありました。4.1.0 では、カードの項目が増えるのを避けて、ADR-0003 の見直す条件に「題名と URL 以外も貼りたいという要望が出たら」と書き、必要になったときに考えることにしていました。ところが、その「必要になったとき」は、公開したその日のうちに来ました。

公開したあとで、ログインした Chrome で5つのボタンを実際に押し、開いた投稿画面に貼ってみたのです。すると Qiita でも Medium でも、貼った2行はただの文字とリンクのままで、記事の題名や画像の付いたカードにはなりませんでした。アメブロは、白紙の投稿画面が開くだけです。「どこに貼っても崩れない」ことと、「貼った先で記事が引き立つ」ことは、別のことだったわけです。そこで、それぞれのサービスで、記事をカードにする書き方を1つずつ確かめ直しました。

図3.2-1 記事は1つ、差し込み口はサービスごと
図3.2-1 記事は1つ、差し込み口はサービスごと。実測。記事は1つでも、カードになる書き方はサービスごとに違います。そこで 4.2.0 から、共有先ごとに「そのサービスでカードになる下書き」を宣言するようにしました。図3.2-1 記事は1つ、差し込み口はサービスごと。実測。記事は1つでも、カードになる書き方はサービスごとに違います。そこで 4.2.0 から、共有先ごとに「そのサービスでカードになる下書き」を宣言するようにしました。

旅行用の変換プラグを思い浮かべてください。家電のプラグは1つでも、国ごとにコンセントの形が違うので、差し込み口に合わせたアダプタを挟みます。記事の題名や URL も同じで、中身は1つでも、カードになる「差し込み口の形」はサービスごとに違いました。

サービスごとの、記事をカードにする書き方(2026-10-01 にログインした Chrome で確認・ADR-0004)
共有先動き下書きの形貼った・開いた結果
Qiitacompose題名・空行・URL・空行プレビューで、URL の行が記事のカードになる。4.1.0 の形は空行が足りず、カードにならない
Zenncompose題名・空行・@[card](URL)プレビューで、記事のカードになる
MediumcomposeURL だけ空の行に貼って Enter を押すと、記事のカードになる。題名と URL の2行を貼ると1つの段落にまとめられ、Enter を押してもカードにならない
アメブロopen(4.1.0 は compose)カードの HTML を本文として送る題名とカードが入った状態で、投稿画面が開く
noteopen(変更なし)公式の入口に URL を渡す記事のカードが入った「noteで書く」の画面が開く

表の中で、アメブロだけは動きそのものが変わっています。アメブロの投稿画面(srventryinsertinput.do)は、URL に entry_title と entry_text という項目を付けて開くと、題名と本文をあらかじめ入れた状態で開くことが分かったのです。1ページ目で「入口なし」としたとおり、これは外のサイト向けに公開された共有の入口ではありません。けれど、読まれる項目があるなら、コピーして貼ってもらうより、開いた時点で本文にカードが入っているほうが、読者の手間が1つ減ります。そこでアメブロは compose をやめ、note と同じ open に移しました。本文には、アメブロのエディタが記事のカードとして表示する HTML(ogpCard という形)を入れて送ります。公開された仕様ではないので、確かめた日付ごと記録に残しています。

Medium は、少し事情が違います。Medium の公式ヘルプには、新しい行に URL を貼って Enter を押すと埋め込みになる、と書いてあります。ところが「題名+改行+URL」を貼ると、Medium は2行を1つの段落にまとめてしまい、URL が独立した行になりません。そのため、Enter を押してもカードになりませんでした。そこで Medium だけは URL だけをコピーし、コピーの知らせに「貼り付けたあと Enter を押すと、記事のカードになります」と添えることにしました。Medium には記事を取り込む機能(インポート)もありますが、公式ヘルプはこれを、取り込む権利を持つ内容に限っています。他人の記事を紹介する今回の用途には当たらないので、使っていません。

共有先ごとの書き方は、コードではなく、共有先のカードに書きます。4.2.0 の Qiita のカードは、次のようになりました。

TOMLdestinations/qiita.toml(4.2.0 の実物・コメントとアイコンの行を除く)
key = 'qiita'
label = 'Qiita'
brand_color = '#55C500'
action = 'compose'
endpoint = 'https://qiita.com/drafts/new'
draft = "{title}\n\n{url}\n"
draft_format = 'text'

draft がひな形で、{title} や {url} のところに記事の値が入ります。使える差し込み口は {title}・{url}・{description}・{image}・{host} の5つだけで、それ以外を書くと生成器が止めます。draft_format は、値をそのまま入れる text か、HTML として安全な形に直して入れる html かの区別です。アメブロのカードは html なので、題名や説明に < や & が入っていても、カードの形は崩れません。この判断は ADR-0004 に残しました。カードになる書き方は、共有先ごとのカードに宣言する。2ページ目で見た「共有先はデータ」という作りを、下書きの形にまで広げたわけです。

アメブロには、もう1つ気をつけることがありました。下書きを URL に入れて送るので、URL が長くなるのです。日本語の1文字は URL の中では9文字になり、題名はアメブロの題名の欄とカードの両方に入ります。長すぎる URL を送ると、アメブロは 400(リクエストが正しくない)を返し、投稿画面そのものが開きません。そこで、通る長さを二分探索で測ってみると、面白いことが分かりました。

アメブロの投稿画面に通る URL の長さ(2026-10-01 に実測)
状態アメブロへ送るクッキー通った URL400 になった URL
ログインしていないほぼ無し6,000 文字まで(ログイン画面へ転送)8,000 文字
ログイン済み約1,800 文字5,105 文字まで5,140 文字

ログインすると、通る URL が1,000文字以上短くなりました。上限は URL だけでなく、クッキーまで合わせたリクエスト全体の大きさで決まっていたのです。クッキーの量は読者ごとに違い、ページからは測れません。そこで、下書きを送る URL を 3,500 文字以内に収めることにしました。ログイン済みで通った 5,105 文字との差は約1,600文字あるので、今回の計測よりクッキーが1,600文字ほど多い読者まで通る見込みです。超えそうなときは、まず説明を縮めて外し、次に画像を外し、最後に題名を縮めます。この順番を受け持つのが、さきほどの LinkCardPolicy です。手元の試験ページで、この記事の題名・説明・画像を使って確かめたときは 3,038 文字で、カードの中身はどれも削られずに入りました。

なぜ、書き込みの完了を待たずにタブを開くのか。答えは、ブラウザに備わったポップアップブロッカーです。

ページが勝手に新しい窓やタブを開けると、広告の窓が次々に開くような迷惑なサイトを作れてしまいます。そこでブラウザは、読者がクリックなどの操作をした、その直後にだけ、窓を開くことを許します。仕様では、この「操作の直後」をユーザーの操作の直後と呼んでいます。この記事では、クリックのたびに発行される「有効期限つきの切符」にたとえます。

図3.3-1 窓を開けるのは、クリックの切符が有効なうちだけ
図3.3-1 窓を開けるのは、クリックの切符が有効なうちだけ。説明。ブラウザは、クリックのその場で頼まれた窓だけを開きます。だから、コピーの完了を待たずに、同じ瞬間にタブを開きます。図3.3-1 窓を開けるのは、クリックの切符が有効なうちだけ。説明。ブラウザは、クリックのその場で頼まれた窓だけを開きます。だから、コピーの完了を待たずに、同じ瞬間にタブを開きます。

切符が有効なのは、クリックを受けて動き始めた処理の、最初のひと続きのあいだです。そこで await を使って、クリップボードへの書き込みが終わるのを待つと、待っているあいだに処理がいったん途切れます。書き込みが終わって処理が再開したときには、ブラウザから見ると「クリックの直後」ではなくなっていることがあり、そこで窓を開こうとすると、ポップアップブロッカーに止められてしまうのです。

だから compose では、クリップボードへの書き込みと、タブを開く処理の両方を、最初の await より前に呼び始めるようにしました。書き込みは「始めた」だけで、終わりを待っていません。終わったかどうかは、タブを開いたあとで、結果として受け取ります。こうすれば、タブを開く瞬間は、必ずクリックの切符が有効なうちになります。

書き込みを先に始めているのにも、小さな理由があります。クリップボードへの書き込みも、ブラウザによっては「操作の直後」かどうかや、ページに焦点があるかどうかを見ます。タブを先に開くと、焦点が新しいタブへ移ってしまい、書き込みが断られやすくなります。そこで、書き込みを先に始め、そのすぐあとにタブを開く順番にしました。ただ、これでも書き込みが終わる前に焦点が移ることはありえます。その場合にどうなるかは、次の節で扱います。

コードの7行目で、書き込みを new Promise で包んでいるのにも意味があります。クリップボードの機能がそもそも使えない環境では、書き込みを呼んだ瞬間に例外(エラー)が出ることがあります。そのまま例外が飛ぶと、8行目のタブを開く処理まで届かずに、処理が止まってしまいます。Promise で包むと、その場の例外も「あとで受け取る失敗」に変わるので、書き込みがどんな形で失敗しても、8行目は必ず実行されます。

コピーに失敗しても、投稿画面は開く

それでは、クリップボードへの書き込みに失敗したら、どうなるのでしょうか。

図3.4-1 コピーに失敗しても、投稿画面は開く
図3.4-1 コピーに失敗しても、投稿画面は開く。現在のしくみ。どちらの色でも投稿画面は開きます。違うのは知らせの文言だけです。失敗したことを、成功のふりをして隠しません。図3.4-1 コピーに失敗しても、投稿画面は開く。現在のしくみ。どちらの色でも投稿画面は開きます。違うのは知らせの文言だけです。失敗したことを、成功のふりをして隠しません。

結論から言うと、成功しても失敗しても、投稿画面は開きます。違うのは、画面の中に出す知らせの文言だけです。成功なら「記事のリンクをコピーしました。投稿画面に貼り付けてください」(4.1.0 では「タイトルとURLを…」。Medium では、このあとに Enter の案内が付きます)、失敗なら「コピーできませんでした。投稿画面にタイトルとURLを入力してください」と出します。

失敗の知らせを、成功のふりをして隠さないことは大事です。もし失敗しても「コピーしました」と出してしまったら、読者は投稿画面で貼り付けて、何も入らないことに戸惑います。失敗したなら、失敗したと正直に伝えれば、読者は元のタブから自分でコピーするなど、次の手を打てます。

一方で、「URLをコピー」のボタン(copy)では、コピーに失敗したとき、URL を入れた小さな入力の窓(prompt)を出して、読者が自分でコピーできるようにしています。compose では、この窓を出しません。compose で失敗が分かるころには、読者の目はもう新しいタブの投稿画面にあります。元のページに窓を出しても、読者からは見えないのです。見えない窓で読者を待たせるより、知らせの文言だけにしておくほうが親切だと判断しました。

知らせの文言は、サイトの設定で変えられます。設定の messages という表に、composed(成功)と compose_failed(失敗)の2つの項目を足しました。どちらも書かなければ、上の既定の文言が使われます。

TOMLconfig/share_config.example.toml(知らせの文言の設定例・4.2.0)
composed = "記事のリンクをコピーしました。投稿画面に貼り付けてください" # Optional. After copying for qiita / zenn / medium; a destination's compose_hint follows it.
compose_failed = "コピーできませんでした。投稿画面にタイトルとURLを入力してください" # Optional. When that copy fails; the editor still opens.

既定の文言を持たせたのは、これまでの設定ファイルを1文字も直さずに、新しい版へ上げられるようにするためです。項目を必須にすると、新しい版に上げた瞬間、すべてのサイトの設定が検査で止まってしまいます。今回の変更は「足すだけ」の変更なので、版の番号も、互換性を保った機能の追加を示す 4.1.0 にしました。

ここまでで、compose で分からないことも2つあります。1つは、新しいタブが本当に開けたかどうかです。投稿画面はnoopenerを付けて開くので、開いた窓の手がかりが元のページに返ってきません。開いた先のページが、元のページを書き換えるのを防ぐための指定ですが、その代わりに、開けたかどうかも分からないのです。もう1つは、実際のブラウザでどれくらいの割合でコピーが断られるかです。ログインした Chrome で押したときは、どれもコピーに成功しましたが、1回ずつ確かめただけで、割合を測ったわけではありません。Safari では、まだ確かめていません。どちらも ADR-0003 の「悪い結果」と「見直す条件」に書いてあります。

compose のボタンは、画面の上では、X や note のボタンと同じ丸い見た目をしています。けれど、HTML の中では、別の部品として描いています。X や note・アメブロは <a>(リンク)、compose の3つは <button>(ボタン)です。

TypeScriptweb/src/domain/service/ShareActionPolicy.ts(操作ごとの、ボタンかリンクかの表)
const CONTROL: Readonly<Record<ShareAction, { readonly followsLink: boolean; readonly linkable: boolean }>> = {
  [ShareAction.Open]: { followsLink: true, linkable: true },
  [ShareAction.Copy]: { followsLink: false, linkable: true },
  [ShareAction.Native]: { followsLink: false, linkable: true },
  [ShareAction.Compose]: { followsLink: false, linkable: false },
};

どちらで描くかを決めているのは、表示の層ではなく、ドメイン層のこの表です。followsLink は「押すことが、そのままリンクをたどることか」(open だけが真)、linkable は「リンクとして描いてよいか」(compose だけが偽)を表します。compose をリンクとして描くと、手順を通らずにリンクだけがたどられたとき、コピーせずに投稿画面が開いてしまいます。だから compose だけは、リンクとして描いてはいけないのです。

この表は、操作の種類(ShareAction)のすべての値について書くことを、TypeScript の型(Record)が求めています。いつか操作を1つ足したら、この表に書き足すまでコンパイルが通りません。表示の層は、手順の層が表から取り出して渡す2つの値を読むだけです。円い表示なら、followsLink が真ならリンク、偽ならボタン、と描き分けます。

なぜ分けるのでしょうか。リンクとボタンは、見た目を同じにできても、意味が違うからです。リンクは「どこかへ移動する」もので、行き先の URL を持っています。ボタンは「何かをする」もので、押したときの動きを持っています。open の共有先は、入口の URL へ移動するのが仕事なので、リンクがふさわしい。一方、compose は、移動する前にコピーという「動き」をするのが仕事なので、ボタンがふさわしいのです。

この違いは、画面を見ない人にとって、とくに大事です。画面の読み上げソフトは、リンクを「リンク」、ボタンを「ボタン」と読み上げます。compose をリンクとして描くと、読み上げを聞いた人は「Qiita へ移動するのかな」と思って押し、思いがけずクリップボードが書き換わることになります。ボタンとして描けば、「Qiita、ボタン」と読み上げられ、押すと何かの動きが起きることが伝わります。キーボードで操作する人にとっても、リンクは Enter で、ボタンは Enter と Space で押せる、という違いがあります。

見た目は同じ丸いボタンのままにしているので、目で見て使う人にとっては、X も Qiita も同じように押せます。見た目は共有欄としてそろえ、中身の意味は動きに合わせて分ける。この部品では、既存の copy や native でも同じ考え方を取っていて、今回の compose もそれに合わせました。

順番は、代役を使った試験で確かめる

「コピーを始めてから、待たずにタブを開く」という順番は、目で見ても確かめにくいものです。人がボタンを押すと、コピーもタブも一瞬で終わってしまうからです。そこで、この順番を試験で確かめています。試験では、本物のクリップボードやブラウザの代わりに、呼ばれたことを記録するだけの代役を渡します。

試験の名前は、日本語で「何を確かめるか」がそのまま読めるように書いています。compose について足した試験のうち、2つを見てみましょう。

TypeScriptweb/test/application.test.ts(compose の試験の名前・抜粋)
it('書き込みが成功したら composed を返し、操作を一度だけ通知する', async () => { /* … */ });
it('書き込みが拒否されても・同期で投げても投稿画面は開き、compose-copy-failed を返す(prompt は出さない)', async () => { /* … */ });

1つ目は、うまくいったときの試験です。クリップボードの代役が書き込みを受け付けたら、結果が composed になること。そして、「共有ボタンが押された」という合図が、1回だけ出ることを確かめます。合図が2回出ると、5ページ目で見る計測で、押された回数が2倍に数えられてしまいます。

2つ目は、失敗したときの試験です。クリップボードの代役に、書き込みを断らせたり、呼ばれた瞬間に例外を投げさせたりします。どちらの場合も、タブを開く代役が呼ばれていること、結果が compose-copy-failed になること、そして copy のときのような入力の窓(prompt)を出していないことを確かめます。「失敗しても投稿画面は開く」という約束を、失敗の種類ごとに確かめているわけです。

既存の copy の試験には、「書き込みの完了を待ってから結果を返す(完了前に成功を名乗らない)」というものもあります。compose も同じで、タブは待たずに開きますが、結果(composed か、compose-copy-failed か)は、書き込みが終わってから返します。待たないのは窓を開くところだけで、成功を名乗るのは、成功が分かってから。この2つを分けて考えるのが、compose の作りの要でした。

この順番の作り方について、判断を書き残しておきます。コピーとタブは、最初の await より前に、この順で呼び始める。ブラウザの制約と、読者の手間の両方を満たす、ちょうど間の置き方です。

貼り付けると、記事のカードになる

最後に、読者の側から見た compose の結果を見ておきましょう。投稿画面に貼り付けると、何が入るのでしょうか。

図3.7-1 貼り付けると、記事のカードになる
図3.7-1 貼り付けると、記事のカードになる。実測。Qiita に貼り付けると、題名の行と、前後が空いた URL の行が入り、URL の行は記事のカードになります。あとは自分の感想や使ってみた話を書き足すだけです。図3.7-1 貼り付けると、記事のカードになる。実測。Qiita に貼り付けると、題名の行と、前後が空いた URL の行が入り、URL の行は記事のカードになります。あとは自分の感想や使ってみた話を書き足すだけです。

Qiita の投稿画面に貼り付けると、1行目に記事の題名、1行あけて記事の URL が入ります。プレビューを見ると、URL の行は、記事の題名・説明・画像の付いたカードになっています。あとは、その下に自分の言葉で書き足すだけです。「この記事を参考に試してみたら、ここでつまずいた」「自分の環境ではこうだった」。紹介の記事の書き始めとして、ちょうどよい形だと思います。

4.1.0 のこの節では、「カードになるかどうかは、各サービスのエディタの機能と設定しだいで、この記事では確かめていません」と書いていました。4.2.0 では、そこを確かめています。ログインした Chrome で4つのボタンを押し、Qiita と Zenn は貼ったあとのプレビューで、Medium は貼って Enter を押したあとで、アメブロは開いた時点で、記事がカードとして入ることを見ました。確かめるために作った下書きは、あとで削除しています。

こうして、入口の無い4つにも、1回押せば書き始められる道ができました。次のページでは、増えたボタンを共有欄のどこに、どう並べたかを見ていきます。

考えてみる:compose を小窓(ポップアップ)で開かず、いつも新しいタブにしたのはなぜ?

これまでの open の共有先は、設定しだいで、小さな窓(600×500 程度)で開いていました。X の投稿画面のように、短い文を書いてすぐ閉じる画面なら、小窓が便利です。けれど compose で開くのは、記事を書くためのエディタです。小窓では狭すぎますし、ログインしていなければ、その狭い窓でログインの画面を通ることになります。同じタブで開く案もありましたが、それだと元の記事が画面から消えて、読み返しながら書くことができません。そのうえ、コピーの結果を知らせるページも消えてしまいます。そこで compose だけは、設定に関わらず、いつも新しいタブで開くことにしました。代わりに、小窓の設定が compose には効かないことを、設定の説明に書いています。

COMMENTS
コメント…

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

PASS IT ON

この気づきを、誰かにも。

この記事を書いた人

Takaomi Murasaki

Promari SNS Shareの開発を通して、Webの仕組みや設計の選び方を紹介しています。動く実装と、その判断に至るまでを記事に残しています。

公開記事 11 件

プロフィールを見る
THANK YOU FOR READING.すべての記事へ ↗

FROM INSIGHT TO IMPACT

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

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

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

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

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

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

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

ご相談後の流れ

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