-
Créez des extensions web pour Safari
Démarrez avec les extensions web Safari en créant et en testant une de A à Z, sans utiliser Xcode. Découvrez comment le blocage de contenu, la modification des pages, la messagerie native et le modèle Autorisations fonctionnent ensemble pour créer une expérience de navigation puissante et respectueuse de la vie privée sur toutes les plateformes.
Chapitres
- 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
Ressources
- 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
Vidéos connexes
WWDC26
-
Rechercher dans cette vidéo…
-
-
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.