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

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

Chrome 80 は大きなリリースでした。Web ワーカーの ECMAScript モジュールnullish coalescingoptional chaining など、多くの期待されている機能が含まれていました。リリースは、いつもどおり 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] パネルに示すように、問題の要素には id 属性があり、値は HTML1 です。

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

JavaScript の URL() コンストラクタでこの URL を解析すると、さまざまなコンポーネントが明らかになります。値が #HTML1hash プロパティに注目してください。

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 のないものにリンクしたい場合はどうすればよいですか?たとえば、Web Worker の ECMAScript モジュールの見出しにリンクするとします。以下のスクリーンショットからわかるように、問題の <h1> には id 属性がありません。つまり、この見出しにリンクすることはできません。これが、テキスト フラグメントが解決する問題です。

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

テキスト フラグメント

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

ブラウザの互換性

対応ブラウザ

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

ソース

セキュリティ上の理由から、この機能ではリンクを 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 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 などの対応ブラウザでクリックすると、セクション全体がハイライト表示され、スクロールして表示されます。

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

startend を選択した理由について説明します。実際、両側に 2 つの単語しかない、少し短い URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules,Web%20Workers. でも問題ありません。startend を以前の値と比較します。

さらに一歩進んで、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

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

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

上のスクリーンショットでは、「text」という単語が 4 回表示されています。4 番目の出現は緑色の Code フォントで記述されています。この特定の単語にリンクする場合は、starttext に設定します。「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-startend-suffix のそれぞれは、単一のブロックレベル要素内のテキストにのみ一致しますが、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 を生成する手順をご確認ください。Google では、テキスト フラグメントへのリンクというオープンソースのブラウザ拡張機能を提供しています。この拡張機能を使用すると、任意のテキストを選択して、コンテキスト メニューの [選択したテキストへのリンクをコピー] をクリックすることで、そのテキストにリンクできます。この拡張機能は、次のブラウザで使用できます。

テキスト フラグメントへのリンク Chrome 拡張機能。

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;
}

ポリフィルの適用

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

ポリフィルには、インポートしてテキスト フラグメント リンクの生成に使用できるファイル 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 の 1 つで、ユーザーが広告を操作すると、テキスト フラグメント URL dating.example.com#:~:text=Log%20Out を使用して dating.example.com への非公開のクロスオリジン iframe が動的に作成されたとします。「ログアウト」というテキストが見つかった場合、被害者が現在 dating.example.com にログインしていることがわかります。これはユーザー プロファイリングに使用できます。単純な Text Fragments の実装では、一致が成功するとフォーカスが切り替わるように判断される可能性があるため、evil-ads.example.com では blur イベントをリッスンして、一致が発生したタイミングを把握できます。Chrome では、上記のシナリオが発生しないように Text Fragment を実装しています。

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

オプトアウトを希望するサイトの場合、Chromium は ドキュメント ポリシー ヘッダー値をサポートしているため、ユーザー エージェントがテキスト フラグメント 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 ヘルプのサポート サイトのドキュメントに沿って操作します。

一部の検索では、関連するウェブサイトのコンテンツ スニペットを使用して、簡単な回答やサマリーが提供されます。このような強調スニペットが表示される傾向が最も高いのは、検索が質問の形式で入力された場合です。ユーザーが強調スニペットをクリックすると、ソースのウェブページにある強調スニペットのテキストに直接移動します。これは、自動作成されたテキスト フラグメントの URL によって実現されます。

注目のコンテンツ スニペットが表示された Google 検索エンジンの検索結果ページ。ステータスバーにテキスト フラグメントの URL が表示されます。
クリックすると、ページの関連するセクションがスクロールして表示されます。

まとめ

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

謝辞

テキスト フラグメントは、Nick BurrisDavid Bokan によって実装および仕様化されました。Grant Wang も貢献しています。この記事を詳しく確認していただいた Joe Medley に感謝いたします。UnsplashGreg Rakozy によるヒーロー画像。