Introducing PageExtender for Safari Extend pages with your own CSS and JS files

April 9, 2019
App icon

PageExtender is a Safari extension that injects custom CSS and JS files. Files are loaded based on the domain name from a CSS and JS folder that you specify. E.g. create a to tweak the style of Google or a file to customize the behavior of Wikipedia.

Buy it on the Mac App Store ($5) or get the source and run it for free (donations appreciated).

Download on the Mac App Store


You might be asking yourself, why would someone want to customize websites like that? Sometimes I notice things that annoy me. A few examples:


Years ago I had come across Chris Wanstrath’s dotjs, a Chrome Extension that injects JS files named after the domain, located in ~/.js. I absolutely loved the idea of using dotfiles to customize websites.

After all, I often find myself wanting to tweak things on website I visit often. Fortunately, someone had ported that extension to Safari, my browser of choice.

Being able to inject JS is nice, but what about CSS? Someone had created an analogous Safari extension for CSS. Cool! Now I was able to create CSS and JS files in ~/.js and ~/.css to take control of the pages I visit.

Both extensions were spinning up a web server to serve the files. This is necessary because extensions are not allowed to access files on the file system. I then modified one of the extensions to deliver both CSS and JS files, such that only one extension and one server was needed. Unfortunately I never managed to release my custom extension.

This served me well for years until Apple decided to revamp the extension system in Safari 12, introduced with Mojave. None of the old extensions worked anymore. Instead, extensions are now called Safari App Extensions and have to be shipped within a regular macOS app.

Missing my old set up, I set out to create an extension that brings back my customizability.

How It Works

The macOS app serves as the config UI allowing the user to specify where to look for CSS and JS files:


Unfortunately, this cannot simply default to ~/.css and ~/.js because of sandboxing. Only by having the user select a folder through the standard open dialog the app gets permission to access a folder.

Running the app for the first time will cause Safari to pick up the embedded extension that you can now enable in Safari’s preferences.

The extension itself is quite simple. A JS script runs on every page load and requests the files to inject from the extension handler, a Swift binary. This handler looks up the files matching the domain in the configured folders and sends them back to the script.

The look up is based on the domain. For both CSS and JS a default file is looked up as well, allowing you to have code that is injected into all pages.

The script then injects the contents of the files into the page by adding <style> elements for each CSS file and by evaluating each JS file once the DOM is ready.

With this approach no web server is required as the extension handler has access to the file system. At the same time, users not familiar with dotfiles can choose any path for storing their CSS and JS files, resulting in a nicer experience overall.

Buy it on the Mac App Store ($5) or get the source and run it for free (donations appreciated).

Download on the Mac App Store