WWDC Quick Look 💓 By SwiftGGTeam
What's new in Safari extensions

What's new in Safari extensions

元の動画を見る

ハイライト

Safari 17 は Manifest v2/v3 を引き続きサポートし、Web 拡張は xrOS までカバー。コンテンツブロッカーに :has() セレクタ、Declarative Net Request にリクエストヘッダー変更、App 拡張にサイト別権限、Private Browsing と Profiles での拡張挙動の制御が強化されました。

主要内容

クロスプラットフォーム Web 拡張

Safari 拡張には 4 種類あります。Content Blockers、Share Extensions、App Extensions、Web Extensions です。Safari 17 も 4 種類すべてをサポートしますが、Apple は Web Extensions をブラウザカスタマイズの未来と位置づけています。

Apple は W3C WebExtensions Community Group の共同議長を務め、他ブラウザベンダーと標準化を推進しています。1 つの Web Extension コードが iOS、iPadOS、macOS、xrOS で動作します。

xrOS の Web Extensions は iOS 版と同等の能力(スクリプト注入、バックグラウンドコンテンツ、ポップオーバー)を持ちます。

00:45

Safari 17 は Manifest v2 と v3 の両方をサポートしますが、新機能は v3 に追加され続けます。適切なタイミングで v3 へ移行してください。

コンテンツブロッカーの強化

コンテンツブロッカーは JSON ルールで宣言的にコンテンツをブロックまたは非表示にし、ユーザーが訪問したサイト情報へのアクセスは不要です。今年 :has() セレクタが追加されました。

:has() により子要素に基づいて親要素をマッチできます。例えば .paid-promo 子要素を含む .post を非表示にするなど、以前は純粋な CSS セレクタでは難しかった処理が可能です。

02:50

Declarative Net Request の更新

Declarative Net Request は Web 拡張が宣言的にネットワークリクエストをインターセプト・変更できます。Safari がシステムレベルでルールを実行し、省電力とプライバシーを両立します。

Safari 16.4 からリクエストヘッダーの変更(設定、追加、削除、完全除去)に対応しました。

この機能には manifest で declarativeNetRequestWithHostAccess 権限の宣言が必要で、拡張はサイト別の許可を得て初めてルールが有効になります。

03:37

新 API declarativeNetRequest.setExtensionActionOptions で、バッジテキストを操作回数(ブロック数など)の自動表示に設定できます。閲覧履歴を秘匿したまま、拡張の動作をユーザーに示せます。

05:09

スクリプトとストレージ API

registerContentScript 系 API でコンテンツスクリプトをプログラム的に登録・更新・削除できます。manifest の静的宣言を補完し、特定ページや条件に応じて動的注入でき、登録はセッションをまたいで永続化されます。

05:45

Safari 16.4 で session storage 領域が追加されました。local storage と異なりメモリ上に保持され、Safari 終了時に消去されます。復号キーや認証トークンなど機密データに適しています。

06:22

サイト別権限

Safari App Extensions がサイト別許可に対応し、Web 拡張の権限モデルと統一されました。拡張を初めて有効化した時点ではサイトアクセス権はありません。ページアクセスを試みるとツールバーアイコンにプロンプトが表示され、「1 日許可」または「常に許可」を選べます。

許可後はツールバーアイコンが着色表示され、権限はいつでも取り消せます。

旧 Safari からアップグレードするユーザーは既存権限が移行され、「各 Web サイトで確認」でプライバシーを強化するバナーも表示されます。

07:24

すべての拡張はデフォルトでツールバーアイコンを表示します。正しい着色のため PDF ベクターアイコンの提供を推奨します。

Private Browsing と Profiles

Safari 17 では、Private Browsing ウィンドウにアクセスできる拡張を個別に制御でき、他の閲覧シーンで拡張を無効にする必要はありません。

