これまで誰もリンクしたことがない場所に大胆なリンクをする: テキスト フラグメント

テキスト フラグメントを使用すると、URL フラグメント内でテキスト スニペットを指定できます。そのようなテキスト フラグメントを含む URL に移動する場合、ブラウザはそのテキスト フラグメントを強調してユーザーの注意を引くことができます。

Thomas Steiner
Thomas Steiner

フラグメント識別子

Chrome 80 は大きなリリースでした。このリリースには、ウェブワーカーの ECMAScript モジュールnull 合体オプション チェーンなど、待望の機能が数多く含まれていました。このリリースは、通常どおり Chromium ブログのブログ投稿で発表されました。以下のスクリーンショットは、このブログ投稿の抜粋です。

id 属性が設定された要素を赤いボックスで囲んだ Chromium ブログ投稿

赤いボックスが何を意味するのか疑問に思われていることでしょう。これは、DevTools で次のスニペットを実行した結果です。id 属性を持つすべての要素がハイライト表示されます。

document.querySelectorAll('[id]').forEach((el) => {
  el.style.border = 'solid 2px red';
});

フラグメント識別子をページの URL のハッシュで使用すると、赤いボックスでハイライト表示された要素へのディープリンクを配置できます。別にある [プロダクト フォーラムでフィードバックをお寄せください] ボックスへのディープリンクが必要な場合は、URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1 を手作りすることで実行できます。デベロッパー ツールの [要素] パネルからわかるように、対象の要素には、値が HTML1id 属性があります。

要素の id を表示するデベロッパー ツール

JavaScript の URL() コンストラクタを使用してこの URL を解析すると、さまざまなコンポーネントが表示されます。hash プロパ��ィの値が #HTML1 であることに注目してください。

new URL('https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1');
/* Creates a new `URL` object
URL {
  hash: "#HTML1"
  host: "blog.chromium.org"
  hostname: "blog.chromium.org"
  href: "https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1"
  origin: "https://blog.chromium.org"
  password: ""
  pathname: "/2019/12/chrome-80-content-indexing-es-modules.html"
  port: ""
  protocol: "https:"
  search: ""
  searchParams: URLSearchParams {}
  username: ""
}
*/

要素の id を見つけるためにデベロッパー ツールを開く必要がありましたが、ページのこの特定のセクションがブログ投稿の作成者によってリンクされる可能性について大いに語っています。

id なしでリンクしたい場合はどうすればよいですか?ウェブワーカーの ECMAScript モジュールの見出しにリンクするとします。以下のスクリーンショットからわかるように、問題の <h1> には id 属性がありません。つまり、この見出しにリンクすることはできません。これがテキスト フラグメントによって解決される問題です。

id のない見出しが表示されている Dev Tools

テキスト フラグメント

テキスト フラグメントの提案では、URL ハッシュでテキスト スニペットを指定できるようになりました。そのようなテキスト フラグメントを含む URL に移動する場合、ユーザー エージェントはそのテキスト フラグメントを強調してユーザーの注意を引くことができます。

ブラウザの互換性

対応ブラウザ

  • 89
  • 89
  • x
  • x

ソース

セキュリティ上の理由から、この機能ではリンクを noopener コンテキストで開く必要があります。そのため、<a> アンカー マークアップに rel="noopener" を含めるか、ウィンドウ機能機能の Window.open() リストに noopener を追加してください。

start

最も単純な形式のテキスト フラグメントの構文は、ハッシュ記号 # の後に :~:text= が続き、最後に start(リンク先のパーセントでエンコードされたテキストを表す)のようになります。

#:~:text=start

たとえば、Chrome 80 の機能を発表するブログ投稿ウェブ ワーカーの ECMAScript モジュールの見出しにリンクする場合、URL は次のようになります。

https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules%20in%20Web%20Workers

テキスト フラグメントはこのように強調されます。Chrome などの対応ブラウザでリンクをクリックすると、テキスト フラグメントがハイライト表示され、ビューにスクロールします。

