In this article, I’ll show you how to access a web page’s content, alter it using JavaScript, and display it using your Safari extension’s UI. Specifically, I’ll demonstrate how to display an article’s content in a popover.
I often use Large Language Models (LLMs) to summarize articles when I’m unsure if they’re worth reading. Without the extension we’re about to build, I’d need to manually trigger Safari’s reader mode (⌘ + ⇧ + R), copy all content, and paste it into Claude or ChatGPT. This extension is a handy tool that reduces friction!
Start by creating a new project using the “Safari App Extension” template, or add a Safari Extension target to your project. When prompted, set type
to Safari App Extension (the default at the time of writing).
After the initial setup is complete, follow this guide to modify the template so that you can use SwiftUI to build the UI. Follow all the instructions except for one: do not delete script.js
. We’ll need it to capture the web page’s content.
Once that’s done, we’ll proceed to get the current web page’s content, modify it to extract the article content, and display it. You can check out the complete sample project here.
- Getting Active Safari Page (
getActiveSafariPage
)
let page = try await getActiveSafariPage()
- Uses Safari’s extension API to retrieve the current window, tab, and page
- Returns a
SFSafariPage
object representing the active browser page
- Requesting Content (
requestAndReceiveContent
)
page.dispatchMessageToScript(
withName: SafariExtensionMessage.Name.getContent.rawValue,
userInfo: nil)
- Sends a “getContent” message to the injected content script
- Content Script Processing (
script.js
)
function getPageContent() {
// Tries to extract content in this order:
// 1. Structured article data (JSON-LD)
// 2. Article elements with specific selectors
// 3. Fallback to body text
const content = {
title: document.title,
body: getReaderContent().trim()
};
safari.extension.dispatchMessage("pageContent", { content });
}
- Message Handling (
SafariExtensionHandler
)
case .pageContent:
NotificationCenter.default.post(
name: NSNotification.Name(SafariExtensionMessage.Notification.messageReceived.rawValue),
object: nil,
userInfo: userInfo
)
- Receives the content message from the script
- Posts it to NotificationCenter
- Content Reception (
PopoverViewModel
)
for await notification in NotificationCenter.default
.notifications(named: NSNotification.Name(SafariExtensionMessage.Notification.messageReceived.rawValue))
.compactMap({ notification in
// Converts notification data to WebPageContent
})
- Listens for the notification with the page content
- Converts the received data into a
WebPageContent
struct - Returns the structured content to be displayed in the popover