スクリプト注入やページ読み取りを行う拡張は Private Browsing ではデフォルトでオフ。ページ内容にアクセスしないコンテンツブロッカーなどは自動許可されます。

Profiles は今年 Safari の新機能で、履歴、Cookie、サイトデータを分離します。各 Profile に独立した拡張設定があり、Profile 内で有効化すると新しい UUID、バックグラウンドページ、ストレージを持つ別インスタンスになります。サイト別権限は Profile 間で共有されます。

09:02

ネイティブホスト App と通信する拡張は、複数 Profile からのメッセージを処理する必要があります。beginRequest(with context:)userInfo をデコードし、Profile 実行時は SFExtensionProfileKeyprofileIdentifier を確認します。

Safari 17 の Develop メニューでは、Web Extension のバックグラウンドコンテンツが拡張ごとにグループ化され、各 Profile の inspect 可能なバックグラウンドページと service worker が一覧表示されます。

詳細

コンテンツブロッカーの :has() ルール

[
  {
    "action": {
      "type": "hide"
    },
    "trigger": {
      "url-filter": ".*",
      "if-domain": ["example.com"],
      "selector": ".post:has(.paid-promo)"
    }
  }
]

キーポイント:

  • "type": "hide" はネットワークをブロックせず要素を非表示
  • "url-filter": ".*" はすべての URL にマッチ
  • "if-domain" で指定ドメインに限定
  • "selector": ".post:has(.paid-promo)".paid-promo 子要素を含む .post にマッチ
  • :has() はネストや他セレクタとの組み合わせをサポート

Declarative Net Request によるヘッダー変更

manifest.json で権限を宣言:

{
  "manifest_version": 3,
  "permissions": [
    "declarativeNetRequest",
    "declarativeNetRequestWithHostAccess"
  ],
  "host_permissions": [
    "*://example.com/*"
  ]
}

キーポイント:

  • ヘッダー変更とリダイレクトには declarativeNetRequestWithHostAccess が必要
  • host_permissions でアクセス可能ドメインを宣言
  • ユーザーがサイト別許可を与えて初めてルールが有効

rules.json のルール定義:

[
  {
    "id": 1,
    "priority": 1,
    "action": {
      "type": "modifyHeaders",
      "requestHeaders": [
        {
          "header": "User-Agent",
          "operation": "set",
          "value": "MyExtension/1.0"
        },
        {
          "header": "X-Custom-Header",
          "operation": "append",
          "value": "extra-value"
        },
        {
          "header": "X-Tracking-Id",
          "operation": "remove"
        }
      ]
    },
    "condition": {
      "urlFilter": "||example.com",
      "resourceTypes": ["main_frame"]
    }
  }
]

キーポイント:

  • "operation": "set" は既存値を置換
  • "operation": "append" は既存値に追記
  • "operation": "remove" はヘッダーを完全削除
  • "resourceTypes" で適用リソース種別を限定

バッジカウント設定:

// Safari 16.4+
chrome.declarativeNetRequest.setExtensionActionOptions({
  displayActionCountAsBadgeText: true
});

キーポイント:

  • displayActionCountAsBadgeText: true で操作回数をバッジに自動表示
  • 現時点ではこの API の唯一のオプション
  • ルール発火回数に基づき自動更新

プログラム的コンテンツスクリプト登録

await chrome.scripting.registerContentScripts([
  {
    id: "webkit-highlight",
    matches: ["https://webkit.org/*"],
    js: ["highlight.js"],
    runAt: "document_idle",
    persistAcrossSessions: true
  }
]);

await chrome.scripting.updateContentScripts([
  {
    id: "webkit-highlight",
    matches: ["https://webkit.org/*", "https://bugs.webkit.org/*"]
  }
]);

await chrome.scripting.unregisterContentScripts({ ids: ["webkit-highlight"] });

キーポイント:

  • registerContentScripts で動的登録
  • persistAcrossSessions: true でセッション跨ぎ永続化
  • updateContentScripts で既存設定を変更
  • unregisterContentScripts で ID 指定削除
  • manifest の静的 content_scripts を補完

