View in English

  • Apple Developer
    • 今すぐ始める

    「今すぐ始める」を詳しく見る

    • 概要
    • 学ぶ
    • Apple Developer Program

    最新情報

    • 最新ニュース
    • Hello Developer
    • プラットフォーム

    プラットフォームを詳しく見る

    • Appleプラットフォーム
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store

    特集

    • デザイン
    • 配信
    • ゲーム
    • アクセサリ
    • Web
    • Home
    • CarPlay
    • テクノロジー

    テクノロジーを詳しく見る

    • 概要
    • Xcode
    • Swift
    • SwiftUI

    特集

    • アクセシビリティ
    • App Intent
    • Apple Intelligence
    • ゲーム
    • 機械学習とAI
    • セキュリティ
    • Xcode Cloud
    • コミュニティ

    コミュニティを詳しく見る

    • 概要
    • 「Appleに相談」イベント
    • コミュニティによるイベント
    • デベロッパフォーラム
    • オープンソース

    特集

    • WWDC
    • Swift Student Challenge
    • デベロッパストーリー
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Center
    • ドキュメント

    ドキュメントを詳しく見る

    • ドキュメントライブラリ
    • テクノロジー概要
    • サンプルコード
    • ヒューマンインターフェイスガイドライン
    • ビデオ

    リリースノート

    • 注目のアップデート
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • tvOS
    • Xcode
    • ダウンロード

    ダウンロードを詳しく見る

    • すべてのダウンロード
    • オペレーティングシステム
    • アプリ
    • デザインリソース

    特集

    • Xcode
    • TestFlight
    • フォント
    • SF Symbols
    • Icon Composer
    • サポート

    サポートを詳しく見る

    • 概要
    • ヘルプガイド
    • デベロッパフォーラム
    • フィードバックアシスタント
    • お問い合わせ

    特集

    • アカウントヘルプ
    • App Reviewガイドライン
    • App Store Connectヘルプ
    • 近日導入予定の要件
    • 契約およびガイドライン
    • システムステータス
  • クイックリンク

    • イベント
    • ニュース
    • Forum
    • サンプルコード
    • ビデオ
 

ビデオ

メニューを開く メニューを閉じる
  • コレクション
  • すべてのビデオ
  • 利用方法