テキスト フラグメントがスクロールしてハイライト表示されます。

startend

では、見出しだけでなく、ウェブワーカーの ECMAScript モジュールというセクション全体にリンクする場合はどうすればよいでしょうか。セクションのテキスト全体をパーセントでエンコードすると、URL が実質的に長くなってしまいます。

幸いなことに、もっと良い方法があります。start,end 構文を使用すると、テキスト全体ではなく、目的のテキストをフレームに収めることができます。したがって、目的のテキストの先頭にパーセントでエンコードされた単語をいくつか指定し、テキストの末尾にカンマ , で区切ってパーセントでエンコードされた単語をいくつか指定します。

たとえば、次のようになります。

https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules%20in%20Web%20Workers,ES%20Modules%20in%20Web%20Workers..

start には ECMAScript%20Modules%20in%20Web%20Workers、カンマ ,end として ES%20Modules%20in%20Web%20Workers. が続きます。Chrome などの対応ブラウザでクリックスルーすると、セクション全体がハイライト表示され、スクロールされて表示されます。

テキスト フラグメントがスクロールしてハイライト表示されます。

ここで、startend の選択について疑問があるかもしれ��せん。実際には、左右に 2 語しかない、やや短い URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules,Web%20Workers. でも機能します。start �� end ��������値��比較します。

さらに一歩進めて、startend の両方に 1 つの単語のみを使用すると、私が困っていることがわかります。URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript,Workers. はさらに短くなりましたが、ハイライト表示されたテキスト フラグメントは、本来望まれていたものではなくなりました。ハイライト表示は Workers. という単語が最初に出現する箇所で停止します。これは正しいですが、強調したいことではありません。問題は、目的のセクションが現在の 1 語の start 値と end 値で一意に識別されないことです。

意図しないテキスト フラグメントがビューにスクロールされてハイライト表示される

prefix--suffix

一意のリンクを取得する 1 つの方法は、startend に十分な長さの値を使用することです。ただし、場合によっては不可能なこともあります。さて Chrome 80 リリースのブログ投稿を 例に挙げたのはなぜでしょうかこのリリースでは、次のようにテキスト フラグメントが導入されました。

ブログ投稿のテキスト: テキスト URL フラグメント。ユーザーや作成者は、URL で提供されるテキスト フラグメントを使用して、ページの特定の部分にリンクできるようになりました。ページが読み込まれると、ブラウザはテキストをハイライト表示し、フラグメントを表示するためにスクロールします。たとえば、次の URL は「Cat」の Wiki ページを読み込み、「text」パラメータでリストされているコンテンツまでスクロールします。
テキスト フラグメントに関するお知らせのブログ投稿の抜粋。

上のスクリーンショットでは、「text」という単語が 4 回出現しています。4 回目は緑色のコードフォントで書かれます。この特定の単語にリンクする場合は、starttext に設定します。「text」という単語は 1 つの単語なので、end はありえません。その場合、何をすべきでしょうか。URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=text は、見出しにすでに出現する「Text」という単語が最初に出現する箇所と一致します。

「Text」が最初に出現したときのテキスト フラグメントの一致。

幸いなことに、解決策があります。このような場合は、prefix​--suffix を指定できます。緑色のコード フォントの前の単語は「the」、後の単語は「parameter」です。「text」という単語の他の 3 つの出現箇所の、前後の単語が同じではありません。この知識を身につけると、前の URL を調整して、prefix--suffix を追加できます。他のパラメータと同様に、パラメータもパーセント エンコードする必要があります。また、複数の単語を含めることができます。https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=the-,text,-parameter。 パーサーが prefix--suffix を明確に識別できるようにするには、これらを start およびオプションの end と、ダッシュ(-)で区切る必要があります。

「text」の出現箇所でマッチングするテキスト フラグメント。

完全な構文

テキスト フラグメントの完全な構文を以下に示します。(角かっこは省略可能なパラメータを示します)。すべてのパラメータの値は、パーセントでエンコードする必要があります。これは、ダッシュ -、アンパサンド &、カンマ , の文字がテキスト ディレクティブ構文の一部として解釈されないために特に重要です。

