I'm a solo developer in Japan, and I build minulo, an iPhone app for people who want to reply to Instagram DMs without getting pulled into Reels.
The idea is simple. Instagram, YouTube and a few others open inside a WKWebView, and CSS removes the Reels/Shorts entry points. The official apps are blocked with Screen Time (FamilyControls / ManagedSettings), so opening them sends you to minulo instead.
Here are three things that were harder than I expected.
1. Inject twice. CSS goes in injectedJavaScriptBeforeContentLoaded, otherwise the Reels tab flashes before it disappears. JS rules and a MutationObserver go in injectedJavaScript. These sites are SPAs, so URL-scoped rules get re-applied on pushState/replaceState/popstate.
I once wrapped history.pushState inside the function that runs on every DOM change. Every run added another wrapper, so the longer the page stayed open, the slower it got, until it froze. Wrap once.
2. display:none can blank the whole feed. Hiding suggested posts with display:none stopped Instagram from rendering anything below them. The feed is virtualized, and a zero-height item seems to tell it there's nothing more to draw. I reproduced it 3/3 in the simulator. Now I tag the element and keep its height.
Also, a[href^="/reel"] looked like it hid the profile Reels tab. It only hid the icon. The link was still tappable, because that URL is /<username>/reels/. a[href$="/reels/"] fixed it.
3. CSS doesn't throw. When a site changes its markup, the selector simply stops matching. Nothing errors, so try/catch won't catch it. Every 30 seconds, each rule's selector is run through querySelectorAll and the match counts get posted back to the app. A rule that stays at zero is treated as broken. Two caveats: skip counting on login pages (everything is zero there), and only count URL-scoped rules on their own URLs.
Bonus: opening a URL from a ShieldAction extension is silently ignored on a real device. My workaround: the shield button posts a local notification, and tapping it opens minulo. The blocked app's name is only known inside the extension, so I put it in the URL query, percent-encoded. With a space in the name, URL(string:) returns nil and nothing happens.
Hiding is free: https://apps.apple.com/app/id6809352229
Top comments (0)