Session Storage の利用

await chrome.storage.session.set({
  authToken: "eyJhbGciOiJIUzI1NiIs...",
  decryptionKey: "base64-encoded-key"
});

const data = await chrome.storage.session.get(["authToken"]);
console.log(data.authToken);

await chrome.storage.local.set({ settings: { theme: "dark" } });
await chrome.storage.session.set({ tempKey: "value" });

キーポイント:

  • chrome.storage.session は Safari 16.4 で追加
  • メモリ上に保持し、ディスクには書き込まない
  • Safari 終了時に自動消去
  • 認証トークンや復号キーなど機密データ向け
  • local/sync と同じ API

SVG アイコン設定

{
  "manifest_version": 3,
  "icons": {
    "16": "icon.svg",
    "32": "icon.svg",
    "48": "icon.svg",
    "128": "icon.svg"
  }
}

キーポイント:

  • Safari 16.4 以降 SVG アイコンをサポート
  • 1 つの SVG で全サイズの PNG を代替可能
  • Safari がスケーリングを自動処理
  • 正しい着色のためベクターアイコンを推奨

Profile 対応メッセージ処理

import SafariServices

class ExtensionRequestHandler: NSObject, NSExtensionRequestHandling {
    func beginRequest(with context: NSExtensionContext) {
        guard let item = context.inputItems.first as? NSExtensionItem,
              let userInfo = item.userInfo else { return }
        
        if let profileId = userInfo[SFExtensionProfileKey] as? String {
            handleRequest(forProfile: profileId, context: context)
        } else {
            handleRequest(context: context)
        }
    }
}

キー�ポイント:

  • SFExtensionProfileKey で実行中 Profile を識別
  • profileIdentifier は各 Profile の一意 ID
  • Profile ごとに独立した UUID、バックグラウンドページ、ストレージ
  • ネイティブホスト App は profileIdentifier でデータを分離

重要ポイント

1. :has() セレクタで精密なコンテンツブロック

  • やること:コンテンツブロッカールールで :has() を使い、URL だけでなくページ構造に基づいて非表示
  • 価値:「広告ラベル付き記事カード」など複雑なケースを誤ブロック少なく実現
  • 始め方:対象サイトの DOM を分析し、:has() ルールを作成・テスト

2. Declarative Net Request でリクエストヘッダー管理

  • やること:User-Agent 統一、トラッキングヘッダー削除など HTTP ヘッダーを宣言的に変更
  • 価値:全リクエストを傍受して変更するより省電力で、ページ内容アクセスも不要
  • 始め方:manifest v3 で declarativeNetRequestWithHostAccess を宣言し、modifyHeaders ルールを記述、サイト別許可フローをテスト

3. 機密データを localStorage から sessionStorage へ

  • やること:認証トークンや一時復号キーを localStorage から sessionStorage へ移行
  • 価値:ディスクに書き込まず Safari 終了時に消去、漏洩リスク低減
  • 始め方:機密データの chrome.storage.localchrome.storage.session に置き換え

4. Private Browsing と Profiles に適応

  • やること:Private Browsing と複数 Profile で拡張挙動をテストし、データ分離を確認
  • 価値:仕事用・個人用 Profile を同時利用するユーザーに対応
  • 始め方:Safari 設定で複数 Profile を作成し各 Profile で独立テスト。SFExtensionProfileKey の処理を確認

5. プログラム的スクリプト登録で動的機能

  • やること:ユーザー設定や現在ページに応じてコンテンツスクリプトを動的登録・解除
  • 価値:全ページへの注入を避け、必要時のみ読み込んでオーバーヘッド削減
  • 始め方:オプション画面に機能スイッチを用意し、ON で registerContentScripts、OFF で unregisterContentScripts

関連セッション

コメント

GitHub Issues · utterances