#:~:text=[prefix-,]start[,end][,-suffix]

prefix-startend-suffix のそれぞれは、1 つのブロックレベルの要素内のテキストにのみ一致しますが、start,end の全範囲は複数のブロックにまたがることができます。たとえば次の例では、開始文字列「Thequick」が連続した 1 つのブロックレベル要素内に存在しないため、:~:text=The quick,lazy dog は一致しません。

<div>
  The
  <div></div>
  quick brown fox
</div>
<div>jumped over the lazy dog</div>

ただし、次の例では一致します。

<div>The quick brown fox</div>
<div>jumped over the lazy dog</div>

ブラウザ拡張機能を使用してテキスト フラグメント URL を作成する

テキスト フラグメント URL を手動で作成するのは面倒な作業です。特に、URL を一意にする場合には注意が必要です。必要に応じて、この仕様にいくつかのヒントと正確なテキスト フラグメント URL の生成手順が記載されています。Google では、テキストを選択してから [Copy Link to Selected Text] をクリックすることで、任意のテキストにリンクできる Link to Text Fragment というオープンソースのブラウザ拡張機能を提供しています。この拡張機能は、次のブラウザで使用できます。

Text Fragment にリンク ブラウザ拡張機能。

1 つの URL に複数のテキスト フラグメントが含まれている

1 つの URL に複数のテキスト フラグメントを含めることができます。特定のテキスト フラグメントは、アンパサンド文字 & で区切る必要があります。3 つのテキスト フラグメントを含むリンクの例を次に示します。https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=Text%20URL%20Fragments&text=text,-parameter&text=:~:text=On%20islands,%20birds%20can%20contribute%20as%20much%20as%2060%25%20of%20a%20cat's%20diet

1 つの URL に 3 つのテキスト フラグメントを含める。

要素とテキスト フラグメントの混在

従来の要素フラグメントは、テキスト フラグメントと組み合わせることができます。たとえば、ページの元のテキストが変更されたときにテキスト フラグメントが一致しなくなった場合に、意味のある代替手段を提供するなど、両方を同じ URL に含めることはまったく問題ありません。「サービス フォーラムでフィードバックをお寄せください」セクションにリンクされている URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1:~:text=Give%20us%20feedback%20in%20our%20Product%20Forums. には、要素フラグメント(HTML1)とテキスト フラグメント(text=Give%20us%20feedback%20in%20our%20Product%20Forums.)の両方が含まれています。

要素フラグメントとテキスト フラグメントの両方とのリンク。

Fragment ディレクティブ

まだ説明していない構文の要素が 1 つあります。それは、フラグメント ディレクティブ :~: です。上記のような既存の URL 要素フラグメントとの互換性の問題を回避するために、テキスト フラグメントの仕様ではフラグメント ディレクティブが導入されています。フラグメント ディレクティブは、URL フラグメントの一部で、コード シーケンス :~: で区切られます。これは、text= などのユーザー エージェント指示用に予約されており、読み込み中に URL から削除されます。そのため、作成者スクリプトが直接やり取りすることはできません。ユーザー エージェントの手順は、ディレクティブとも呼ばれます。具体例では、text=テキスト ディレクティブと呼ばれます。

機能検出

サポートを確認するには、document で読み取り専用の fragmentDirective プロパティをテストします。フラグメント ディレクティブは、ドキュメントではなくブラウザに対する指示を URL で指定するためのメカニズムです。これは、作成者スクリプトとの直接のやり取りを避け、既存のコンテンツに破壊的変更が発生することを恐れずに、将来のユーザー エージェント向けの指示を追加することを目的としています。今後追加される要素の例として、翻訳ヒントがあります。

if ('fragmentDirective' in document) {
  // Text Fragments is supported.
}

機能検出の主な目的は、リンクが動的に生成されるケース(検索エンジンなど)で、リンクをサポートしていないブラウザにテキスト フラグメント リンクが配信されないようにすることです。