その他のビデオ

  • 概要
  • Summary
  • トランスクリプト
  • コード
  • SafariのためのWeb機能拡張の作成

    Safari用のWeb機能拡張をゼロから作成しテストする方法を見てみましょう。Xcodeは不要です。プラットフォームを横断したプライバシー重視の優れたブラウジング体験を、コンテンツブロック、ページ変更、ネイティブメッセージング、許可モデルの連係により実現するシステムについて学びます。

    関連する章

    • 0:00 - Introduction
    • 3:23 - Get started
    • 7:23 - Block content
    • 14:40 - Modify webpages
    • 19:53 - Package and distribute
    • 22:33 - Communicate with your app
    • 26:04 - Next steps

    リソース

    • w3.org — W3C WebExtensions Community Group
    • Packaging and distributing Safari Web Extensions with App Store Connect
    • WebKit.org – Report issues to the WebKit open-source project
    • Submit feedback
    • MDN Web Docs - Web Extensions API
      • HDビデオ
      • SDビデオ

    関連ビデオ

    WWDC26

    • Safari 27用WebKitの新機能
  • このビデオを検索

    こんにちは! KiaraですSafariチームの エンジニアです。 Safariに追加したい機能の アイデアがあり そのアイデアを実現したいなら このセッションが役立ちます。 SafariのWeb拡張機能を 構築して配布するために 必要なことをすべて解説します。 内容が多いので 休憩したり 役立つセクションに スキップしながら進めてください。 SafariのWeb拡張機能は アプリにパッケージされています。 App Storeで入手できる お気に入りのコンテンツの1つです。 広告のブロックや カスタム新規タブページの作成 お気に入りのストリーミングサイトでの 再生体験の向上など 小さなツールですが Web体験を 大きく改善できます。 AppleはW3C Web Extensions Working Groupにて他のブラウザと ブラウザ間のWeb拡張機能構築に 使うAPIの標準化を進めています。 別のブラウザ向けの 拡張機能を作成済みなら その拡張機能をSafariに 移行できます。 パッケージングと配布のセクションへ ジャンプしてください App Store Connectを使って 拡張機能を配布する方法を紹介します。 今日はWeb拡張機能を ゼロから構築して すべての基礎を網羅します。 拡張機能の開発プロセスを 解説しながら Safariにカスタマイズ体験を もたらすAPIと機能を紹介します。 SafariでのWeb拡張機能の テスト方法と TestFlightでベータ版を ユーザーと共有する方法も紹介します。 世界に公開する準備ができたら 拡張機能を App Storeに申請します。 まずはこのセッションの サンプルコードをダウンロードします。 今日構築する拡張機能の すべてのリソースが含まれています。 ぜひ一緒に進めてみてください。 このセッションでは 実際の拡張機能の構築を コンテンツをブロックし Webページを変更するものを通じて 拡張機能をパッケージする いくつかの方法を紹介します App Storeへの配布方法も 説明します。 拡張機能の機能をさらに 拡張する方法として 拡張機能をそれを含む アプリと連携させる方法も示します。 最終的に この拡張機能は iOS、iPadOS、macOS そしてvisionOSのSafariで 同時に動作します。 Web拡張機能の優れた点は すべてが HTML、CSS、JavaScript で構成されていることです。 Web開発の経験があれば 必要な知識のほとんどは すでに身に付いています。 今日のセッションでは ブラウジング中に 気が散るサイトをブロックする 拡張機能を構築します。 ブラウジング中は つい引き込まれてしまうことがあります。 webkit.orgを例にとると そこには何百もの記事があり 何時間でも読み続けてしまいます WebKitの新情報を 読んでいるだけで終わります。 だからこういう拡張機能が 必要なのです。 今日これを構築します 2つのブロックモードがあります: サイトで最大10分の 閲覧を許可するLightモードと WebKitの記事を数本 読むのに最適なモードです そしてFullモードは ユーザーが移動しようとした 瞬間にリダイレクトします。

    はじめに お気に入りの コードエディタを開きます。 Xcodeも使えますが 拡張機能の構築はどのエディタでも可能です。 コードエディタでこの拡張機能の ファイルを格納するフォルダを作成します。 基礎として すべての拡張機能に 必要な最初のファイルはmanifestです。 manifestはJSON形式のファイルで ブラウザに拡張機能の情報を伝えます そして機能についても ブラウザに伝えます。 manifestは拡張機能の IDカードのようなものです。 拡張機能の名前のような 情報が含まれます 説明やバージョン番号なども 含まれます。 次に 拡張機能のアイコンを 格納するimagesフォルダを追加します。 アイコンはさまざまな場所に 表示されます。 ツールバーなど… または拡張機能の設定などです。 表示される場所によって 異なるサイズが必要です。 そのためアイコンはsvgで追加します。 Safariがアイコンのスケーリングを 完璧に処理してくれるので コードエディタに戻って 実際の動作を確認できます。 manifestファイルに 拡張機能のアイコンを追加しました。 アイコンはimagesフォルダ内に 格納されています。 確認するために変更を保存して Safariを開きます 拡張機能を読み込みます。 Safariへの拡張機能の 読み込みはとても簡単です。 Command+コンマでSafariの 設定を開くだけです ペインをクリックして に チェックを入れます。 これでデベロッパペインが 有効になります そこから一時的な拡張機能を 追加できます。 コード署名が検証されていない 拡張機能なので 署名なし拡張機能を 許可する必要があります。 許可したら 拡張機能の リソースを含むフォルダを選択します。 これだけで拡張機能が Safariに読み込まれました! ほとんどの拡張機能には ユーザーが操作するUIがあります。 この拡張機能では ユーザーが 気が散るサイトをブロックリストに 追加できるようにしたいです。 そのためカスタムUIを 追加する必要があります。 これを実現する方法は いくつかあります。 1つは拡張機能の アクションボタンを使う方法です。 Safariのツールバーに追加される 拡張機能用のボタンです。 クリックすると Safariは定義した UIのポップアップを表示します。 ポップアップのファイル名や manifestで定義するリソースは 正しいmanifestキーに 関連付けていればどんな名前でも構いません Safariがその役割と 使い方を認識できるようにするためです。 この例では デフォルトポップアップの 読み込みに使うファイルは"popup.html"です。 この拡張機能では ポップアップにブロックリストを表示すると UIが少し窮屈に見えてしまいます そこで代わりに拡張機能の オプションページを使って表示します。 これはユーザーが拡張機能の 設定を行うフルページです。 ではコードエディタに戻って この変更を加えます。 manifestにオプションページが 定義されています。 拡張機能のフォルダに そのファイルを追加します。 このページのフルUIを 追加する前に Hello Worldのような シンプルなものから始めます。 Safariでは拡張機能を 再読み込みすることで変更をテストできます。 いいですね! 新しい変更が反映され 拡張機能にこの設定ボタンが表示されました。 ボタンをクリックすると 拡張機能のオプションページが開きます! 設定が完了したので 拡張機能にもう少し機能を追加する必要があります "Hello World"を表示するだけでは 実用的ではありません。 そこでLightモードと Full modeを切り替えられる ページを設計しました。 このインターフェースはすでに HTML、CSS、JavaScriptで記述しています。 見た目もよくインタラクティブですが 接続作業が必要です。

    では拡張機能をアップグレードして コンテンツのブロックを開始する方法を見ていきます。 declarative net request APIを使います この APIを使うと拡張機能が ネットワークリクエストを ブロック、変更、またはリダイレクトできます。 このAPIはお気に入りのWeb拡張機能を 支える技術です。 この機能により 拡張機能は 広告などのWebコンテンツをフィルタリングし ブラウジング中に ユーザーを追跡するトラッカーもブロックできます。 ただしこの機能に アクセスするためには 権限を追加する必要があります。 権限は拡張機能が Safariに必要なアクセスを伝える仕組みです。 クッキーへのアクセスなど さまざまなものがあります ストレージへのデータ保存や クリップボードへの書き込みなどです。 コンテンツブロッキングには declarative net request権限を追加する必要があり 拡張機能のmanifestで 設定できます。 これで準備が整い ルールの定義を始められます。 ルールにはID、優先度 そして条件が満たされたときの アクションタイプがあります。 このルールは例として webkit.orgへのすべてのナビゲーションをブロックします。 ルールの定義方法は2つあります。 1つはmanifestで 定義する方法です。 これは静的ルールと呼ばれます。 使用するルールが あらかじめわかっている場合に最適です。 柔軟性が必要な場合は 実行時にJavaScriptを使って 動的にルールを追加できます。 動的なアプローチを選択するのは ブロックするサイトがわからないためです ユーザーがリストに追加するまで。 このロジックはutilitiesフォルダ内の rules.jsというファイルに記述します。 そしてhost.jsファイルを使って ルールを作成します ユーザーがブロックリストに サイトを追加したときに。 コードに戻って これを接続していきます。 拡張機能のmanifestに declarative net request権限を追加しました。 また前のオプションページを HTML、CSS、JavaScriptファイルに置き換えました 拡張機能用にすでに 作成したファイルです。 2つの新しいファイルを含む utilitiesフォルダも追加しました。 ルールを追加するために rules.jsファイルを開きます。 これらのルールにはIDが必要なため ヘルパーメソッドを追加しました サイトのホストを 一意の整数IDにマッピングするためです。 ID、タイプに"block" そしてurlFilterを指定するルールを作成します サイトのホストに マッチするようにします。 そしてdeclarative net request updateDynamicRules APIを使って 拡張機能にルールを追加します。 hostファイルでは サイトがリストに追加されたとき 拡張機能が フルブロッキングモードの場合に ルールを追加できます。 Safariに戻って 拡張機能を再読み込みして更新します。

    オプションページで webkit.orgをリストに追加します。

    サイトにアクセスすると ブロックされました! 拡張機能はリストに追加された サイトへのナビゲーションをブロックできるようになりました。 ただし 表示されるエラーページは あまり好ましくありません。 ユーザーをもっと意図的な場所 つまり拡張機能用に設計した カスタムページに誘導したいです。 そこでリダイレクトルールの 出番です。 このルールは前のブロックルールに似ていますが タイプがredirectになります ユーザーが到達するページの extensionPathを指定できます。 ただし変更前に 拡張機能のホスト権限を追加する必要があります。 ネットワークリクエストをブロックするには 拡張機能はページへのアクセスが不要です。 しかしネットワークリクエストを リダイレクトするにはアクセスが必要です。 そのため拡張機能のmanifestで declarativeNetRequestWithHostAccessを使います 代わりの権限として。 拡張機能は起動時に どのサイトへのアクセスも必要ないため オプションのホスト権限を使って 実行時にサイトへのアクセスをリクエストできます。 ホスト権限は拡張機能がアクセスしたい サイトをSafariに伝えます。 マッチパターンの配列として 設定できます 各パターンはスキーム、ホスト パスで構成されます。 リストにはどんなサイトでも 追加できるため すべてのURLに マッチするパターンを使います。 拡張機能が動作するために 特定サイトへのアクセスが必要な場合は ホスト権限を使うこともできます。 ただし拡張機能は 自動的にサイトへのアクセスを得るわけではありません。 ユーザーのプライバシーを尊重するために 拡張機能の権限モデルを設計しました。 ユーザーのブラウジング体験は 個人データを露出する可能性があるため ユーザーがコントロールを持ち 拡張機能がアクセスできるサイトを決定します。 明示的にアクセスをリクエストすると Safariは拡張機能のアクションボタンに バッジを表示します。 ボタンをクリックすると アラートが表示され 拡張機能にページへの アクセスを許可するか確認します。 ユーザーがアクセスを許可すると アイコンが色付きになります 拡張機能がそのページで アクティブであることを示します。 この拡張機能は起動時に どのサイトへのアクセスも必要ないため オプションのホスト権限を 選択しました。 これにより拡張機能が必要なときに 任意のサイトへのアクセスをリクエストできます。 manifestで権限を declarativeNetRequestWithHostAccessに変更しました。 拡張機能は実行時に 任意のサイトへのアクセスをリクエストできます。 ではrulesファイルで リダイレクトルールを作成します。 前のブロックルールに非常に似ていますが 今回はタイプがredirectです。 そしてカスタム拡張機能ページへの パスが含まれています。 ページのリソースも 拡張機能のフォルダに追加しました。 ナビゲーションをブロックする代わりに ユーザーを 拡張機能用に設計したページに リダイレクトします。 拡張機能はサイトへのアクセスが 必要なため permissions.request APIを使って ドメインとサブドメインへのアクセスをリクエストします。 この体験を確認するために Safariで拡張機能を更新します。

    オプションページで webkit.orgを追加します。

    サイトが追加される前に 拡張機能へのアクセス許可を求められます。 いいですね! 期待通りの動作です。 アクセスを許可してページを更新します。

    すばらしい! ナビゲーションがカスタム拡張機能ページに リダイレクトされました。 拡張機能がどんどん 完成に近づいています! 気が散るサイトへのナビゲーションを リダイレクトできるようになりました。 でも正直に言うと お気に入りのサイトを 完全に遮断するのは難しいですよね。 そこでページ上の カウントダウンタイマー付きで 最大10分のブラウジングを 許可するモードを追加します。

    そのためにはコンテンツを 直接ページに注入する方法が必要です。 ここでコンテンツスクリプトの 出番です。 コンテンツスクリプトを使うと 拡張機能がWebページの内容を 読み取り変更できます。 スクリプトは静的にすることができ manifestで直接宣言します ファイルと実行するサイトの マッチパターンを指定します。 対象サイトがあらかじめわかっている 場合に有効です。 ただし私の場合 リストに追加されるまで サイトがわかりません。 そのため registered content scripts APIを使って 動的に追加します。 静的スクリプトと同様に機能しますが 2つの追加フィールドがあります: IDと永続フラグです。 これをtrueに設定すると スクリプトが Safariの再起動後も 残り続けます。 このAPIを使うために manifestにscripting権限を追加し utilitiesフォルダに 新しいscripting.jsファイルを追加します。 ここでコンテンツスクリプトを 定義します。 ID、タイマー用のJavaScriptファイル スタイリング用のCSSを指定します サイトのドメインとサブドメインを カバーするマッチパターンも指定します。 このフラグをtrueに設定します。 そしてregister content scripts APIを使って スクリプトを追加します。 addHostメソッドに戻り ユーザーがブロックリストにサイトを追加したとき そのサイトのタイマーを表示する コンテンツスクリプトを追加します。 拡張機能がフルブロッキングモードの場合 ページが読み込まれる前にリダイレクトが発生するため 常にスクリプトを 登録しておけます。 Lightブロッキングモードを選択した状態で webkit.orgをリストに追加します。 サイトにアクセスすると ページに10分タイマーが表示されます。

    この拡張機能は世界に 公開できる状態にほぼ近づいていますが 最初に修正したいことが あります。 気づいているかもしれませんが 拡張機能を再読み込みするたびに 同じサイトをリストに 再度追加し続ける必要があります。 これはすべての情報を メモリに保存しているためです 拡張機能が再読み込みされると その状態が失われます。

    storage APIを使って このデータを保持できます。 Safariは2種類の ストレージ領域をサポートしています。 セッションストレージは 素早いインメモリ処理に最適です 再起動後に残す必要がない場合に。 ただしブロックリストを 保持したいため ディスクにデータを書き込む ローカルストレージが適切な選択です。

    storage APIを使うために manifestに権限を追加します。 次にutilitiesフォルダに 新しいファイルを追加します。 このファイルに いくつかのヘルパーメソッドを定義します ストレージ内のホストの 更新と取得のためです。 ブロックモードの保存と 取得のためのメソッドも追加します。

    addHostメソッドに戻り ユーザーが新しいサイトを追加したとき ストレージ内のホストリストを 更新できます。 保存されたリストを使って ブロックリストを表示できます。 この変更により ブロックリストは常に 追加されたすべてのサイトの 完全なリストで表示されます! 同様に ユーザーが 2つのモードを切り替えるとき 変更をストレージに保存します 拡張機能が フルブロッキングモードの場合は すべてのサイトの リダイレクトルールを作成できます。 storage APIの利点を確認するために Safariで ブロッキングモードを"Full"に変更して サイトをリストに追加します。

    拡張機能を再読み込みすると 先ほど加えた変更が そのまま残っています! いいですね! storage APIを使うことで 拡張機能の永続性の問題が1つ解決しましたが もう1つ修正が必要な問題があります。 登録されたコンテンツスクリプトは Safariの再起動後も 拡張機能の更新後は残りません。 ユーザーが拡張機能を更新すると コンテンツスクリプトが失われます。 修正するために 拡張機能が 更新されたことを知る方法が必要です ストレージからホストを読み取り スクリプトを再作成するためです。 その場所として最適なのが バックグラウンドページまたはサービスワーカーです。 どちらも同じことができます 拡張機能のライフサイクルの管理など ブラウザイベントのリスンや 拡張機能の各部分間でのメッセージ転送。 Safariは両方をサポートしているので 好みで選べます! DOMにアクセスできるため バックグラウンドページを使います。 バックグラウンドページを追加するために manifestで指定します。 次にファイルを 拡張機能のフォルダに追加します。 ここでonInstalledイベントを 登録します。 これにより拡張機能が 新しいバージョンに更新されたことがわかります。 これが発生したとき ストレージからホストを読み取り コンテンツスクリプトを 再登録します。

    ストレージへの読み書きに 拡張機能を接続したことで 大きな改善が得られました。 拡張機能をApp Storeに 公開する方法を確認する時期です。 1つの方法は App Store Connectを使うことです。 App Store Connectでは拡張機能の アップロード、申請、管理ができます 構築したばかりの場合でも 既存のものをSafariに 移行したい場合でも。 そして最大の利点は? Macを使わずに どのブラウザからでも可能です。 まずdeveloper.apple.comにアクセスして Apple Developer Programに 登録します。 登録後 appstoreconnect.apple.comに アクセスします。 SafariのWeb拡張機能は 含まれるアプリにパッケージする必要があるため App Store Connectを使って このアプリを作成できます。 アプリを作成するときに いくつかの設定が必要です 拡張機能を利用可能にしたい プラットフォームなどです。 iOSとmacOSを選択します これにより拡張機能が iPhone、iPad、Mac そしてApple Vision Proの互換アプリとして 利用可能になります。 アプリの一意のIDである バンドル識別子も設定します。 すべての詳細を追加したら Xcode Cloudのタブに切り替えます Safari Web Extension Packagerまで スクロールして 拡張機能のリソースをアップロードします。 アップロードが完了すると Safari Web Extension Packagerが 数分で拡張機能の パッケージ化を処理してくれます! 完了したら 問題を確認したり 次のステップとして拡張機能をテストできます TestFlightを使って。 TestFlightを使うと ベータビルドを配布し 継続的に改善を加えて ユーザーフィードバックを反映できます 拡張機能をApp Storeに 申請する前に。 申請準備が整ったら タブに移動して 仕上げの作業をします。 拡張機能の動作を示す スクリーンショットなどです。 ユーザーが理解しやすいように 説明文も追加します 拡張機能の機能と 特徴について。 すべての詳細を追加したら ビルドを選択して審査に申請します。 これがApp Store Connectを使って 拡張機能を配布する方法です! このセッションでは 標準的なWebExtension APIと機能を 組み合わせてカスタマイズ可能な ブラウジング体験を構築する方法を紹介しました。 しかし拡張機能は Webプラットフォームを超えることもできます。

    ネイティブメッセージングを使って プラットフォームが提供する機能に アクセスする方法を ご説明します。 ここからはXcodeが必要です。 最も簡単な方法は Safari Web Extension Packagerツールを使うことです。 Terminalでこのコマンドを実行すると Xcodeプロジェクトが作成されて起動します。 アプリとWeb拡張機能が 含まれます。 そこからメッセージの送受信のために 接続します。 これをネイティブメッセージングと呼びます。 3人がメモを渡し合うような 仕組みです。 拡張機能のJavaScriptが 最初に動き出します。 中間のApp Extensionが そのメッセージを受け取り ネイティブアプリに 渡します。 そしてアプリが処理を行い 同じ方法で結果を返します。 このアプリでは ブロックリストへの変更前に 生体認証を要求することで 拡張機能の設定を保護します。 まず拡張機能のmanifestに nativeMessaging権限を追加します 拡張機能のmanifestに。 次に拡張機能のバックグラウンドページに メッセージを送信するメソッドを追加します 拡張機能からネイティブアプリへの。 返ってくるメッセージで 認証が成功したかどうかがわかります。 アプリがメッセージを 受信するために SafariWebExtensionHandlerを 変更する必要があります Packagerツールが作成したファイルに 含まれるクラスです。 優れた点として テンプレートには アプリが拡張機能からの メッセージを受信できる 設定がすでに含まれています。 必要なのは 少し調整するだけです requestBioAuthキーの メッセージを解析するなど。 次にSystem APIを使って 生体認証によるユーザー認証をリクエストします ユーザーが認証すると アプリは同じ方法で結果を Web拡張機能に 送り返します。 Xcodeで拡張機能をビルドして Safariで変更をテストします。 Xcodeで拡張機能へのメッセージ送受信の 変更が反映されています。 Project Navigatorでは プロジェクトにWeb拡張機能の すべてのリソースが含まれています。 Command+Bショートカットを使って 拡張機能をビルドします。 Safariで拡張機能を有効にして オプションページを開き webkit.orgをリストに追加します。

    追加される前に Touch IDでの 認証が求められます。 認証後 サイトがリストに追加されます。 これがネイティブメッセージングを使って アプリと Web拡張機能を 連携させる方法です。 これで拡張機能の 配布準備が整いました。 Xcodeを使って配布します。

    まずアーカイブをビルドします。 以前App Store Connectで ビルドを作成したため このビルド番号が前回のビルドより 1つ上であることを確認します。 Organizerウィンドウから 拡張機能を配布します。 これがSafariのWeb拡張機能を ゼロから作成して配布する方法です ゼロから。 今日は多くのことをカバーしました。 初めて始める場合でも 拡張機能をさらに発展させたい場合でも アイデアを実際のものにする 準備ができたと感じていただければ幸いです。 皆さんの作品を 楽しみにしています。

    まだの方は サンプルコードプロジェクトを ダウンロードして 今日紹介したAPIを 試してみてください。 Web拡張機能のクロスブラウザドキュメントを MDNで確認することで 詳細を学べます。 また フィードバックアシスタントで フィードバックをお寄せください。 またはbugs.webkit.orgに バグを報告してください Safari 27でWeb拡張機能を テストする際に。 このセッションにご参加いただきありがとうございました 素晴らしいWWDCをお過ごしください!

    • 3:44 - Manifest file

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0
      }
    • 4:29 - Adding an extension icon

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          }
      }
    • 5:30 - Adding an action button

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "action": {
              "default_popup": "popup.html"
          }
      }
    • 6:17 - Adding custom UI to your extension

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "options_ui": {
              "page": "options.html"
          }
      }
    • 6:30 - Including the UI in the extension manifest

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          }
      }
    • 6:40 - Hello World

      <!DOCTYPE html>
      <html>
          <body>
          <p>Hello World</p>
          </body>
      </html>
    • 8:18 - Adding declarativeNetRequest permission

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          },
        
          "permissions": [ "declarativeNetRequest" ]
      }
    • 8:22 - Blocking network requests

      // block rule
      {
          id: 1,
          priority: 1,
          action: {
              type: "block"
          },
          condition: {
              urlFilter: "||webkit.org",
              resourceTypes: [ "main_frame" ]
          }
      }
    • 8:41 - Modifying network requests

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          },
        
          "permissions": [ "declarativeNetRequest" ],
      
          "declarativeNetRequest": {
              "rule_resources": [
                  {
                      "id": "ruleset_id",
                      "enabled": true,
                      "path": "rules.json"
                  }
              ]
          }
      }
    • 8:50 - Updating dynamic rules

      await browser.declarativeNetRequest.updateDynamicRules({
          addRules: [ rule ]
      })
    • 9:19 - Wiring up the static declarativeNetRequest rules

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          },
        
          "permissions": [ 
            "declarativeNetRequest" 
          ]
      }
    • 9:40 - Adding block rules dynamically

      // A helper function to map the host to the declarative net request rule ID.
      export function hostToRuleID(host) {
      	let hash = 0;
      	for (let i = 0; i < host.length; i++) {
      		hash = ((hash << 5) + hash) + host.charCodeAt(i);
      		hash |= 0;
      	}
      	return Math.abs(hash) || 1;
      }
      
      function createBlockRule(host) {
      	return {
      		id: hostToRuleID(host),
      		priority: 1,
      		action: {
      			type: "block"
      		},
      		condition: {
      			urlFilter: `||${host}`,
      			resourceTypes: ["main_frame"]
      		}
      	}
      }
      
      export async function createRules(hosts) {
      	try {
      		await browser.declarativeNetRequest.updateDynamicRules({
      			addRules: hosts.map(createBlockRule)
      		})
      	} catch {
      		console.log("Failed to create declarative net request rules")
      	}
      }
    • 10:10 - Handling adding hosts to the settings

      import { createRules, removeAllRules, removeRule } from './rules.js'
      
      export async function addHost(host, blockingMode) {
        if (!host)
          return
        
        if (blockingMode === "full")
          await createRules([host])
      }
    • 10:48 - Redirecting network requests

      {
          id: 1,
          priority: 1,
          action: {
              type: "redirect",
              redirect: {
                  extensionPath: "/blocked.html"
              }
          },
          condition: {
              urlFilter: "||webkit.org",
              resourceTypes: [ "main_frame" ]
          }
      }
    • 11:17 - Declaring optional host permissions

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          },
        
          "permissions": [ "declarativeNetRequestWithHostAccess" ],
          "optional_host_permissions": [ "https://webkit.org/*" ]
      
      }
    • 11:54 - Declaring optional host permissions for all sites

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          },
        
          "permissions": [ "declarativeNetRequestWithHostAccess" ],
          "optional_host_permissions": [ "*://*/*" ]
      
      }
    • 13:12 - Add the redirect rule

      // A helper function to map the host to the declarative net request rule ID.
      export function hostToRuleID(host) {
      	let hash = 0;
      	for (let i = 0; i < host.length; i++) {
      		hash = ((hash << 5) + hash) + host.charCodeAt(i);
      		hash |= 0;
      	}
      	return Math.abs(hash) || 1;
      }
      
      function createBlockRule(host) {
      	return {
      		id: hostToRuleID(host),
      		priority: 1,
      		action: {
      			type: "block"
      		},
      		condition: {
      			urlFilter: `||${host}`,
      			resourceTypes: ["main_frame"]
      		}
      	}
      }
      
      function createRedirectRule(host) {
      	return {
      		id: hostToRuleID(host),
      		priority: 1,
      		action: {
      			type: "redirect",
      			redirect: { extensionPath: "/blocked.html" }
      		},
      		condition: {
      			urlFilter: `||${host}`,
      			resourceTypes: ["main_frame"]
      		}
      	}
      }
      
      export async function createRules(hosts) {
      	try {
      		await browser.declarativeNetRequest.updateDynamicRules({
      			addRules: hosts.map(createRedirectRule)
      		})
      	} catch {
      		console.log("Failed to create declarative net request rules")
      	}
      }
    • 13:42 - Dynamically ask for host permissions

      import { createRules, removeAllRules, removeRule } from './rules.js'
      
      export async function addHost(host, blockingMode) {
        if (!host)
          return
        
        const granted = await browser.permissions.request({
          origins: [`*://${host}/*`, `*://*.${host}/*`]
        })
        if (!granted)
          return
        
        if (blockingMode === "full")
          await createRules([host])
      }
    • 14:55 - Defining content scripts

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          },
      
          "options_ui": {
              "page": "options.html"
          },
        
          "permissions": [ "declarativeNetRequestWithHostAccess" ],
          "optional_host_permissions": [ "*://*/*" ],
        
          "content_scripts": [
              {
                  "js": [ "content.js" ],
                  "css": [ "content.css" ],
                  "matches": [ "*://*.webkit.org/*" ]
              }
          ]
      }
    • 15:13 - Dynamically registering content scripts

      let script = {
          id: "id",
          js: [ "content.js" ],
          css: [ "content.css" ],
          matches: [ "*://*.webkit.org/*" ],
          persistAcrossSessions: true
      }
      
      await browser.scripting.registerContentScripts([ script ])
    • 15:31 - Adding the scripting permission

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "icons": {
              "512": "images/icon.svg"
          },
        
          "options_page": "options.html",
        
          "permissions": [
              "declarativeNetRequestWithHostAccess",
              "scripting"
          ],
        
          "optional_host_permissions": [ "*://*/*" ]
      }
    • 15:41 - Registering content scripts

      // scripting.js
      
      function contentScript(host) {
          return {
              id: `cs-${host}`,
              js: [ "content.js" ],
              css: [ "content.css" ],
              matches: [ `*://${host}/*`, `*://*.${host}/*` ],
              persistAcrossSessions: true
          }
      }
      
      export function registerScripts(hosts) {
          const scripts = hosts.map(contentScript)
          try {
              await browser.scripting.registerContentScripts(scripts)
          } catch {
              console.log("Failed to register content scripts")
          }
      }
    • 16:02 - Adding a host

      // host.js
      
      export async function addHost(host, blockMode) {
          if (!host)
              return
      
          const granted = await browser.permissions.request({
              origins: [`*://${host}/*`, `*://*.${host}/*`]
          })
      
          if (!granted)
              return
      
          if (blockingMode === "full")
              await createRules([ host ])
      
          await registerScripts([ host ])
      }
    • 17:06 - Web extensions storage APIs

      await browser.session.storage.set({
        key: value
      })
      
      await browser.local.storage.set({
        key: value
      })
    • 17:21 - Adding storage permission to the web extension manifest.json

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "icons": {
              "512": "images/icon.svg"
          },
        
          "options_page": "options.html",
        
          "permissions": [
              "declarativeNetRequestWithHostAccess",
              "scripting",
              "storage"
          ],
        
          "optional_host_permissions": [ "*://*/*" ]
      }
    • 17:30 - Saving data with storage

      // storage.js
      
      export async function updateHosts(hosts) {
          await browser.storage.local.set({ hosts: hosts })
      }
      
      export async function getHosts() {
          const { hosts = [] } = await browser.storage.local.get("hosts")
          return hosts
      }
      
      export async function saveBlockMode(mode) {
          await browser.storage.local.set({ blockMode: mode })
      }
      
      export async function getBlockMode() {
          const { blockMode = "full" } = await browser.storage.local.get("blockMode")
          return blockMode
      }
    • 17:41 - Persisting hosts to storage

      // host.js
      
      export async function addHost(host, blockMode) {
          if (!host)
              return
      
          const granted = await browser.permissions.request({
              origins: [`*://${host}/*`, `*://*.${host}/*`]
          })
      
          if (!granted)
              return
      
          if (blockingMode === "full")
              await createRules([ host ])
      
          await registerScripts([ host ])
      
          let existingHosts = await getHosts()
          let updatedHosts = [ ...existingHosts, host ]
          await updateHosts(updatedHosts)
      }
    • 17:51 - Reading from storage

      // options.js
      
      let existingHosts = await getHosts()
      let blockMode = await getBlockMode()
      
      displayBlocklist(existingHosts)
    • 18:00 - Switching block modes

      // host.js
      
      export async function userDidSwitchMode(blockMode) {
          await saveBlockMode(blockMode)
      
          if (blockMode === "full") {
              let hosts = await getHosts()
              await createRules(hosts)
          } else
              await removeAllRules()
      }
    • 19:01 - Adding a background script

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "icons": {
              "512": "images/icon.svg"
          },
        
          "options_page": "options.html",
        
          "permissions": [
              "declarativeNetRequestWithHostAccess",
              "scripting",
              "storage"
          ],
        
          "optional_host_permissions": [ "*://*/*" ],
        
          "background": {
              "scripts": [ "background.js" ],
              "type": "module"
          }
      }
    • 19:39 - Background script

      // background.js
      
      import { registerScripts } from "./utilities/scripting.js"
      import { getHosts } from "./utilities/storage.js"
      
      browser.runtime.onInstalled.addListener(async (details) => {
          if (details.reason !== "update")
              return
      
          const hosts = await getHosts()
          await registerScripts(hosts)
      })
    • 22:49 - Package your web extension into an app for Xcode

      xcrun safari-web-extension-packager --copy-resources /path/to/ShinyOnTrack
    • 23:32 - Adding the nativeMessaging permission

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "icons": {
              "512": "images/icon.svg"
          },
        
          "options_page": "options.html",
        
          "permissions": [
              "declarativeNetRequestWithHostAccess",
              "scripting",
              "storage",
              "nativeMessaging"
          ],
        
          "optional_host_permissions": [ "*://*/*" ],
        
          "background": {
              "scripts": [ "background.js" ],
              "type": "module"
          }
      }
    • 23:40 - Sending a native message

      // background.js
      
      import { registerScripts } from "./utilities/scripting.js"
      import { getHosts } from "./utilities/storage.js"
      
      browser.runtime.onInstalled.addListener(async (details) => {
          if (details.reason !== "update")
              return
      
          const hosts = await getHosts()
          await registerScripts(hosts)
      })
      
      export async function requestBioAuth() {
          const message = { message: "requestBioAuth" }
          const response = await browser.runtime.sendNativeMessage(message)
          return response?.success
      }
    • 23:55 - Handling native messages

      // SafariWebExtensionHandler.swift
      
      import LocalAuthentication
      
      class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
          func beginRequest(with context: NSExtensionContext) {
              let request = context.inputItems.first as? NSExtensionItem
              let message = request?.userInfo?[SFExtensionMessageKey] as? [String: Any]
      
              if message?["message"] as? String == "requestBioAuth" {
                  let lAContext = LAContext()
                  Task {
                      do {
                          let success = try await lAContext.evaluatePolicy(
                              .deviceOwnerAuthenticationWithBiometrics,
                              localizedReason: "Authenticate to change blocked sites"
                          )
                          self.reply(context: context, success: success)
                      } catch {
                          self.reply(context: context, success: false)
                      }
                  }
              }
          }
      }
    • 24:25 - Replying to a native message

      // SafariWebExtensionHandler.swift
      
      import LocalAuthentication
      
      class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
          func beginRequest(with context: NSExtensionContext) {
              let request = context.inputItems.first as? NSExtensionItem
              let message = request?.userInfo?[SFExtensionMessageKey] as? [String: Any]
      
              if message?["message"] as? String == "requestBioAuth" {
                  let lAContext = LAContext()
                  Task {
                      do {
                          let success = try await lAContext.evaluatePolicy(
                              .deviceOwnerAuthenticationWithBiometrics,
                              localizedReason: "Authenticate to change blocked sites"
                          )
                          self.reply(context: context, success: success)
                      } catch {
                          self.reply(context: context, success: false)
                      }
                  }
              }
          }
      
          private func reply(context: NSExtensionContext, success: Bool) {
              let response = NSExtensionItem()
              response.userInfo = [SFExtensionMessageKey: ["success": success]]
              context.completeRequest(returningItems: [response], completionHandler: nil)
          }
      }
    • 0:00 - Introduction
    • Learn how Safari web extensions — built with HTML, CSS, and JavaScript and packaged inside an app — can run across iOS, iPadOS, macOS, and visionOS. Preview the distraction-blocker extension built throughout the session, which offers a 10-minute light mode and a full redirect mode.

    • 3:23 - Get started
    • Set up an extension from scratch by writing a manifest.json file, then add a popup UI so the extension is reachable from Safari's toolbar. The same project runs unchanged across every Apple platform that ships Safari.

    • 7:23 - Block content
    • Use the declarativeNetRequest API to block, modify, and redirect network requests, and declare the host permissions — including optional host permissions — that let users grant access on the sites where the extension should run.

    • 14:40 - Modify webpages
    • Inject content into pages with content scripts to render a countdown timer on distracting sites. Register scripts dynamically with the scripting API and persist user preferences and per-host state using the storage API and a background service worker.

    • 19:53 - Package and distribute
    • Submit a Safari web extension to the App Store using App Store Connect, and share beta builds with testers through TestFlight.

    • 22:33 - Communicate with your app
    • Generate an Xcode project with the Safari Web Extension Packager, then use native messaging to pass requests between the JavaScript extension and its containing app — unlocking platform features like Local Authentication that aren't available to web APIs.

    • 26:04 - Next steps
    • Download the sample project, explore the cross-browser WebExtensions documentation on MDN, and file feedback through Feedback Assistant or bugs.webkit.org.

