I Migrated a 900-Page Site to a New Domain. Here’s Everything That Broke.
Written by Avinash Chandran

In December 2025, I migrated an entire product site from its own domain to a subdomain under a parent brand. suvit.io became taxone.vyapar.com. 900+ pages. Full rebrand. New DNS, new CDN, new tracking, new Search Console property.
Seven months later, the site is recovering and trending upward. But the migration surfaced problems I didn’t expect, broke things I thought were fine, and taught me lessons I haven’t seen covered well anywhere else.
This is the full story. I’m writing it as the reference I wish I had before I started.
Why We Moved#
Suvit was an AI-powered accounting platform for Chartered Accountants in India. It served 10,000+ CA firms and 30,000+ accountants. It had its own domain, its own brand, and a decent organic presence. But the parent company, Vyapar, had a much stronger brand in the Indian accounting software market.
Running Suvit as a standalone product meant competing for the same audience without leveraging Vyapar’s recognition or domain authority. The decision was to bring Suvit under the Vyapar umbrella as “Vyapar TaxOne” on taxone.vyapar.com.
Three things drove it:
| Driver | Rationale |
|---|---|
| Brand consolidation | Vyapar had established trust in the Indian accounting market. Suvit was competing for the same audience without leveraging that recognition. |
| Domain authority | A subdomain under vyapar.com would inherit some trust and backlink equity. A standalone .io domain would never get that head start. |
| Product positioning | Vyapar was building a suite of accounting tools. TaxOne needed to sit inside that ecosystem as the CA-facing product, not beside it. |
The trade-off was obvious: short-term organic traffic loss in exchange for long-term brand and authority gains.
What Got Migrated#
This wasn’t a simple redirect. The migration touched every layer of the web presence:
| Component | Before | After |
|---|---|---|
| Main domain | suvit.io | taxone.vyapar.com |
| CDN | static.suvit.io | static.taxone.vyapar.com |
| App URLs | in.suvit.io | taxone.vyapar.com/app/ |
| Help center | help.suvit.io | taxone.vyapar.com/help/ |
| Content pages | 900+ pages with “Suvit” branding | All rebranded to “Vyapar TaxOne” |
| Tracking | GTM + GA4 on suvit.io | Reconfigured for taxone.vyapar.com |
| Search Console | suvit.io property | New taxone.vyapar.com property (old kept active) |
| DNS & SSL | Suvit Cloudflare config | New Cloudflare DNS + SSL under Vyapar |
Planning Phase (October–November 2025)#
Before touching anything, I spent two months on groundwork.
Baseline data collection. I pulled 5 months of GSC performance data (July to November 2025) as the pre-migration benchmark. Clicks, impressions, CTR, average position. Without this baseline, there’s no way to measure recovery.
Content audit. I mapped all 900+ pages that needed brand reference updates. Blog posts, help center articles, business pages. Every meta title, meta description, OG tag, internal link, and body mention of “Suvit” had to change.
Migration tooling. I built a custom HTML-based migration tool for bulk content transformation. It ran five rule types:
| Rule | What It Did |
|---|---|
| Brand name replacement | Suvit → Vyapar TaxOne (title case, lowercase, uppercase variants) |
| App URL mapping | in.suvit.io → taxone.vyapar.com/app/ |
| Help Center URL mapping | help.suvit.io → taxone.vyapar.com/help/ |
| CDN/image URL flagging | Flagged static.suvit.io references for manual review |
| Catch-all domain replacement | Any remaining suvit.io → taxone.vyapar.com |
One early lesson: rule ordering is critical. The brand replacement rule was corrupting URLs when it fired before the URL rules. “suvit.io” was turning into “vyapar taxone.io” before the URL rule could match it. URL rules must always execute before brand-name rules.
Redirect mapping. I documented every URL pattern that needed 301 redirects: main domain, CDN paths, app URLs, help center URLs, and a catch-all for any unmapped Suvit URLs.
Technical baseline. A full SEMrush site audit on the existing Suvit domain established technical health before migration. Crawl errors, indexing status, Core Web Vitals. This became the comparison point for every post-migration audit.
Execution (December 2025)#
The migration was a coordinated effort between SEO and the development team. Four things happened in sequence:
1. Domain cutover. The core 301 redirect chain from suvit.io to taxone.vyapar.com went live. Every old URL needed to resolve to its TaxOne equivalent without breaking internal links, bookmarks, or indexed pages.
2. CDN migration. Static assets moved from static.suvit.io to static.taxone.vyapar.com with proper CNAME records and SSL via Cloudflare.
3. Content rebranding. All blog posts, help center articles, and business pages were run through the migration tool. Meta titles, descriptions, OG tags, internal links, and body content were all updated through Strapi (the CMS), with manual updates for business pages and the homepage.
4. GSC and sitemap. New Search Console property verified for taxone.vyapar.com. XML sitemap regenerated and submitted. Old Suvit GSC property kept active for monitoring redirect health.
Everything That Broke#
This is where it gets interesting. Several issues surfaced that weren’t visible during planning.
1. robots.txt was silently killing every image on the site#
The deployed robots.txt had three rules that looked perfectly reasonable:
Disallow: /_next/
Disallow: /*?*
Disallow: /static/Block framework internals, query string URLs, and static directories. Makes sense on paper.
Except this was a Next.js site. In Next.js, every optimized image is served through a URL pattern like this:
/_next/image?url=https://static.taxone.vyapar.com/image.png&w=640&q=75The /_next/ rule blocked the path. The /*?* rule blocked the query string. Between them, not a single image on the entire site could be crawled or indexed.
I found this during a post-migration SEMrush audit that flagged 6,819 image resources. My first assumption was old CDN references. Deeper investigation showed that even after fixing the CDN, images would remain invisible to Google because of these robots.txt rules.
The fix:
# BEFORE (broken)
Disallow: /_next/
Disallow: /*?*
Disallow: /static/
# AFTER (fixed)
Disallow: /_next/
Allow: /_next/image*
Disallow: /*?utm_*
Disallow: /blog?*The blanket /*?* rule was removed entirely. Specific blocks were added for UTM parameters and blog filter URLs. The /_next/ rule was kept but /_next/image* was explicitly allowed.
Why this matters beyond my migration: This issue had been silently hurting the old Suvit domain too. It wasn’t caused by the migration. It was legacy technical debt that nobody caught because nobody tested robots.txt rules against the URL patterns Next.js actually generates. If you’re running a Next.js site, go check yours right now.
2. React client-side routes returning 404s to crawlers#
The sign-in and sign-up pages (/app/signIn and /app/signUp) are React client-side routes. They work perfectly in a browser. JavaScript loads, React Router renders the page, users see the login form.
But when a search engine crawler hits these URLs, there’s no JavaScript execution. The server returns a raw 404.
These are important pages. Internal links across the site point to them from hero sections, headers, and footers. SEMrush flagged them as broken links, dragging down the site’s health score.
There was an internal debate about this. A developer tested with:
curl -I -H "Accept: text/html" https://taxone.vyapar.com/app/signInIt returned a 200. “See? It works.” But Googlebot doesn’t send Accept: text/html headers. That curl test was hitting a different code path than what bots actually experience. Here’s what you should test with instead:
# This is closer to what Googlebot actually does
curl -I -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" https://taxone.vyapar.com/app/signInThe fix: Server-side rewrites were configured to return a 200 with appropriate meta tags for /app/signIn and /app/signUp specifically. The robots.txt was updated:
Disallow: /app/
Allow: /app/signIn
Allow: /app/signUpThe Allow rules override the broader Disallow: /app/ for those specific CTA destination pages.
The lesson: Client-side routing is invisible to crawlers. Any page you want Google to access must have a server-side response. And curl -I with custom Accept headers is not a valid substitute for testing actual crawler behavior.
3. WWW vs non-WWW duplicate content#
Both www.taxone.vyapar.com and taxone.vyapar.com were serving full page content. No redirect. Google was seeing two versions of every page, splitting link equity and creating canonicalization confusion.
I caught this during the homepage audit when a fetch of the www version returned full HTML instead of a redirect.
The fix: A Cloudflare redirect rule enforcing non-www as canonical:
# Cloudflare Page Rule
www.taxone.vyapar.com/*
→ 301 Redirect to https://taxone.vyapar.com/$1The $1 preserves the full path. Every www URL now 301s to its non-www equivalent.
4. 6,819 old CDN references scattered across the codebase#
Even after migrating the CDN, references to static.suvit.io persisted in multiple places:
| Location | What Was Found |
|---|---|
| next.config.js | Old CDN domain in remotePatterns |
| MDX content files | Hardcoded image paths referencing old CDN |
| Strapi CMS entries | Blog and help center images pointing to old CDN |
| Hardcoded image paths | Inline <img> tags in business page templates |
The old CDN’s robots.txt was also blocking crawlers. So even if Google somehow resolved the old URLs, it couldn’t crawl the images.
The fix: A codebase-wide find-and-replace of static.suvit.io → static.taxone.vyapar.com, plus updating the Next.js config:
// next.config.js — before
remotePatterns: [
{ hostname: 'static.suvit.io' }
]
// next.config.js — after
remotePatterns: [
{ hostname: 'static.taxone.vyapar.com' }
]Post-deploy crawl confirmed the old references were cleared.
5. Cloudflare email protection URLs triggering false 404s#
Cloudflare’s email protection feature generates /cdn-cgi/l/email-protection URLs whenever an email address appears on a page. SEMrush picked these up as 404 errors, inflating the error count.
# Fix: block crawlers from Cloudflare internal paths
Disallow: /cdn-cgi/Simple, but it cleaned up dozens of false positives in the audit report.
6. 878 toxic backlinks from AWS spam farms#
GSC’s linking domains report showed a massive cluster of suspicious domains. All bare AWS EC2 IP addresses across multiple regions (US-East, Ireland, Sydney, Osaka, Mumbai). Adult and spam sites linking to the same 19 target pages with clustered link counts.
I extracted, deduplicated, and validated 878 unique IP addresses. Important detail: bare IPs require the domain: prefix in Google’s disavow file. They can’t be listed as raw URLs:
# Disavow file format for bare IPs
# WRONG:
http://54.236.xxx.xxx
# RIGHT:
domain:54.236.xxx.xxxTwo things to remember about disavow files. First, AWS recycles IP addresses. The IPs you disavow today will eventually point to completely different servers. These files need periodic refresh. Second, disavowing is only meaningful if a manual action exists or rankings have demonstrably dropped. Always check the Manual Actions report in GSC before investing effort in a disavow.
The Final robots.txt#
After multiple iterations, here’s what the production robots.txt looked like:
User-agent: *
# CTA pages (override the /app/ block)
Allow: /app/signIn
Allow: /app/signUp
# Block application dashboard
Disallow: /app/
# Block Cloudflare internals
Disallow: /cdn-cgi/
# Block API, admin, dashboard
Disallow: /api/
Disallow: /admin/
Disallow: /dashboard/
# Block paid ad landing pages
Disallow: /accounting-automation-tool*
Disallow: /gst-automation*
# Block UTM parameters
Disallow: /*?utm_*
# Block blog filter/sort URLs
Disallow: /blog?*
# Allow Next.js image optimization
Allow: /_next/image*
Disallow: /_next/
Sitemap: https://taxone.vyapar.com/sitemap.xmlEvery rule here exists because of a specific problem that was found and fixed. None of them are boilerplate.
GSC Performance: Before vs After#
Pre-migration baseline: July to November 2025 (5 months, Suvit). Post-migration: January to June 2026 (6 months, TaxOne). December 2025 excluded as the active transition month.
| Metric | Before (Suvit) | After (TaxOne) | Change |
|---|---|---|---|
| Total Clicks | 451K | 347K | -23% |
| Total Impressions | 11.9M | 9.65M | -19% |
| Average CTR | 3.8% | 3.6% | -0.2pp |
| Average Position | 13.7 | 7.6 | See caveat below |
How to read these numbers#
The click and impression drops are expected. Every domain migration resets the trust Google built for the old domain over years. A 23% click drop over a comparable period is within the normal range. Some migrations see 40 to 60% drops.
The average position shift from 13.7 to 7.6 is a statistical artifact, not a ranking win. Here’s what actually happened: when total impressions drop by 19%, the queries that disappear first are the long-tail, low-ranking ones sitting at position 30+, 50+, 80+. Those queries were dragging the average down on the old domain. When they fall off post-migration, the remaining query pool naturally shows better average positions. Not because individual keywords ranked higher, but because the underperforming tail got pruned from the dataset.
If you ever report a domain migration and see average position “improve” alongside an impression drop, do not frame it as a win. Validate with keyword-level data first.
CTR holding nearly flat (3.8% → 3.6%) is the real positive signal. It means the pages that are ranking are maintaining their click-through quality. The titles and descriptions survived the rebrand.
Traffic recovery timeline#
| Period | What Happened |
|---|---|
| January–February 2026 | Initial dip. Expected. New domain is starting fresh in Google’s eyes. |
| March 2026 | Stabilization. The floor was found. |
| April–June 2026 | Visible upward trend in clicks and impressions. Recovery phase begins. |
| July 2026 (current) | Trending toward pre-migration levels. Trajectory is clearly positive. |
Post-Migration Work (January–June 2026)#
Migration day is not the finish line. It’s the start of a monitoring and remediation phase that lasted months.
GTM and GA4 reconfiguration. The tracking setup was rebuilt for the new domain with two key events:
| Event | Trigger |
|---|---|
cta_click | Fires on clicks of “Start Free Trial,” “Sign up now,” and “Sign In” across Hero, Header, Mid Page, and Footer sections |
signup_initiated | Fires when users land on /app/signUp or /app/signIn |
Homepage content optimization. The original H2s were brand-narrative driven (“One AI Accounting Platform, Complete Control on Filing and Compliance”) rather than keyword-anchored. I rewrote them to target search terms like “accounting software for CA” and “GST automation for CAs.” Also identified that the FAQ section existed in HTML but had no FAQ JSON-LD schema, losing out on potential rich results.
Help center QA. A thorough content review uncovered incorrect domain references (taxone.io instead of taxone.vyapar.com), spelling errors, grammar issues, and a factual inconsistency in the password lockout threshold that needed product-side confirmation.
llms.txt for AI discoverability. I created a structured llms.txt file following the llmstxt.org specification. It’s a markdown file served at the site root that helps LLMs understand the site’s content structure. It included core pages, features, help center collections, blog articles, and ecosystem context, mirroring the indexable content set while excluding paths blocked in robots.txt.
What I’d Do Differently#
I’d audit the robots.txt against actual framework URL patterns before migration day. The image indexing issue had been silently hurting the old domain too. Nobody caught it because nobody tested the rules against the URLs Next.js actually generates.
I’d set up server-side route handling for all CTA destination pages before the cutover, not after. Every day those pages returned 404s to crawlers was a day of broken internal link signals.
I’d build the disavow file review into a quarterly process from the start. Submitting it once and forgetting about it defeats the purpose when the IPs get recycled.
And I’d keep the brand transition banner (“Suvit Is Now Vyapar TaxOne”) above the fold for at least 6 months. Ours was removed during a redesign earlier than I’d have liked. Users arriving via branded “Suvit” searches during the transition period needed that signal.
The Migration Checklist#
If you’re planning a domain migration, here’s the sequence I’d follow based on what I learned:
| Phase | Task | Why It Matters |
|---|---|---|
| Pre-migration | Pull 3–6 months of GSC baseline data | You can’t measure recovery without a benchmark |
| Pre-migration | Audit robots.txt against your framework’s actual URL patterns | Next.js, Nuxt, and other frameworks generate URLs that look like internals but aren’t |
| Pre-migration | Map every URL pattern that needs 301 redirects | Include CDN, app, help center, and catch-all patterns |
| Pre-migration | Run a full site audit (SEMrush, Screaming Frog, etc.) | Establish technical health baseline for comparison |
| Pre-migration | Build and test content transformation tooling | 900+ pages can’t be manually updated. Rule ordering matters. |
| Execution | Deploy 301 redirects | Every old URL must resolve to its new equivalent |
| Execution | Migrate CDN and update all codebase references | Search the entire codebase. Config files, CMS entries, MDX, hardcoded paths. |
| Execution | Rebrand all content (meta, OG, body, internal links) | Missed references create brand confusion and broken links |
| Execution | Verify new GSC property and submit sitemap | Keep old property active for redirect monitoring |
| Execution | Enforce www vs non-www redirect | Duplicate content from day one otherwise |
| Post-migration | Run a full crawl audit on the new domain | Problems invisible during planning will surface here |
| Post-migration | Fix server-side handling for client-side routes | Any CTA or important page using React Router needs a server response |
| Post-migration | Audit and clean toxic backlinks | Check Manual Actions first. Use domain: prefix for bare IPs. |
| Post-migration | Reconfigure GTM/GA4 tracking | Tracking breaks silently. Verify events are firing on the new domain. |
| Post-migration | Keep brand transition messaging visible | Users on old branded queries need a clear signal for 6+ months |
Where It Stands Now#
Seven months in, the new domain is trending back toward pre-migration traffic levels. But more importantly, it’s running on a cleaner technical foundation than the old domain ever had. The robots.txt is properly scoped. Client-side routes have server-side fallbacks. CDN references are unified. WWW/non-www is resolved. 878 toxic backlinks are disavowed.
The migration wasn’t just a domain swap. It was an accidental technical debt cleanup that the old site had been carrying for years. The recovery isn’t about getting back to where Suvit was. It’s about building on infrastructure that can actually support the next phase of growth.
If you’re facing a domain migration, the most useful thing I can tell you is this: the problems you’ll spend the most time on are not the ones you planned for. They’re the ones hiding in your robots.txt, your framework’s URL patterns, and the gap between how your React app looks in a browser and how it looks to a crawler. Plan for the redirect chain. But budget twice as much time for everything that breaks after.