テキスト フラグメントのスタイル設定

デフォルトでは、ブラウザのテキスト フラグメントのスタイルは mark のスタイル(通常は黄色の上に黒、mark の CSS システムカラー)と同じです。ユーザー エージェント スタイルシートには、次のような CSS が含まれています。

:root::target-text {
  color: MarkText;
  background: Mark;
}

ご覧のとおり、ブラウザは疑似セレクタ ::target-text を公開します。これを使用して、適用するハイライト表示をカスタマイズできます。たとえば、赤い背景の上に黒のテキストとなるようにテキスト フラグメントを設計できます。いつものように、オーバーライド スタイルがユーザー補助の問題を引き起こさないように、必ず色のコントラストをチェックし、ハイライトが他のコンテンツよりも視覚的に目立つようにしてください。

:root::target-text {
  color: black;
  background-color: red;
}

ポリフィル性

テキスト フラグメント機能は、ある程度ポリフィルできます。機能が JavaScript で実装されているテキスト フラグメントの組み込みサポートを提供していないブラウザ向けに、拡張機能によって内部で使用されるpolyfillが用意されています。

polyfillには fragment-generation-utils.js ファイルが含まれています。このファイルをインポートして、テキスト フラグメント リンクの生成に使用できます。以下のコードサンプルに、その概要を示します。

const { generateFragment } = await import('https://unpkg.com/text-fragments-polyfill/dist/fragment-generation-utils.js');
const result = generateFragment(window.getSelection());
if (result.status === 0) {
  let url = `${location.origin}${location.pathname}${location.search}`;
  const fragment = result.fragment;
  const prefix = fragment.prefix ?
    `${encodeURIComponent(fragment.prefix)}-,` :
    '';
  const suffix = fragment.suffix ?
    `,-${encodeURIComponent(fragment.suffix)}` :
    '';
  const start = encodeURIComponent(fragment.textStart);
  const end = fragment.textEnd ?
    `,${encodeURIComponent(fragment.textEnd)}` :
    '';
  url += `#:~:text=${prefix}${start}${end}${suffix}`;
  console.log(url);
}

分析を目的としたテキスト フラグメントの取得

多くのサイトではこのフラグメントをルーティングに使用しています。そのため、ブラウザはテキスト フラグメントを削除して、ページを壊れないようにしています。分析などを目的として、ページへのテキスト フラグメント リンクを公開する必要性が確認されていますが、提案されたソリューションはまだ実装されていません。当面の回避策として、以下のコードを使用して必要な情報を抽出できます。

new URL(performance.getEntries().find(({ type }) => type === 'navigate').name).hash;

セキュリティ

テキスト フラグメント ディレクティブは、ユーザーのアクティベーションの結果であるフル(同一ページ以外)のナビゲーションでのみ呼び出されます。また、デスティネーションと異なるオリジンからナビゲーションが行われる場合は、デスティネーション ページが十分に分離されていることがわかるように、noopener コンテキスト内でナビゲーションを行う必要があります。テキスト フラグメント ディレクティブは、メインフレームにのみ適用されます。つまり、iframe 内でテキストは検索されず、iframe ナビゲーションではテキスト フラグメントは呼び出されません。

プライバシー

テキスト フラグメントの仕様を実装する際は、ページ上でテキスト フラグメントが検出されたかどうかに関係なく、情報が漏洩しないことが重要です。要素フラグメントは元のページ作成者が完全に管理しますが、テキスト フラグメントは誰でも作成できます。上記の例では、<h1>id がないため、ウェブ ワーカーの ECMAScript モジュールという見出しにリンクする方法がなかったことを思い出してください。ただし、テキスト フラグメントを慎重に作成することで、私を含め、誰でも任意の場所にリンクできるようになりました。

