SVG 要素のリンク処理を正しくハンドリングする修正

hotwired/turbo

SVG 要素内のアンカータグが href を文字列として扱えない問題を解消し、同一ページへのハッシュリンクやプリフェッチ判定が正しく機能するようになりました。

背景

Turbo のリンク判定ロジックは HTMLAnchorElement.href.startsWith('#') でハッシュリンクを除外していましたが、SVG のアンカー (SVGAElement) は hrefSVGAnimatedString として提供し、文字列メソッドが存在しません。そのため、SVG 内のハッシュリンクでクリックやホバーした際に例外が発生し、期待通りに動作しないケースが報告されていました(#1510)。

この欠陥は、SVG リンクが実装対象外であったことに起因し、SPA のナビゲーションやプリフェッチ機能が SVG 内でも一貫して動作しないという UX の不整合を招いていました。

技術的な変更

src/util.jsgetLinkHrefString ヘルパーを追加し、HTMLAnchorElementSVGAElement の両方から属性値を安全に取得できるようにしました。実装は link.getAttribute('href') ?? link.getAttribute('xlink:href') ?? '' というシンプルなフォールバックです。

/**
 * Returns consistently the href attribute value as a string for both HTMLAnchorElement and SVGAElement.
 * HTMLAnchorElement href property returns an absolute URL if the attribute contains a valid relative URL.
 * SVGAElement exposes href as SVGAnimatedString which does not implement String methods.
 * getAttribute() will return the proper value of the attribute in both cases.
 */
export function getLinkHrefString(link) {
  return link.getAttribute("href") ?? link.getAttribute("xlink:href") ?? ""
}

findLinkFromClickTarget では従来 link.href.startsWith('#') を使用していた箇所を getLinkHrefString(link).startsWith('#') に置き換え、SVG のハッシュリンクでも安全に判定できるようにしました。

- if (link.href.startsWith("#")) return null
+ if (getLinkHrefString(link).startsWith("#")) return null

src/observers/link_prefetch_observer.js でも同様に linkToTheSamePage の比較ロジックを getLinkHrefString(link).startsWith('#') に変更し、プリフェッチ対象判定を統一しました。

- return (link.pathname + link.search === document.location.pathname + document.location.search) || link.href.startsWith("#")
+ return (link.pathname + link.search === document.location.pathname + document.location.search) || getLinkHrefString(link).startsWith("#")

テストケースは SVG 内のハッシュリンク同一オリジンの SVG リンク を新たに追加し、クリックやホバー時に期待通りにページ遷移・プリフェッチが行われないことを検証しています。これにより、SVG が関与するシナリオでも既存の挙動が保たれることを保証しました。

設計判断

SVG の href 取得を専用ヘルパー getLinkHrefString に抽象化した点が本修正の中心です。この選択は 「同一インタフェースで属性取得を統一」 という設計方針に沿っており、将来的に他の属性(例: xlink:href)が追加された場合でもヘルパーを拡張すれば既存ロジックを改変せずに対応できます。

また、findLinkFromClickTargetlink_prefetch_observer の変更は、最小限の差分 に抑えることで既存コードの動作保証と後方互換性を維持しています。新しいヘルパー導入以外のロジック変更は行っていないため、既存のテストスイートへの影響は極めて低くなっています。

まとめ

SVG 要素の href が文字列メソッドを持たない問題を getLinkHrefString で統一的に処理し、リンク判定とプリフェッチロジックを修正しました。この変更により、SVG 内のハッシュリンクや同一オリジンリンクでも例外が発生せず、Turbo のナビゲーション体験が一貫化されます。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
d4005f0c

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文・背景・技術的な変更・設計判断・まとめが揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックのファイル名付きハイライトは正しい形式です。しかし PR 番号のリンクが `[PR #1518](URL)` となっており、要求された `[#1518](URL)` 形式になっていません。その他の GitHub リンク記法も不完全です。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

エンジニア向けの専門的な記述で、過度な初心者向け解説はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論・各論・結論のパラグラフ構成になっており、段落はトピックセンテンスで始まり、1段落1トピック・6文以内に収まっています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のコードブロックは Diff に記載された変更と一致しており、ファイル名・差分内容ともに正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

SVGAElement、SVGAnimatedString、getAttribute などの用語が正しく使用され、PR の記述と合致しています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な主張は PR の目的・実装に裏付けられており、誤りは見当たりません。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

全ての事実は PR タイトル、説明、Diff に基づいており、外部情報や憶測は含まれていません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号 (#1518) と Issue 番号 (#1510) が正しく記載されています。数値的な誤りはありません。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR の内容(SVG 要素のリンク処理修正)を的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

バージョンサポートやリリース日程など、PR に記載されていない外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の歪曲はなく、PR の記述と一致しています。