Introducing PageExtender for Safari Extend pages with your own CSS and JS files
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 google.com.css
to tweak the style of Google or
a wikipedia.org.js
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).
Motivation
You might be asking yourself, why would someone want to customize websites like that? Sometimes I notice things that annoy me. A few examples:
-
Wikipedia uses SVG images a lot, but the articles include them as PNG images, which is unfortunate. I want my crisp SVGs that I can zoom in to! Thus, I’ve created a
~/.js/wikipedia.org.js
that loops through all the<img/>
and tweaks thesrc
attribute to point to the underlying SVG. -
I want to see code files on GitHub in my programming font. Just set
font-family
in~/.css/github.com.css
. -
GitHub’s text fields show a formatting toolbar at the top. I know the shortcuts and I also know Markdown, thus I never use on any of those buttons. Now they’re all hidden.
-
GitHub’s text fields use a proportional font that make aligning code and crafting ASCII drawings impossible. Changed that to my programming font.
-
Hide obnoxious banners urging you to register or to disable your ad blocker on pages that you end up visiting often, such as Medium. Often these pages also disable scrolling. Put an
overflow: initial !important
in there for some peace of mind. -
Hide promoted and sponsored content that you couldn’t care less about. Quora is a good example here. They also try make it hard for ad blockers to hide that content. Thanks to
innerText
it’s still possible to detect those. This is my script that removes these.
History
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).