Developer Footer

  • ビデオ
  • WWDC26
  • SafariのためのWeb機能拡張の作成
  • メニューを開く メニューを閉じる
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    メニューを開く メニューを閉じる
    • アクセシビリティ
    • アクセサリ
    • Apple Intelligence
    • App Extension
    • App Store
    • オーディオとビデオ(英語)
    • 拡張現実
    • デザイン
    • 配信
    • 教育
    • フォント(英語)
    • ゲーム
    • ヘルスケアとフィットネス
    • アプリ内課金
    • ローカリゼーション
    • マップと位置情報
    • 機械学習とAI
    • オープンソース(英語)
    • セキュリティ
    • SafariとWeb(英語)
    メニューを開く メニューを閉じる
    • 英語ドキュメント(完全版)
    • 日本語ドキュメント(一部トピック)
    • チュートリアル
    • ダウンロード
    • フォーラム(英語)
    • ビデオ
    Open Menu Close Menu
    • サポートドキュメント
    • お問い合わせ
    • バグ報告
    • システム状況(英語)
    メニューを開く メニューを閉じる
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles(英語)
    • フィードバックアシスタント
    メニューを開く メニューを閉じる
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(英語)
    • Mini Apps Partner Program
    • News Partner Program(英語)
    • Video Partner Program(英語)
    • セキュリティ報奨金プログラム(英語)
    • Security Research Device Program(英語)
    Open Menu Close Menu
    • Appleに相談
    • Apple Developer Center
    • App Store Awards(英語)
    • Apple Design Awards
    • Apple Developer Academy(英語)
    • WWDC
    最新ニュースを読む。
    Apple Developerアプリを入手する。
    Copyright © 2026 Apple Inc. All rights reserved.
    利用規約 プライバシーポリシー 契約とガイドライン