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

テキスト フラグメントを使用すると、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 のハッシュで使用することで、赤いボックスでハイライト表示された要素にディープリンクを配置できます。たとえば、横にある [フィードバックを送信するプロダクト フォーラム] ボックスにディープリンクを設定する場合は、https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1 という URL をハンドクラフトします。デベロッパー ツールの [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 Technology Preview: サポートされています。

ソース

セキュリティ上の理由から、この機能ではリンクを 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

次に、見出しだけでなく、Web Worker の 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、カンマ ,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 番目の出現は緑色のコードフォントで記述されています。この単語にリンクする場合は、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」です。他の 3 つの単語「text」には、周囲に同じ単語がありません。この知識を基に、前の 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 であることを確認するときに手間がかかります。必要に応じて、仕様にはヒントとテキスト フラグメント URL を生成する手順が正確に記載されています。Link to Text Fragment というオープンソースのブラウザ拡張機能を提供しています。この拡張機能を使用すると、任意のテキストを選択して、コンテキスト メニューで [Copy Link to Selected Text] をクリックすると、そのテキストにリンクできます。この拡張機能は、次のブラウザで使用できます。

テキスト フラグメントへのリンク 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 を動的に作成したとします。「Log Out」というテキストが見つかった場合、被害者が現在 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 レスポンス ヘッダーを挿入できる拡張機能を使用することです。たとえば、(Google プロダクトではなく)ModHeader を挿入し、次のようにレスポンス(リクエストではない)ヘッダーを挿入します。

Document-Policy: force-load-at-top

より複雑なオプトアウト方法としては、エンタープライズ設定 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 によるヒーロー画像。