For weeks, ChatGPT was quietly acting as an iOS app's best salesperson, sending roughly a third of all its downloads. Then, almost overnight, it went silent.
No bad review. No App Store penalty. No new version. Nothing in the app had changed.
The website had moved to a new address. It was a routine, by-the-book migration, the kind that's usually forgotten by lunchtime. Within days, the app's biggest traffic source had dropped to close to zero, and total downloads were cut in half.
Here's how a "harmless" domain move wiped out a ChatGPT traffic source, the one-line regex bug that quietly made it worse, and the checklist that would have prevented all of it.
The "textbook" migration
The app is a small iPhone utility with a marketing site: a homepage, feature pages, comparison pages and about a dozen how-to guides. The site was originally served from a Cloudflare Workers subdomain (something.workers.dev).
In early October it was rebuilt with Astro and moved to a Cloudflare Pages subdomain (something.pages.dev). As part of the rebuild:
- canonical tags,
og:url, JSON-LD, the sitemap androbots.txtall switched to the new host - URLs changed shape:
/guides/foo.htmlbecame/guides/foo, and/guides/became/guides - the old Workers host became a small redirect Worker that sends visitors to the new host
On paper, it was flawless. In practice, it was about to go badly wrong.
The day the referrals stopped
App Store Connect reports first-time downloads by source. One source is App Referrer, meaning another app sent the user to the App Store, and ChatGPT appears there by name.
| Period | Share of downloads from App Referrer (mostly ChatGPT) | Daily downloads |
|---|---|---|
| Several weeks before the move | roughly a third | baseline |
| A couple of weeks after the move | close to zero | roughly half of baseline |
App Store search dipped only modestly, so the main loss was ChatGPT. The referrals stopped within days of the canonical change. (All figures here are rounded on purpose.)
This is a correlation, not a controlled experiment. Still, no other change lined up: the listing, keywords and app version stayed the same through that window.
Why AI assistants punish a domain move harder than Google does
ChatGPT's web answers rely heavily on Bing's index. When a page that recommended the app was indexed under the old host, ChatGPT could find it, cite it and link to the App Store.
After the move:
- The old URLs told Bing "the real page is now elsewhere" (new canonicals and redirects).
- The new host was brand new to Bing: no history and no inbound links.
- Bing didn't hand the old pages' standing to the new host quickly. The old pages lost value, and the new ones had none yet.
Google handled it a bit better and indexed the new homepage within a few weeks. Bing's URL Inspection showed something more worrying:
Blocked – URL cannot appear on Bing.
"The inspected URL is known to Bing but has some issues which are preventing us from serving it."
The Live URL test for the same page passed: "URL can be indexed by Bing", "No SEO/GEO issues found". So it wasn't a technical block. It was a quality or trust decision about a new, unlinked site, and the Bing dashboard said as much: "Your site lacks inbound links from high-quality domains."
The one-line bug hiding in plain sight
The redirect Worker on the old host looked roughly like this:
const TARGET = 'https://new-host.example';
// Some pages must still be served in place (older app builds load them in a web view).
const PROXIED_PAGES = new Set(['/privacy', '/terms']);
// "Assets" are proxied so the proxied pages can load CSS and images.
const isAsset = (pathname) =>
pathname.startsWith('/_astro/') || /\.[a-z0-9]+$/i.test(pathname);
export default {
async fetch(request) {
const { pathname, search } = new URL(request.url);
if (PROXIED_PAGES.has(pathname) || isAsset(pathname)) {
return fetch(TARGET + pathname + search); // served as 200
}
return Response.redirect(TARGET + pathname + search, 301);
},
};
Spot it? It's easy to miss. The "is it an asset?" regex matches any file extension, including .html. Every old /guides/foo.html URL, which were exactly the URLs search engines had indexed, was proxied with a 200 instead of redirected with a 301.
So the old host kept serving full copies of every page, while those copies' canonical tags pointed at the new host. To a crawler, that looks like two sites with the same content, one of them disowning itself. That's a good way to make a search engine distrust both.
The fix is small: exclude HTML from the asset rule, and map old URL shapes straight to the final URL in one hop.
const isAsset = (pathname) =>
pathname.startsWith('/_astro/') ||
(/\.[a-z0-9]+$/i.test(pathname) && !/\.html?$/i.test(pathname));
// /index.html → /, /guides/ → /guides, /guides/foo.html → /guides/foo
const canonicalPath = (pathname) => {
let path = pathname.replace(/\/index\.html?$/i, '/').replace(/\.html?$/i, '');
if (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1);
return path || '/';
};
// ...
return Response.redirect(TARGET + canonicalPath(pathname) + search, 301);
A quick way to test every legacy URL, using the old sitemap from git history:
while read p; do
curl -s -o /dev/null -w "$p -> %{http_code} %{redirect_url}\n" "https://old-host.example$p"
done < old-paths.txt
Every line should show 301 pointing at the matching new URL. Following the redirect (curl -L) should end on a 200.
Two more traps waiting in Search Console
-
Google Search Console showed "Couldn't fetch" for a sitemap that worked. The sitemap returned
200with valid XML, even when fetched with a Googlebot user agent. The status stayed stuck with no "Last read" date for over a week. It's a known display quirk on new properties: the file is fine, Google just hasn't fetched it yet. Resubmitting with a query string (sitemap-0.xml?v=2) creates a new entry, androbots.txtshould list the sitemap anyway. - "Request indexing" hit "Quota exceeded" on the very first click. On new properties this happens often. Don't fight it: the sitemap and internal links from an already-indexed page do the same job without a quota.
The checklist that would have saved the traffic
- Don't move unless you have to. If the old address is indexed and cited, its history is an asset.
-
If you move, move once, ideally to a domain you own. Shared hosts like
*.pages.devor*.workers.devstart with little trust, and every later move resets it. - Redirect every old URL with a single 301 to its exact new equivalent. Test the real legacy URLs (from the old sitemap in git), not just the homepage.
- Never serve full copies of pages from the old host, even by accident through an asset rule.
- Set up Bing Webmaster Tools and Google Search Console before the move, not after. Bing has a site-move tool, and you'll want the before-and-after data.
- Use IndexNow to tell Bing about changed URLs immediately.
- Plan for inbound links. A new host with no backlinks may be crawled but not shown. Directory listings, launch sites and a few relevant articles help.
Where it stands now
The redirect fix is live, every legacy URL now returns a clean 301, a Bing review has been requested, and the backlink work has started. Recovery isn't guaranteed or quick. Search engines rebuild trust over weeks, not days. A follow-up will share what actually brought the traffic back.
The app in this story is Clean Kit, an iPhone photo and storage cleaner that runs entirely on-device. It's on the App Store.
Top comments (9)
This one hit close to home — the regex bug is brutal. .html pages proxied with a 200 while their canonicals point at the new host is exactly the kind of silent split that takes weeks to notice. The Bing angle is the part most people miss: Google forgiving a move in weeks while Bing treats the new host as untrusted with no history. Your point about verifying the old URLs actually 301 (not 200) is the one I'd tattoo on every migration runbook.
the regex was indeed brutal for the downside
Curious whether others have seen this. Has anyone tracked ChatGPT or Perplexity referrals before and after a domain or URL change? The before/after was surprisingly sharp here, and it'd be good to know if Bing re-trust usually takes weeks or months.
The regex catch is the part I would steal for my own checklist: test the old sitemap URLs from git history, not just the homepage. A single curl loop showing 301 to the exact new URL would have caught the .html pages being proxied as 200s before Bing did.
The .html matching the asset regex is such a clean trap: the old host kept serving 200 copies whose canonicals disowned themselves. Did anything in Bing's tools ever point at that, or did it only show up once you curled the legacy paths from the old sitemap? I'd have trusted that passing Live URL test too.
That isAsset regex is such a believable bug. Anyone reviewing it reads "anything with a file extension" and pictures CSS and images, not the .html URLs that carried all the search history. I like that the test loop runs over the old sitemap from git instead of a hand-picked list. I'd keep that script and rerun it for a few weeks after the move, because a later tweak to the Worker can quietly turn some of those 301s back into 200s. Did Bing's URL Inspection change at all once the proxied copies were gone?
The obsession with wrapping remote APIs into multi-layered agent abstractions is creating massive hidden latency and failure cascades in production.
If you profile real-world workloads, bare-metal local inference (via direct C++ / llama.cpp or vLLM bindings) consistently beats fragile cloud wrappers on both predictability and cost. Tooling should eliminate abstraction penalties, not compound them.
Really useful post. Worth adding for anyone reading: ChatGPT's search leans heavily on Bing's index, so after a move, verify the new host in Bing Webmaster Tools too (not only Google Search Console), submit the sitemap there, and ping IndexNow on every deploy. Bing tends to be slower to pick up a move on its own than Google.
This is such a great reminder that sometimes the smallest technical details can have the biggest real-world consequences. What I find particularly interesting is how a seemingly harmless domain migration and a tiny regex oversight ended up affecting an entire app's visibility and downloads. The lesson goes beyond SEO — it's about understanding how interconnected our systems really are. Also, appreciate you sharing the actual debugging process rather than just the outcome. Real-world engineering lessons like this are incredibly valuable for developers and indie builders. Great write-up! 👏