悪質な広告ネットワーク evil-ads.example.com を運営しているとします。さらに、ある広告 iframe で、ユーザーが広告を操作すると、テキスト フラグメント URL を使用して、非表示のクロスオリジン iframe を dating.example.com に動的に作成したとします。 dating.example.com#:~:text=Log%20Out 「Log Out」というテキストが見つかった場合は、被害者が現在 dating.example.com にログインしているため、ユーザー プロファイリングに使用できます。単純な Text Fragment の実装では、一致が成功するとフォーカスの切り替えが発生すると判断する可能性があるため、evil-ads.example.comblur イベントをリッスンすることで、一致が発生したタイミングを知ることができます。Chrome では、上記のシナリオを回避する方法でテキスト フラグメントを実装しています。

また、スクロール位置に基づいてネットワーク トラフィックを悪用する攻撃もあります。たとえば、会社のイントラネットの管理者として、被害者のネットワーク トラフィック ログにアクセスできるとします。たとえば、「... に苦しむ場合の対処法」という長い人事資料があり、次に燃え尽き症候群や不安などの����������������あ����します。リストの各アイテムの横にトラッキング ピクセルを配置します。ドキュメントの読み込みが、たとえば「燃え尽き症候群」項目の隣にあるトラッキング ピクセルの読み込みと同時に発生することが判明した場合、イントラネット管理者は、従業員が機密情報であり誰にも表示されないと考えていた :~:text=burn%20out のテキスト フラグメント リンクを従業員がクリックしたかどうかを判断できます。この例は多少の工夫が必要です。また、このサンプルの悪用には非常に特殊な前提条件を満たす必要があるため、Chrome セキュリティ チームは、ナビゲーションのスクロールを実装するリスクを管理しやすいと評価しました。他のユーザー エージェントは、代わりに手動スクロール UI 要素を表示することもできます。

オプトアウトを希望するサイトに対して、Chromium ではドキュメント ポリシー ヘッダーの値をサポートしています。これにより、ユーザー エージェントはテキスト フラグメント URL を処理しなくなります。

Document-Policy: force-load-at-top

テキスト フラグメントを無効にする

この機能を無効にする最も簡単な方法は、HTTP レスポンス ヘッダーを挿入できる拡張機能(Google プロダクトではなく ModHeader など)を使用して、次のようにレスポンス(リクエストではない)ヘッダーを挿入することです。

Document-Policy: force-load-at-top

より複雑なオプトアウトのもう 1 つの方法は、エンタープライズ設定 ScrollToTextFragmentEnabled を使用することです。macOS でこれを行うには、以下のコマンドをターミナルに貼り付けます。

defaults write com.google.Chrome ScrollToTextFragmentEnabled -bool false

Windows の場合は、Google Chrome Enterprise ヘルプのサポートサイトのドキュメントをご覧ください。

検索によっては、検索エンジンの Google が、関連するウェブサイトのコンテンツ スニペットを使用して、簡単な回答や要約を提供します。これらの強調スニペットは、検索が質問の形式で表示される可能性が最も高いです。ユーザーが強調スニペットをクリックすると、ソースのウェブページにある強調スニペット テキストに直接移動します。これは、自動的に作成されるテキスト フラグメント URL のおかげです。

強調スニペットが表示されている Google 検索エンジンの検索結果ページ。ステータスバーにテキスト フラグメント URL が表示されます。
クリックすると、ページの該当セクションが表示されます。

おわりに

テキスト フラグメント URL は、ウェブページ上の任意のテキストにリンクできる強力な機能です。学術コミュニティはこれを使用して、非常に正確な引用リンクや参照リンクを提供できます。検索エンジンはこれを使用して、ページ上のテキスト検索結果へのディープリンクを作成できます。ソーシャル ネットワーク サイトでは、ユーザーがアクセスできないスクリーンショットではなく、ウェブページの特定の部分を共有できるようにします。テキスト フラグメント URL を使用して、私と同じように役立つことを願っています。必ず Link to Text Fragment ブラウザ拡張機能をインストールしてください。

謝辞

Text Fragments は、Nick BurrisDavid BokanGrant Wang の協力を得て実装および指定しました。この記事の執筆を徹底してくれた Joe Medley に感謝します。ヒーロー画像(作成者: Greg RakozyUnsplash