Safari Extension Development Experience
TL;DR:
- This is a noob’s perspective on Safari Web Extension development. I did not have prior experience in this area or Apple Development ecosystem in general.
- I created a total of four Safari Web extensions in this fun experiment: fuzzy tab search, no duplicate tabs, linkding bookmarklet, redirect to invidious.
- XCode is not great for web extension authoring.
- Web extensions must undergo the same review process and adhere to the same set of requirements as regular apps in the App Store.
- Overall, the technical aspect, encompassing learning the Web Extensions API and coding, constitutes less than 10%. The remaining 90% involves dealing with XCode and various App Store review requirements.
Why Safari Web Extensions?
Earlier this year, I decided to quit my job to take a break after feeling burnt out. While employed, I exclusively used either Internet Explorer or Chrome because that’s what most customers preferred. However, on my personal computer (Macbook), I use Safari. It suffices for my light browsing needs and looks aesthetically pleasing on MacOS. Given my limited free time during employment, I rarely used my personal computer, hence Safari anyway. Now, with ample free time, I find myself using Safari more frequently. However, I miss the useful extensions I grew accustomed to on Chrome. Surprisingly, many of them are unavailable for Safari. So, I decided to create them myself.
You might wonder why I didn’t just switch to Chrome or Firefox. Well, I did give them a try, but I eventually returned to Safari. As I’ve been travelled quite a bit recently, I observed that Safari consumes significantly less battery compared to Chrome and Firefox.
Anyway, I compiled a list of extensions that I use most often, unavailable in the App Store but simple enough to hack together quickly:
- Search tab using fuzzy logic
- Automatically close duplicate tabs
- Linkding bookmark
- Redirect YouTube videos to Invidious
In this post, I document my experience building these extensions.
Some background: I’m new to the Apple Development ecosystem but not to software development in general. While I had to learn about Web Extensions from scratch, I do have web development background, which probably makes it easier for me to ramp up.
Apple Developer Account Fee ๐ฒ
TLDR: The $100 per year for a developer account is undeniably a hurdle for someone who just wants to make extensions for fun.
Wanting to share my extensions with others, I initially explored signing up for an App Store developer account. The shock came when I discovered the annual feeโit’s $100 before tax ๐ญ, and it’s an annual commitment. Given my current lack of income, shelling out $100 just for fun seemed … not responsible ๐.
I considered the option of creating these extensions and charging some fees, hoping to cover the annual cost. But how much should I charge for these extensions? Seeking input, I asked my wife about which extensions she found useful. Surprisingly, the only one that made sense to her was the one that closes duplicate tabs; the other three left her perplexed ๐ค. This realization led me to believe that my extensions' audience is likely geared toward the engineering type.
But then, Safari has less than, what, 10% of the market share (?), and most engineers I know prefer Chrome or Firefox. This realization meant I might not have many people willing to pay for these niche extensionsโperhaps around ~50 payers per year. To break even, I would need to charge at least $2 per extension, possibly still losing money after the App Store tax ๐.
Signing up for Developer Account
I assumed signing up for a developer account would be a straightforward process. Well, it’s straightforward when it works, but I happened to be unlucky. On the night I signed up, I found myself stuck in an infinite sign-in loop. What frustrated me the most was that I could sign in to my account on all Apple pages selling their expensive toys, yet the developer page refused to let me in.
Despite trying multiple browsers and Incognito/Private mode, I faced constant failure. While troubleshooting in the browser networking tab, I noticed that the page failed to load a script, seemingly related to parsing phone numbers. If memory serves me right, it returned a 404 error, suggesting a potential CDN issue or a code check-in with not-yet-deployed dependencies. The lesson learned here, as a customer, is to do serious tasks during the day rather than after working hours. Perhaps some engineers hastily checked in code before leaving for the day.
Two amusing details stood out in this incident. Firstly, I wasn’t even entering a phone number; I was using email to sign in. Despite this, Apple attempted to load a phone parsing script. Maybe someone at Apple doesn’t lazy load dependencies! Secondly, I could sign in on all of Apple’s commercial sites, but not the developer site. I guess Apple prioritizes consumers over developers ๐. Perhaps we need Steve Ballmer to join Apple! (Developer x 3 joke, anyone?)
In any case, I closed my laptop and went to sleep. The next day, everything went smoothly; someone had probably fixed the issue while I was asleep. Sign-in worked seamlessly, and the onboarding process was not ncomplicated. Although they mentioned it would take 48 hours to process my information, it took less than an hour for my Developer Account to become active.
Safari Extension Documentation
I believe this is the primary documentation: Creating a Safari Web Extension.
The documentation predominantly focuses on using XCode to create an Extension project rather than providing detailed insights into implementing an extension for Safari. Interestingly, Apple directs users to the MDN site for API documentation. At first thought, this seems logical, as duplicating API documentation for Firefox extensions wouldn’t make sense when both browswers share the same manifest v3 specs, right? Well, as I later discovered, they are not entirely the same. There are many APIs that function in Firefox are not implemented for Safari. A couple of examples that come to mind are tabs.onUpdated
, where the filter argument doesn’t seem to support properties
, and bookmarks
are not supported at all.
Naively assuming that the APIs documented on MDN for Firefox could be seamlessly applied to Safari due to the reference from Apple’s official documentation, I spent significant amount of time troubleshooting Safari issues that I thought to be my mistakes. Now, I make it a practice to always scroll to the bottom of the MDN page to check the support matrix.
Deployment / Using XCode to upload extensions to App Store
While Safari extensions can now be developed using web technologies (JS, HTML, CSS), their deployment remains distinctly Apple-centric.
Each Safari extension is associated with a corresponding Mac App, which can essentially function as a wrapper. This approach may seem weird, but it’s likely the most practical (cheap) way for Apple to enable Safari extension distribution in the App Store, leveraging the infrastructure built for iOS and MacOS apps over the years. From a Extension developer’s perspective, the advantage is that you don’t need to worry about monetization model, but the downside is the need to learn Mac App development quirks. Simply knowing web development is not sufficient.
In my case, I meerly use the Mac App as a wrapper, so I was able to avoid delving into Swift or Objective-C code. However, I still need to learn several aspects of Mac App development. Here are a few examples:
-
You’ll need to learn new ways to adjust XCode’s project settings such as build settings, package signing, and build targets. They aren’t inherently bad, but they can be confusing initially.
-
Generating icons is not just a requirement for extensions but also for the wrapper Mac apps. Each Mac app has an Asset Catalog in XCode where you must set icon files for various sizes: 16x16, 32x32, 128x128, 256x256, 512x512. Additionally, each size requires 1x and 2x variants ๐ญ. Learning how to do this in XCode is essential or at least the only method I’ve found so far.
-
To initiate the distribution of your extension, you navigate to the Product menu and click Archive. I found this very unintuitive.
-
There’s a concept of a bundle ID. You have to visit a page called App Connect and register the bundle ID first; otherwise, XCode will complain that its local bundle ID is not found on App Connect. Apple should probably make it easier and upload bundle ID in XCode to App Connect when such id does not exist. But it’s just a small hassle. Anyhow, the not-so-obvious aspect is that the bundle ID is case-sensitive. When using the Safari Extension App template to create an XCode project, the product name is, by default, added to the bundle ID. If my product name is Tab Search, then my bundle ID is
com.my-domain.Tab-Search
. The bundle ID I registered in the App Store iscom.my-domain.tab-search
, so XCode kept complaining that my specified bundle ID doesn’t exist. Very confusing. By the way, on the App Connect page, unlike in XCode, they compare bundle IDs by ignoring case. I tried registering another app In App Connect with an uppercase Tab-Search ID to match the case in XCode, but the page complained that the lowercase tab-search already exists!
Toolings
I have to say that I do not like XCode for development at all. It seems very clunky and does not integrate well with web dev tools like npm, typescript, etc. I actually do not know if such integration exists.
The XCode’s generated placeholders use javascript. There’s also no code intellisense for Web Extension APIs, which makes it really hard when you are not familiar with the APIs. Without typing and code intellisense I found that I make too many typos which slows the development down significantly.
Vim mode exists, but don’t bother. Dot repeat does not work.
I gave up using XCode for development and decided to look up a few github projects to understand how people set up their web extension development. There are a few noteworthy ones, e.g. this and this but they are all created for Chrome and/or Firefox.
As a compromise, I decided to adopt one of those templates for Chrome, and hacked together a few things to automatically rebuild the xcode project on change detection. Hot module reloading does not work at all, but using VSCode + typescript + vite is a million times better than XCode + vanilla JS.
App Review Process
I have never dealth with this process before. I suppose it’s one of the perks of working in large companies, where you iterate on established products and have various support roles handling the app review process.
This was the most unpleasant aspect of this Safari Extension marathon week. Let me provide an account of what I had to navigate for each extension I created.
Tab Search
My first ever App Store app review. Upon examining the submission form, a few immediate requirements stood out:
- A product support website. Is this a bit overkill for a simple web extension ?
- A privacy policy page. Huh ? Can you give me a few checkboxes and generate one for me, Apple ?
- Screenshots. Too many of them are required.
These requirements, while make sense for complicated apps in App Store, seem overkill for small web extensions. Does Apple expect that majority of web extensions for Safari will as complicated as regular apps in the App Store ? They’re probably right, given that I’m just a n00b.
Anyway, I wasn’t sure about the standard bar for a product support page, so I did my best. I created a static site using Hugo and deployed it to Netlify. As you can see, I put in considerable effort ๐. I even included a User Guide page and a separate Privacy Policy page to make it look polished and Apple-esque, hoping it would prevent rejection during the review. Fortunately, it did pass the review, but the lack of feedback on the support page leaves me uncertain if the effort was an overkill ๐ค. In the subsequent extensions, I gradually reduced the effort. You’ll see ๐.
By the way, if you click on the site, there’s a link to the extension in the App Store priced at $1.99. The cost is merely to cover the annual App Store fee of $100. You don’t have to purchase it; simply clone the GitHub repo and build it yourself. The GitHub repo contains the web extension code but lacks the wrapper Mac app. To create the wrapper app, use XCode’s Safari App Extension template. I didn’t include it in the repo because:
- I made no changes to the wrapper app’s code. You can get the same code generated by using Safari App Extension in XCode.
- I lack the time and inclination to strip all my Apple account and potential personal info linked to that Mac App before publishing it on GitHub. I don’t like to go through the whole ordeal of setting up these settings in XCode again.
If you’re from the future and the App Store link no longer works, it’s likely because these extensions didn’t generate enough revenue to cover the annual fee, leading to the closure of my account ๐ซก.
Despite putting in significant effort for my first submission, it still got rejected due to a minor issue. The word “logic” in my extension’s subtitle, which reads “search tabs using fuzzy logic,” caused the rejection. It’s not allowed because some other Apple product uses the word “logic”. I find this restriction peculiar. Can’t I use common terms like “photo,” “calculator,” or “book” then?
Regardless, the App Store is owned by Apple, and it’s their house, their rules. Now I understand why some people want to sideload their apps and why, in the Android universe, some engineers opt to release their apps in third-party stores.
Concerning the screenshots, the requirements for sizes are stringent. They only accept images of certain dimensions. This might be straightforward for others, but for me, it took a while to figure out how to produce the compliant screenshots. I attempted to use the default MacOS screenshot tool to select an area of exactly 1280x800, but achieving precision was challenging. A few pixels off was enough for the App Store tool to reject the image. I ended up capturing the entire window and using the ImageMagick CLI to crop. Typing “1280x800” in the CLI was much easier than fiddling with my touchpad for a precise crop.
No Duplicate Tabs
So, the Tab Search review wasn’t too bad, right? Besides the “logic” word issue and the strict screenshot requirements, it seemed fine, and I was excited, thinking I had quickly grasped the process.
For the No Duplicate Tabs extension, I thought it would make sense to have it available on iOS as well as MacOS. I often end up with numerous duplicate tabs in iOS because iOS Safari lacks a tab bar like the desktop Safari, making it easy to lose track of open tabs.
I followed the same process as for MacOS. However, the Archive tool kept rejecting the App Store upload. It turned out that the iOS icon I created did not pass some automatic checks. The tool insisted that the App Store icon should not have a transparency layer and alpha channel. I tried fixing the issue by:
- Filling all the white space with a dark color.
- Removing the alpha channel using ImageMagick with
-background white -alpha remove -alpha off
.
None of that worked. XCode kept spitting out the same error.
By luck, I discovered that MacOS icons and iOS icons have different requirements. I had missed that detail when skimming through the App Store’s documentation. MacOS icons should be in a square with rounded corners, but iOS icons should be a full rectangle without rounded corners. This issue alone cost me hours, as I thought I had accidentally done something wrong, and I had to recreate my icons multiple times. Each time, I had to manually add them to the Assets Catalog in Apple. This whole thing is very time-consuming as I’m not accustomed to design work or working with XCode.
Now that I successfully fixed the icon issues, the next obstacle was screenshots. Apple requires lots of screenshots for various devices, including:
- MacOS
- iPhone 6'7"
- iPhone 6'5"
- iPhone 5'5"
- iPad Pro 6th gen
- iPad Pro 2nd gen.
Providing screenshots for all these devices felt overwhelming. Luckily, for an extension that works in the background to close duplicate tabs, the only screenshot I had to provide was a toggle switch to turn the extension on/off ๐. However, in an attempt to be more helpful to users, I decided to create an App Preview. BIG MISTAKE!
App Preview is in video format and has strict requirements for dimensions. Unfortunately, ImageMagick couldn’t help me here. I found a template in iMovie specifically for creating App Preview videos. Even with this template, it proved impossible for me to create a video accepted by the App Store review tool. For some reason, my original screencast did not have the correct dimensions or ratios. I used the Moom tool as a window manager and specified the window size exactly as required by Apple, but likely, when selecting the recording area, I was a few pixels off. In iMovie, my video was cropped in a way I did not like. Any attempt to change the crop area resulted in a video that failed to meet the App Store’s requirements. I eventually gave up.
At this point, I was exhausted. I had spent less than 10% of my time coding and more than 90% satisfying all these App Review requirements. I still had to create a support page and a privacy policy ๐ข. Feeling fatigued, I just made a copy of the Tab Search support page, and this time I didn’t bother writing a User Guide.
Two days later, the review passed. It turned out that even if you create a multiplatform Safari Extension, you still need multiple reviews. In this case, one iOS review and one MacOS review.
After successfully getting two extensions through the review process, I realized that I’m not having fun. I enjoy coding, but so far, I’ve spent 90% of my time creating icons, screenshots, filling out app review forms, and adhering to all the App Store’s stringent requirements. No wonder there are fewer extensions built for Safari, even though the monetization models are already built-in in Apple ecosystem.
Linkding
I could have stopped there, but there’s one extension that I desperately need, an extension that allows me to click and bookmark a page with Linkding. This extension does exactly what the official bookmarklet does.
So, why not use the bookmarklet? Well, I use Linkding to manage my bookmarks, and I always have my bookmark bar hidden. Toggling the bookmark bar visible and clicking on the bookmarklet gets tiring very fast.
This time, I decided to make a quick and dirty one, just for the MacOS platform, no more iOS. I’m done with iOS requirements for screenshots, and I’m not going to create a site to host the support page and privacy policy. I’ll just use GitHub’s README.md file. For the app description, I’ll write what comes to my mind: why I created the extension, why I charge money for it, and how users could get it for free from GitHub if they choose to do so. You know, a very casual style, something that a weekend project would normally do.
The submission got rejected. For multiple reasons.
One reason is that my app description contains irrelevant information to the app. I don’t know why the following info was deemed irrelevant:
- Explanation on why I built this extension and for what scenarios.
- Note that says I only charge for the extension to cover the annual fee and includes a link to the source code on GitHub.
Anyway, I figured arguing with an Apple reviewer takes more effort than just removing all of that info. I quickly put a brief description that says this extension creates a bookmark using Linkding. Maybe that’s for the better. I don’t know. But I feel like my freedom of expression for my own app is limited when I’m distributing it in the App Store. That is probably what makes me feel unhappy.
The second reason for rejection is that they apparently want to test the extension. I didn’t know that they actually do testing too. From the app consumer’s perspective, this is good. But for me, I need to provide them with a public instance of Linkding, which is a self-hosted service, to test with. I reluctantly complied. On the one hand, deploying the service and making it available publicly takes more time than coding the extension itself, so I feel that this requirement is overkill for this extension. On the other hand, I’m kind of happy that they actually want to test my extension properly. Anyway, doing the right thing is always a good thing, so I’ve deployed a public instance for them to test.
The third reason for rejection is an extremely weird one. The reviewer said this screenshot has a “Debug” symbol. For the life of me, I cannot figure out what they are referring to. That’s the Options page that they are talking about. It only has a text box label, a default Linkding host address, and a button that says Submit. There is not even an icon on the page. There’s not even a word “Debug” or a synonym of that word on the page.
I’m not having fun. I thought of withdrawing the review for the Linkding extension, but I’m just too curious to see what Debug symbol is hiding on the page that I’m not seeing. I messaged the reviewer and asked if it’s the https://localhost:9090
that they are talking about? I explained to them what a self-hosted service is and that this is how users might have their Linkding service set up. It’s not a Debug screenshot. A day later, they came back with the same message saying I have a Debug symbol on my screenshot without pinpointing exactly where in the screenshot ๐ . Now a lightbulb lit up in my head. Maybe they’re not talking about the page that I created. Maybe they are talking about the Safari window’s address bar. My Safari profile name is Dev (because I’m a dev, duh) and I use a hammer as an icon. The hammer is among the options presented to me by Safari when I create my profile. Are they complaining about the hammer?
Anyway, I removed Safari top bar from the screenshot. A day later they passed the review. Maybe it’s indeed the hammer icon. They didn’t confirm. If my suspicion is correct, does XCode icon has a hammer, too ?
Invidious Redirect
This is the last extension in my TODO list. This is also something that I dearly need after YouTube started cracking down on ad blockers. This extension simply redirects a YouTube video page to an Invidious page. I was working on this in parallel while I was waiting for the Linkding review. I didn’t plan to submit it to the App Store because the whole process does not bring me joy, and I started feeling tired after the 3rd review. But I convinced myself that this might be helpful to a lot of people since YouTube has become more aggressive with ads. So, I submitted it.
And it got rejected.
They require me to provide documentary evidence that I do not infringe on the rights of third parties. Eh, I don’t know how. My extension just redirects a page to another page. I do not own either page. The destination page in question is even set by the user of the extension; for example, some might use their self-hosted instance of Invidious, and some might choose a public instance out of convenience. I don’t know how to prove to Apple that I don’t intend to infringe on anybody’s rights.
Anyway, I’m done with this Safari extension experiment. I’m no longer having fun. I feel like the requirements they impose on web extensions are overkill. I won’t pursue getting this resolved.
Summary
Wow, this is a long one. For one week playing with the whole process, from learning about web extension development to getting extensions published on the App Store, I learned a ton. Will I do this again for fun? Probably not. Not until Apple simplifies their review process and requirements for web extensions. Not until they make it less cumbersome to develop web extensions in XCode. Make it so that deploying extensions and getting past review take less than 10% of development time. Oh, and invest in documentation and get to parity with Firefox in terms of web extension API support.
One positive thing that I think I should add to end this super long post is that their reviewers do actually comment or reject submissions timely, within 48 hours, as they advertised.