<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[minulo dev notes]]></title><description><![CDATA[minulo dev notes]]></description><link>https://minulo.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 24 Oct 2026 15:37:52 GMT</lastBuildDate><atom:link href="https://minulo.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Do AI answer engines cite a small app's website? I checked per language, then rebuilt my static site's hreflang]]></title><description><![CDATA[I'm Ayumu Kitabayashi, a solo developer in Japan. I make minulo, an iPhone app that hides Reels and Shorts. Its website, minulo.app, is plain HTML on…]]></description><link>https://minulo.hashnode.dev/do-ai-answer-engines-cite-a-small-app-s-website-i-checked-per-language-then-rebuilt-my-static-site-s-hreflang</link><guid isPermaLink="true">https://minulo.hashnode.dev/do-ai-answer-engines-cite-a-small-app-s-website-i-checked-per-language-then-rebuilt-my-static-site-s-hreflang</guid><dc:creator><![CDATA[ayumu kitabayashi]]></dc:creator><pubDate>Sat, 24 Oct 2026 11:43:25 GMT</pubDate><content:encoded><![CDATA[<p>I'm Ayumu Kitabayashi, a solo developer in Japan. I make minulo, an iPhone app that hides Reels and Shorts. Its website, minulo.app, is plain HTML on GitHub Pages in 13 languages. This post is about two things: how I checked whether AI answer engines cite that site in each language, and what I changed in the site afterwards.</p>
<p>All numbers below come from my own notes between October 17 and October 24, 2026. Each one is a single observation, so treat them as notes rather than a benchmark.</p>
<h2>Why I started measuring</h2>
<p>App Store Connect shows which apps people came from before downloading. Over the 30 days I checked on October 20, ChatGPT accounted for 193 of those referrals. The Perplexity, Gemini and Claude apps accounted for zero. I wanted to know whether the other engines simply weren't mentioning the app, and if they were, what they were citing.</p>
<h2>How I measured</h2>
<p>I picked one question that describes what the app does, and asked it the same way each time:</p>
<pre><code>best free iPhone app to hide Instagram Reels and YouTube Shorts but keep DMs
</code></pre>
<p>Then I asked the same thing in Japanese, Korean, Spanish, Portuguese, German and French, phrased the way a person in that language would search, not word for word.</p>
<p>For every answer I logged one row:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td>Date, engine, language</td>
<td>Oct 24, Perplexity, es</td>
</tr>
<tr>
<td>Position of my app</td>
<td>1st / 4th / not mentioned</td>
</tr>
<tr>
<td>Cited sources</td>
<td>github.com, App Store, someone's blog</td>
</tr>
</tbody></table>
<p>The source column turned out to be the useful one. A position tells you how you did. The sources tell you what to fix.</p>
<p>A few rules I followed:</p>
<ul>
<li>A new conversation for every question.</li>
<li>Never decide from one run. Answers list two to five items and change between runs.</li>
<li>Asking from Japan sometimes pulls the Japanese App Store, so results may differ from what a US user sees.</li>
<li>Most of my Perplexity runs were made while signed in to my own account, which has a history of similar searches. I haven't repeated them signed out yet.</li>
<li>Copilot requires sign-in, so I couldn't read its answers. Instead I watched the AI Performance report in Bing Webmaster Tools, which counts citations in Copilot and partners.</li>
</ul>
<h2>What I found</h2>
<h3>English and Japanese: cited. Spanish, Portuguese, German, French: not</h3>
<p>On October 24, Perplexity put the app first for the English and Japanese questions and fourth for Korean. For Spanish, Portuguese, German and French it wasn't mentioned at all. At the time of that check the site already had more than 40 pages in each of those four languages. Having pages in a language was clearly not enough.</p>
<h3>The cited source was GitHub, not my site</h3>
<p>In English, Japanese and Korean, Perplexity's sources for my app were on GitHub: the app's README and a list I maintain of iPhone apps and settings for hiding Reels and Shorts, with a CSV and a note that I'm the author. For the Korean question, GitHub was the only source cited. The Korean pages on minulo.app weren't cited.</p>
<p>Back on October 19, with the English question, Perplexity had already ranked the app first with the README as its source, while two other engines didn't mention it.</p>
<h3>Where my app was missing, same-language how-to pages won</h3>
<p>In the languages where it didn't appear, the cited pages were how-to and comparison articles on other developers' own sites. They were written in the language of the question, had a table, and used the question's words (free, keeps DMs) in the title.</p>
<p>My working hypothesis: Perplexity prefers a page in the same language, with a table, that reads like a third-party overview. English and Japanese had that. The other languages didn't.</p>
<h3>The other engines</h3>
<ul>
<li>Brave's AI answer (English) listed the app third on October 24. In Brave's regular results, my English page about free options ranked second; on October 17 it hadn't appeared at all.</li>
<li>Gemini didn't mention the app for the English question on October 24. On October 23 it listed it third for the Japanese question but wrote its Japanese reading as "ミニロ" (miniro) instead of "ミヌロ" (minuro). I added the correct reading to the structured data (<code>alternateName</code>) and to <code>llms.txt</code>.</li>
<li>Copilot had zero citations in Bing's AI Performance report for October 1 to 19.</li>
</ul>
<h2>Bing's index was the real bottleneck for Copilot</h2>
<p>Bing Webmaster Tools on October 23 showed that the sitemap had exposed 243 URLs to Bing, but only 3 were indexed, and Bing counted one referring domain. An audit the same day found nothing technical blocking crawlers. My reading is that a new domain with almost no outside links gets discovered but not indexed. No amount of markup fixes that on its own.</p>
<h2>What I changed in the static site</h2>
<p>Given the hypothesis, the plan was to put the same pages in every language and make sure each language version points to the others correctly.</p>
<h3>More same-language pages</h3>
<p>On October 24 I added 88 pages that existed in English but not in other languages (20 Spanish, 20 Portuguese, 19 German, 18 French, 11 Korean). The commit touched 255 files. Most of those were not the new pages but existing ones whose hreflang tags and footer language links had to change.</p>
<h3>Rebuild hreflang from the files that exist</h3>
<p>Page counts differ a lot by language. On the evening of October 24, English had 67 pages in its folder, Korean, German, French, Spanish and Portuguese 64 each, Japanese 62, and the smaller languages 12 to 15. So every page has its own set of alternates.</p>
<p>The site used to be generated from JSON, but I had hand-edited the HTML so often that the JSON went stale. Running the old generators now would restore old titles and hreflang. So I stopped trusting any list and wrote a 37-line script that trusts only the file system:</p>
<pre><code class="language-python">have = [(h, d, n) for h, d, n in L if os.path.exists(path(d, s))]
</code></pre>
<p><code>L</code> holds (hreflang, folder, display name) for the 13 languages. Japanese lives at the root, and the <code>pt</code> folder is Brazilian Portuguese, so its hreflang is <code>pt-BR</code>. For each page slug the script:</p>
<ol>
<li>Removes every existing <code>&lt;link rel="alternate" hreflang=...&gt;</code> from each language version and inserts a fresh block right after the canonical tag. Rebuilding instead of patching means one-sided links can't survive.</li>
<li>Sets <code>x-default</code> to the English version if it exists, otherwise to the first language that exists, so it never points at a missing page.</li>
<li>Rewrites the footer's language links from the same list, with the current language in bold and <code>hreflang</code>/<code>lang</code> attributes on the others.</li>
<li>Replaces that slug's <code>&lt;url&gt;</code> entries in <code>sitemap.xml</code>, each with <code>xhtml:link</code> alternates for every existing version plus <code>x-default</code>.</li>
</ol>
<p>One page has a different filename in Japanese because I published that version first and didn't want to change its URL. A one-line mapping handles it, and every path, URL and relative link goes through the same function:</p>
<pre><code class="language-python">JA = {'turn-off-youtube-autoplay-recommendations.html': 'youtube-autoplay-recommendations-off.html'}
</code></pre>
<p>The sitemap now lists 612 URLs. One known weakness: the script writes a fixed date into <code>lastmod</code>, so it records when I ran it, not when the content changed. Taking <code>lastmod</code> from git history is planned but not done.</p>
<h3>llms.txt stays manual</h3>
<p><code>llms.txt</code> at the root has one line per page: the English title, a short note in parentheses about what the page answers, and the URL of each language version. I edit it by hand, because the note needs someone who has read the page.</p>
<h3>Tell the indexes</h3>
<p>After each push I send the changed URLs with IndexNow to api.indexnow.org, Bing and Naver. The site keeps an IndexNow key file at its root, and a 200 response means the request was accepted. On October 24 I sent all 398 URLs I had at the time this way. Bing's manual URL submission is capped at 100 a day, and Google's indexing requests at about 10, so IndexNow is the practical way to notify that many at once.</p>
<h2>What I don't know yet</h2>
<p>I haven't measured whether any of this worked. On October 31 I'll ask Perplexity, Brave and Gemini the same questions in the same languages as on October 24, and log the position and the cited sources again. If the new same-language pages show up as sources, the hypothesis holds. If GitHub is still the only source, the next step is outside links and same-language lists rather than more pages on my own domain.</p>
<h2>If you want to try this</h2>
<ul>
<li>Use one fixed question per language and log the sources, not just the position.</li>
<li>Repeat runs. A single answer is noise.</li>
<li>Check your own index coverage before blaming the engines.</li>
<li>Build hreflang from the files you actually publish, and rebuild every language version at once.</li>
</ul>
<p>Disclosure: I'm the developer of minulo. The site is at <a href="https://minulo.app/en/?ref=hashnode-en-aicite-1025">https://minulo.app/en/?ref=hashnode-en-aicite-1025</a></p>
]]></content:encoded></item><item><title><![CDATA[ChatGPT sends my iPhone app its best users. Claude couldn't find it at all.]]></title><description><![CDATA[I build minulo, a small iPhone app that hides Instagram Reels and YouTube Shorts but keeps DMs. It's six weeks old, made by one person, with no ad budget…]]></description><link>https://minulo.hashnode.dev/chatgpt-sends-my-iphone-app-its-best-users-claude-couldn-t-find-it-at-all</link><guid isPermaLink="true">https://minulo.hashnode.dev/chatgpt-sends-my-iphone-app-its-best-users-claude-couldn-t-find-it-at-all</guid><dc:creator><![CDATA[ayumu kitabayashi]]></dc:creator><pubDate>Thu, 22 Oct 2026 12:02:10 GMT</pubDate><content:encoded><![CDATA[<p>I build minulo, a small iPhone app that hides Instagram Reels and YouTube Shorts but keeps DMs. It's six weeks old, made by one person, with no ad budget worth mentioning. So I watch where people come from very closely, and one source surprised me: <strong>ChatGPT</strong>.</p>
<p>This is what the numbers look like, what I found when I checked the other assistants, and what I've been doing about it. The numbers are small. I'll say where they're too small to mean much.</p>
<h2>What App Store Connect shows</h2>
<p>App Store Connect lists "source apps" for product page views and downloads. From Sept 10 (the week the app went live) to Oct 16:</p>
<ul>
<li><strong>ChatGPT: 179 product page views, 127 downloads.</strong></li>
<li>Claude, Gemini, Perplexity: nothing listed.</li>
</ul>
<p>127 out of 179 is a high ratio for a product page. People arriving from ChatGPT have already been told what the app does and that it fits their question. They come to download, not to browse.</p>
<p>On one day (Oct 16), ChatGPT accounted for 15 downloads on its own. For an app at this size, that's a meaningful share of the day.</p>
<h2>What my website analytics show</h2>
<p>My site counts where each visitor came from (a <code>?ref=</code> tag if present, otherwise the referrer). Over the last three days:</p>
<table>
<thead>
<tr>
<th>Source</th>
<th>People</th>
<th>Went on to the App Store</th>
</tr>
</thead>
<tbody><tr>
<td>chatgpt.com</td>
<td>15</td>
<td>9</td>
</tr>
<tr>
<td>google.com</td>
<td>17</td>
<td>4</td>
</tr>
<tr>
<td>direct</td>
<td>31</td>
<td>2</td>
</tr>
</tbody></table>
<p>Fifteen people is not a sample I'd build a theory on. But the direction matches App Store Connect: people sent by ChatGPT already have intent.</p>
<p>In the report I published about the first month (<a href="https://minulo.app/en/reels-shorts-data-2026.html">data here</a>), 28% of users said they found the app through an AI chatbot (the app asks where you heard about it).</p>
<h2>Why the other assistants sent nobody</h2>
<p>I assumed Claude, Gemini and Perplexity just weren't recommending small apps. When I checked, the reason was simpler: <strong>they couldn't see my site.</strong></p>
<ul>
<li><strong>Claude</strong> uses Brave Search for web results. A <code>site:minulo.app</code> search on Brave returned <strong>zero pages</strong>. Brave only knew about the App Store listing and a couple of my dev.to posts.</li>
<li><strong>Gemini</strong> leans on Google. Google had indexed part of the site, but a block of pages sat in "Discovered – currently not indexed".</li>
<li><strong>Perplexity</strong>, logged into my own account, actually recommended minulo first for "iPhone app that hides Instagram Reels but keeps DMs". I don't trust that result yet, because that account had searched similar things before. I'll recheck logged out.</li>
</ul>
<p>ChatGPT, on the other hand, seemed to lean on the App Store page and a few third-party articles that mention the app.</p>
<h2>What I changed</h2>
<p>None of this is clever. It's mostly plumbing:</p>
<ol>
<li><strong>Submitted the site to Brave directly.</strong> Brave has a "submit URL" page. I've sent it 65 URLs so far, including the language versions of the main guides. I'll check <code>site:minulo.app</code> again in a week.</li>
<li><strong>IndexNow on every push</strong> (Bing and Naver), and Bing Webmaster's URL submission for the rest.</li>
<li><strong>One question per page, in the words people ask.</strong> "How do I turn off Reels on Instagram?", "Use Instagram only for DMs", "Screen Time limit on Instagram not working". Each page answers the question first, including the free built-in options, and only then mentions the app.</li>
<li><strong>The same pages in 13 languages</strong>, with proper <code>hreflang</code>. A Korean student asking in Korean should get a Korean page.</li>
<li><strong><code>llms.txt</code></strong> listing every page and language version, plus JSON-LD (<code>FAQPage</code>, <code>Article</code>) on the guides.</li>
<li><strong>Third-party mentions.</strong> Assistants cite other sites more readily than a developer's own. So I write on dev.to, Hashnode, Qiita, Zenn and others, answer Quora questions with a clear disclosure, and email writers whose articles cover the same problem.</li>
</ol>
<h2>What I'd tell another indie developer</h2>
<ul>
<li><strong>Look at "source apps" in App Store Connect.</strong> You may already have AI referrals and not know it.</li>
<li><strong>Check what each assistant can actually see.</strong> <code>site:yourdomain</code> on Brave told me more in ten seconds than a week of guessing.</li>
<li><strong>Treat AI referrals as high-intent traffic.</strong> In my case they convert better than search, so a page that an assistant can quote is worth more than its raw traffic suggests.</li>
<li><strong>Be honest on the pages.</strong> If the free settings solve the problem, say so first. An answer that's actually useful is what gets repeated.</li>
</ul>
<p>I'll post the follow-up numbers once Brave has had time to index the site.</p>
<hr />
<p>I build <a href="https://minulo.app/en/?ref=hashnode-1022b">minulo</a>, an iPhone app that hides Reels and Shorts while keeping DMs. Hiding them is free.</p>
]]></content:encoded></item><item><title><![CDATA[Three ways to build a Reels blocker on iOS: content blocker, Safari extension, or WKWebView]]></title><description><![CDATA[I build minulo, an iPhone app that hides Instagram Reels and YouTube Shorts but keeps DMs. Before writing any code I had to choose how to do the hiding…]]></description><link>https://minulo.hashnode.dev/three-ways-to-build-a-reels-blocker-on-ios-content-blocker-safari-extension-or-wkwebview</link><guid isPermaLink="true">https://minulo.hashnode.dev/three-ways-to-build-a-reels-blocker-on-ios-content-blocker-safari-extension-or-wkwebview</guid><category><![CDATA[iOS]]></category><category><![CDATA[Swift]]></category><category><![CDATA[React Native]]></category><dc:creator><![CDATA[ayumu kitabayashi]]></dc:creator><pubDate>Thu, 22 Oct 2026 02:16:45 GMT</pubDate><content:encoded><![CDATA[<p>I build minulo, an iPhone app that hides Instagram Reels and YouTube Shorts but keeps DMs. Before writing any code I had to choose how to do the hiding, and on iOS you really only get three options. They differ less in what they can hide than in <strong>who can see the page</strong> and <strong>whether the user actually ends up in your code</strong>.</p>
<p>Here's how they compare, and what made me pick the third.</p>
<h2>First, what Screen Time can't do</h2>
<p>The obvious first idea is the Screen Time API (FamilyControls + ManagedSettings). It can shield an entire app, or an entire web domain in Safari. It can't reach inside an app: there's no way to say "Instagram, but not the Reels tab." The native Instagram and YouTube apps are closed to third parties, and on iOS there's nothing like Android's accessibility services that could read another app's screen.</p>
<p>So any Reels blocker on iOS works on the <strong>web version</strong> of the network. The question is just where that web page gets rendered.</p>
<h2>Option 1: Safari content blocker</h2>
<p>A content blocker is a JSON list of rules. Safari compiles it and applies it itself. Your code never runs on the page.</p>
<pre><code class="language-json">[{
  "trigger": { "url-filter": "^https://www\\.instagram\\.com/" },
  "action": { "type": "css-display-none", "selector": "a[href='/reels/']" }
}]
</code></pre>
<p><strong>Good:</strong></p>
<ul>
<li>It's private by design: the extension can't read the page, so there's nothing to leak and nothing to explain in a privacy policy.</li>
<li>It's fast, because Safari compiles the rules ahead of time.</li>
<li>It's cheap to maintain.</li>
</ul>
<p><strong>Limits I ran into while preparing the rules:</strong></p>
<ul>
<li>The only CSS you get is <code>display: none</code> on a selector. You can't change other properties, so you can't disable something without removing it. On one network's mobile site, the bottom tabs are laid out by position. Removing a tab with <code>display: none</code> didn't let its neighbours close the gap, and hiding the tab container instead left an empty grey box. Fixing that takes JavaScript.</li>
<li><code>url-filter</code> is a restricted regex. In particular, there's no <code>|</code> alternation, so "these two paths" means two rules.</li>
<li>Be strict about the format. As I understand Safari's parser, an unexpected key can get the whole list rejected, and nothing on the page tells you why. I keep metadata (which rule came from which network) in a separate file, in the same order as the list.</li>
</ul>
<h2>Option 2: Safari web extension (content script)</h2>
<p>A web extension can inject CSS and JavaScript, so it can handle the cases a content blocker can't: watching the DOM, disabling buttons, redirecting <code>/reels/</code> to the inbox.</p>
<p><strong>What you pay for that:</strong></p>
<ul>
<li>The script can read the page, and that includes the user's DMs on that site. Safari asks the user for per-site permission and warns that the extension can read the page. For an app that's supposed to make your phone calmer, that's a hard first screen.</li>
<li>You now maintain JavaScript against someone else's DOM, which changes without notice.</li>
</ul>
<p>My rule for that case is: anything that can be expressed as a selector stays in the content blocker. Only what really needs a script goes to the script.</p>
<h2>The problem both Safari options share</h2>
<p>Both only work <strong>in Safari</strong>. The habit we're trying to break is tapping the Instagram icon, and the icon opens the native app, where Safari extensions don't exist. The user has to remember to go to instagram.com in Safari every time, at exactly the moment they're least likely to.</p>
<p>For a desktop browser that's fine, since people already live in the browser there. On a phone it's the weak point.</p>
<h2>Option 3: your own app with a WKWebView</h2>
<p>This is what minulo does. The app opens the official web version of the network in a WKWebView and injects its own CSS and JS. Login happens on the network's own login page, inside that web view.</p>
<p>The part that makes this work on iPhone is combining it with Screen Time: minulo can shield the native Instagram app, and the shield screen offers a button that sends you into minulo instead. The icon habit lands somewhere without Reels. (I wrote about <a href="https://minulo.hashnode.dev/opening-your-own-app-from-a-screen-time-block-screen-on-ios-shieldaction-can-t-so-use-a-notification">getting from a shield screen into your own app</a>, which is harder than it sounds.)</p>
<p><strong>What I had to handle:</strong></p>
<p><strong>Inject twice.</strong> CSS goes in via <code>injectedJavaScriptBeforeContentLoaded</code> (react-native-webview), so Reels never flash on screen before the rules apply. Behaviour goes in after load, with a <code>MutationObserver</code>, because these sites keep adding DOM long after the first render.</p>
<p><strong>URL-scoped rules need re-applying.</strong> Injection runs once per document, but single-page apps change the URL without reloading. A rule meant only for <code>/explore/</code> either applies everywhere or nowhere unless the injected script watches the URL and swaps a scoped <code>&lt;style&gt;</code> itself.</p>
<p><strong>Selectors fail silently.</strong> When a site renames a class, CSS doesn't throw, it just stops matching. I count matches per rule and treat a rule that keeps matching zero as broken. (<a href="https://dev.to/ayumu_kitabayashi_8e0de96/hiding-instagram-reels-inside-a-wkwebview-css-fails-silently-so-count-it-2p3a">More on that here.</a>)</p>
<p><strong>Injected code runs in the page's world.</strong> The site's own JavaScript can call <code>window.ReactNativeWebView.postMessage</code> just as easily as yours can. Messages from the page to the app are untrusted, so the app checks them against a per-screen token and an allow-list of message types, and treats that as a first line of defence only.</p>
<p><strong>Send a current user agent.</strong> I had a fixed Safari user agent that was two iOS generations out of date. When I went through the App Store reviews of a similar web-view app, 18 of them mentioned suspicious-login warnings, bot checks or locked accounts. A stale user agent is an easy thing to stand out with, so the app now reports the same Safari version as the device it runs on.</p>
<h2>One rule source, three outputs</h2>
<p>I didn't want to choose forever, so the rules live in one TypeScript file and get exported:</p>
<ul>
<li>selector-only rules become content-blocker JSON</li>
<li>the few rules that need code become content-script JS</li>
<li>all of it is injected into the WKWebView in the app</li>
</ul>
<p>The same declaration (<code>urlFilter</code>, <code>css</code>, <code>redirect</code>) is read by all three, so the iOS app and a future browser extension are harder to let drift apart. Earlier, the app ignored <code>urlFilter</code> while the other outputs respected it, and on iOS a rule hid things on every page. With one declaration that all three read, that kind of mismatch shows up in one place instead of three.</p>
<h2>Summary</h2>
<table>
<thead>
<tr>
<th></th>
<th>Content blocker</th>
<th>Safari extension</th>
<th>WKWebView app</th>
</tr>
</thead>
<tbody><tr>
<td>Can read the page</td>
<td>No</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Can disable or redirect, not just hide</td>
<td>No</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Works when the user taps the Instagram icon</td>
<td>No</td>
<td>No</td>
<td>Yes, with a Screen Time shield</td>
</tr>
<tr>
<td>Maintenance</td>
<td>Low</td>
<td>Medium</td>
<td>High</td>
</tr>
<tr>
<td>Best fit</td>
<td>Desktop, privacy first</td>
<td>Desktop power users</td>
<td>Phones</td>
</tr>
</tbody></table>
<p>If you're building for desktop, start with a content blocker and add a content script only where you have to. On iPhone, the deciding factor is the icon: whatever you build has to be where the user's thumb goes.</p>
<hr />
<p>I build <a href="https://minulo.app/en/?ref=hashnode-1022">minulo</a>, an iPhone app that hides Reels and Shorts while keeping DMs. Hiding them is free.</p>
]]></content:encoded></item><item><title><![CDATA[How to turn off YouTube Shorts on iPhone in 2026 (and why no app can fully remove them)]]></title><description><![CDATA[I build minulo, an iPhone app that hides Reels and Shorts, so I spend a lot of time checking what YouTube itself lets you do. Short version for October…]]></description><link>https://minulo.hashnode.dev/how-to-turn-off-youtube-shorts-on-iphone-in-2026-and-why-no-app-can-fully-remove-them</link><guid isPermaLink="true">https://minulo.hashnode.dev/how-to-turn-off-youtube-shorts-on-iphone-in-2026-and-why-no-app-can-fully-remove-them</guid><category><![CDATA[iOS]]></category><category><![CDATA[youtube]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[ayumu kitabayashi]]></dc:creator><pubDate>Wed, 21 Oct 2026 02:51:36 GMT</pubDate><content:encoded><![CDATA[<p>I build minulo, an iPhone app that hides Reels and Shorts, so I spend a lot of time checking what YouTube itself lets you do. Short version for October 2026: there is now a real built-in setting, but it has gaps, and you should try it before installing anything.</p>
<h2>The built-in setting: Shorts feed limit = 0</h2>
<p>In the YouTube app, go to <strong>You → Settings → Time management → Shorts feed limit</strong> and pick <strong>0</strong>. YouTube opened the zero-minute option to everyone in April 2026.</p>
<p>What it does: once you hit the limit, the Shorts feed on Home and the Shorts tab stop.</p>
<p>What it doesn't do:</p>
<ul>
<li>On an adult account you can dismiss the limit and keep scrolling.</li>
<li>Shorts still appear in Subscriptions and in links people send you.</li>
<li>It's an app setting. youtube.com in Safari isn't covered.</li>
</ul>
<p>For most people this alone cuts a lot of Shorts time. If you find yourself dismissing it every evening, read on.</p>
<h2>The weaker options</h2>
<table>
<thead>
<tr>
<th>Option</th>
<th>What it does</th>
<th>The catch</th>
</tr>
</thead>
<tbody><tr>
<td>"Show fewer Shorts" (⋮ on a Shorts shelf)</td>
<td>Hides that shelf and signals you want fewer</td>
<td>YouTube doesn't say how long it lasts</td>
</tr>
<tr>
<td>Turn off watch history</td>
<td>Home stops showing recommendations</td>
<td>Not documented to remove the Shorts tab</td>
</tr>
<tr>
<td>Supervised teen account</td>
<td>Parents can set the Shorts timer, including zero</td>
<td>Only for supervised teens</td>
</tr>
<tr>
<td>iPhone Screen Time</td>
<td>Limits or blocks the whole YouTube app</td>
<td>Can't target Shorts only</td>
</tr>
</tbody></table>
<h2>Why no app can delete Shorts from the YouTube app</h2>
<p>iOS doesn't let one app change what another app draws. So only YouTube can remove Shorts from the official app. Third-party apps have two moves:</p>
<ol>
<li>Open YouTube's website in their own view and hide the Shorts entry points.</li>
<li>Use Screen Time to shield the official app.</li>
</ol>
<p>The first move breaks quietly when YouTube changes its markup. In minulo I count how many elements got hidden on each page load, so I notice when that number drops to zero.</p>
<h2>Where minulo fits</h2>
<p>minulo does both moves: YouTube opens in minulo with Shorts hidden (regular videos, search and subscriptions still work), and the official app can be shielded with Screen Time so the block screen sends you to the Shorts-free view. Hiding is free. UNDOOMED, Nope and LoomWeb take similar approaches, so compare a few.</p>
<p>If the 0-minute limit already works for you, you don't need any of this.</p>
<p>Full list with sources: <a href="https://minulo.app/en/block-youtube-shorts.html?ref=hashnode-1021">How to block YouTube Shorts on iPhone and keep normal videos</a></p>
]]></content:encoded></item><item><title><![CDATA[Opening your own app from a Screen Time block screen on iOS (ShieldAction can't, so use a notification)]]></title><description><![CDATA[I build minulo on my own, an iPhone app that hides Instagram Reels, YouTube Shorts and TikTok's For You while DMs and search keep working. One feature uses…]]></description><link>https://minulo.hashnode.dev/opening-your-own-app-from-a-screen-time-block-screen-on-ios-shieldaction-can-t-so-use-a-notification</link><guid isPermaLink="true">https://minulo.hashnode.dev/opening-your-own-app-from-a-screen-time-block-screen-on-ios-shieldaction-can-t-so-use-a-notification</guid><category><![CDATA[iOS]]></category><category><![CDATA[Swift]]></category><category><![CDATA[React Native]]></category><dc:creator><![CDATA[ayumu kitabayashi]]></dc:creator><pubDate>Tue, 20 Oct 2026 07:28:37 GMT</pubDate><content:encoded><![CDATA[<p>I build minulo on my own, an iPhone app that hides Instagram Reels, YouTube Shorts and TikTok's For You while DMs and search keep working. One feature uses Screen Time (FamilyControls / ManagedSettings) to lock the official apps, and the block screen offers an "Open in minulo" button. Getting that button to actually open my app took a few detours. These are notes for anyone building the same thing.</p>
<p>The app is React Native (Expo). The Screen Time parts start from react-native-device-activity by Robert Herber, with my own changes on top.</p>
<h2>What I wanted</h2>
<ol>
<li>Lock Instagram and other apps with a ManagedSettings shield.</li>
<li>When the user taps the Instagram icon, show a custom block screen (ShieldConfiguration).</li>
<li>The primary button, "Open in minulo", opens my app on the Reels-free version of Instagram.</li>
</ol>
<h2>1. Opening a URL from ShieldAction does nothing</h2>
<p>Button taps arrive in <code>ShieldActionDelegate.handle(action:for:completionHandler:)</code>. Trying to open my own URL scheme from there:</p>
<pre><code class="language-swift">let context = NSExtensionContext()
context.open(url) { _ in }
</code></pre>
<p>did nothing on a real device. No error, the request is silently ignored. Assume there is no direct way to open another app, including your own, from a shield extension.</p>
<h2>2. Post a local notification and let the user tap it</h2>
<p>Instead, the ShieldAction extension posts one local notification. Tapping it opens the app.</p>
<pre><code class="language-swift">sendNotification(contents: [
  "title": title,              // localized text the app stored in the App Group
  "body": body,
  "userInfo": ["url": url],    // the app routes to this URL
  "identifier": "minulo-open", // same id, so it replaces the previous one
  "interruptionLevel": "active",
], placeholders: [:])
</code></pre>
<p>The app handles the notification tap and opens <code>userInfo.url</code>. The flow becomes two taps: the button on the block screen, then the banner that drops in right away.</p>
<h2>3. Wait a little before calling completionHandler</h2>
<p>Calling <code>completionHandler</code> lets the system tear the extension down, which can happen before the notification is scheduled. For the notification button only, I wait 0.4 seconds and return <code>.defer</code>, so the block screen stays up during the wait.</p>
<pre><code class="language-swift">DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
  completionHandler(.defer)
}
</code></pre>
<h2>4. Without notification permission, the button does nothing</h2>
<p>Because the flow goes through a notification, a user who denied notifications gets a button that does nothing, which is the worst outcome. The app writes the permission state to the App Group, and when notifications are off, both the ShieldConfiguration and ShieldAction extensions show and handle a single "Close" button instead.</p>
<h2>5. ShieldConfiguration cannot write to the App Group</h2>
<p>I wanted to count how often the block screen appears, so I wrote to the App Group's UserDefaults from the ShieldConfiguration extension. The writes were silently dropped (analytics showed "none" for every event). Reading works; writing does not. I count in ShieldAction instead, where the button tap happens.</p>
<h2>6. The app name is not always passed in</h2>
<p>I put the blocked app's name into the block screen text and the button URL. Apple sometimes does not pass the name to the extension, so plan for it to be empty. Depending on how you substitute placeholders, the literal string "applicationName" can end up in the text.</p>
<h2>7. Percent-encoding non-ASCII names</h2>
<p><code>CharacterSet.alphanumerics</code> includes non-ASCII letters (Japanese, for example), so names pass through unencoded. I use an explicit ASCII set:</p>
<pre><code class="language-swift">let allowed = CharacterSet(charactersIn:
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~")
</code></pre>
<h2>Summary</h2>
<ul>
<li>A ShieldAction extension cannot open your app directly. Go through a local notification.</li>
<li>Wait briefly and return <code>.defer</code> so the extension is not torn down before the notification is posted.</li>
<li>No notification permission means a dead button. Show only "Close" in that case.</li>
<li>ShieldConfiguration can read the App Group but cannot write to it.</li>
</ul>
<p>minulo on the App Store (hiding is free): <a href="https://apps.apple.com/app/id6809352229">https://apps.apple.com/app/id6809352229</a>
A neutral list of apps and settings that hide Reels and Shorts: <a href="https://github.com/ayumu-kitabayashi/awesome-reels-shorts-blockers">https://github.com/ayumu-kitabayashi/awesome-reels-shorts-blockers</a></p>
]]></content:encoded></item></channel></rss>