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

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

フラグメント識別子

Chrome 80 は大きなリリースでした。Web Worker の ECMAScript モジュール、nullish 結合、オプショナル チェーンなど、期待されていた機能が多数含まれていました。このリリースは、通常どおり、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 を手動で作成します。デベロッパー ツールの [Elements] パネルに表示されているように、問題の要素には値が HTML1 の id 属性があります。

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

この URL を JavaScript の 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 Modules in Web Workers という見出しにリンクするとします。次のスクリーンショットに示すように、問題の <h1> には id 属性がないため、この見出しにリンクする方法はありません。この問題を解決するのが、テキスト フラグメントです。

id のない見出しを表示しているデベロッパー ツール。

テキスト フラグメント

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

ブラウザの互換性

Browser Support

  • Chrome: 89.
  • Edge: 89.
  • Firefox: 131.
  • Safari: 18.2.

Source

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

start

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

#:~:text=start

たとえば、Chrome 80 の機能を発表するブログ投稿の「ECMAScript Modules in Web Workers」という見出しにリンクする場合、URL は次のようになります。

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

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

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

start、end

では、見出しだけでなく、ECMAScript Modules in Web Workers というタイトルのセクション全体にリンクする場合はどうすればよいでしょうか。セクションのテキスト全体をパーセント エンコードすると、結果の 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、カンマ、ES%20Modules%20in%20Web%20Workers. を end として指定します。Chrome などの対応ブラウザでクリックすると、セクション全体がハイライト表示され、スクロールして表示されます。

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

start と end の選択について疑問に思われるかもしれません。実際には、各側に 2 つの単語のみを含む、少し短い URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules,Web%20Workers. でも機能します。start と end を以前の値と比較します。

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

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

prefix-、-suffix

start と end に十分な長さの値を使用することは、一意のリンクを取得するための 1 つのソリューションです。ただし、状況によってはこれが不可能な場合もあります。ところで、なぜ Chrome 80 のリリースに関するブログ投稿を例に選んだのでしょうか。その答えは、このリリースでテキスト フラグメントが導入されたことです。

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

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

「Text」が最初に出現する箇所に一致するテキスト フラグメント。

幸いなことに、解決策はすでに存在しています。このような場合は、prefix- と -suffix を指定できます。緑色のコードフォント「text」の前の単語は「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-、start、end、-suffix はそれぞれ、1 つのブロックレベル要素内のテキストにのみ一致しますが、start,end の範囲全体は複数のブロックにまたがることができます。たとえば、次の例では、開始文字列「The quick」が単一の、中断のないブロックレベル要素内に表示されないため、:~: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 は、Link to Text Fragment というオープンソースのブラウザ拡張機能を提供しています。これを使用すると、任意のテキストを選択してコンテキスト メニューの [Copy Link to Selected Text] をクリックすることで、そのテキストにリンクできます。この拡張機能は、次のブラウザで使用できます。

Link to 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.)の両方が含まれています。

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

フラグメント ディレクティブ

構文の要素のうち、まだ説明していないものが 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;
}

ポリフィル

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

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 Modules in Web Workers という見出しにリンクする方法がありませんでした。しかし、テキスト フラグメントを慎重に作成すれば、私を含め誰でもどこにでもリンクできます。

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

別の攻撃として、スクロール位置に基づいてネットワーク トラフィックを悪用することも考えられます。たとえば、企業のイントラネットの管理者として、被害者のネットワーク トラフィック ログにアクセスできるとします。ここで、人事部門の長いドキュメント「...に苦しんでいる場合の対応方法」があり、その中に「燃え尽き症候群」、「不安」などの症状のリストがあるとします。このリストの各項目の横にトラッキング ピクセルを配置できます。ドキュメントの読み込みが、たとえば burn out 項目の横にあるトラッキング ピクセルの読み込みと一時的に同時に発生していると判断した場合、イントラネット管理者は、従業員が :~:text=burn%20out を含むテキスト フラグメント リンクをクリックしたと判断できます。従業員は、このリンクが機密であり、誰にも見えないと想定していた可能性があります。この例は、そもそもやや不自然なものであり、悪用するには非常に特殊な前提条件を満たす必要があるため、Chrome セキュリティ チームは、ナビゲーションでのスクロールを実装するリスクは管理可能であると評価しました。他のユーザー エージェントは、代わりに手動スクロール UI 要素を表示することを決定する場合があります。

オプトアウトを希望するサイト向けに、Chromium は Document Policy ヘッダー値をサポートしています。この値を送信することで、ユーザー エージェントがテキスト フラグメント URL を処理しないようにできます。

Document-Policy: force-load-at-top

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

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

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 ブラウザ拡張機能をぜひインストールしてください。

謝辞

テキスト フラグメントは、Nick Burris 氏と David Bokan 氏によって実装および指定され、Grant Wang 氏が貢献しました。このドキュメントを詳細にレビューしてくれた Joe Medley に感謝します。