Migrating a website from one domain to another is one of the most critical moments for a business's SEO. A poorly executed migration can wipe out months or years of organic positioning in days.
This article documents the exact process I followed to migrate a local business site from shopgreenway.com to ecoharbor.com, preserving 100% of organic traffic using 301 redirects on Cloudflare. The names are fictional, but the case and metrics are real.
The Scenario
A business with two physical locations decides to unify its online presence under a single brand. They have:
- 9 indexed URLs in Google (home, about, contact, blog, 3 articles, category)
- 2 years of content with stable organic positioning
- Consistent weekly traffic from local searches
- A new site with a similar structure but different slugs on some pages
The goal: redirect every old URL to its closest equivalent on the new site, with zero traffic loss. The old domain stays active for 3-4 months while Google processes the migration.
Step 1: URL Inventory (Sitemap Audit)
Everything starts with the sitemap. I downloaded both sitemap.xml files (old and new) and extracted all indexed URLs. Small to medium sites typically have between 5 and 25 URLs that need direct mapping.
The inventory revealed:
| Type | Count | Example |
|---|---|---|
| Main pages | 4 | /, /about/, /contact/, /blog/ |
| Blog articles | 3 | /getting-started-organic-products/ |
| Categories / Taxonomies | 2 | /category/blog/, author archives |
Step 2: Mapping Equivalences
Not every route has a 1:1 equivalent between sites. In this case:
- Home: Direct 1:1 ✅
- About: Direct 1:1 ✅
- Contact: Slug changed from
/contact-us/to/contact/⚠️ - Blog: Direct 1:1 ✅
- 3 articles: No equivalent content on the new site ❌ → redirect to closest blog category
- Categories: Don't exist as a taxonomy on the new site → redirect to blog archive
Step 3: Implementation on Cloudflare
Cloudflare provides two main mechanisms for redirects:
Option A: Page Rules (Easy, up to 3 free)
Ideal for small migrations with few routes:
# Page Rule: shopgreenway.com/* →
# Forward URL (301) → ecoharbor.com/
# Exceptions for specific routes via Worker/another rule Option B: Cloudflare Workers (Recommended for fine control)
For this case, I used a Worker with explicit routing logic. This avoids the catch-all problem that sends everything to the home page:
// Conceptual Worker fragment
const redirects = {
'/': '/',
'/about-us/': '/about-us/',
'/contact-us/': '/contact/',
'/blogs/': '/blogs/',
'/category/blog/': '/blogs/',
};
// Articles without equivalents → thematic blog category
const blogPatterns = [
{ pattern: '/getting-started/', target: '/blogs/category-101/' },
{ pattern: '/oil-benefits/', target: '/blogs/category-101/' },
{ pattern: '/beginners-guide/', target: '/blogs/category-101/' },
];
async function handleRequest(request) {
const url = new URL(request.url);
const path = url.pathname;
if (redirects[path]) {
return Response.redirect(
'https://ecoharbor.com' + redirects[path], 301
);
}
for (const { pattern, target } of blogPatterns) {
if (path.startsWith(pattern)) {
return Response.redirect(
'https://ecoharbor.com' + target, 301
);
}
}
// Fallback: home
return Response.redirect('https://ecoharbor.com/', 301);
} Step 4: Rigorous Validation
Once redirects were implemented, I validated every single one from the terminal:
for url in \
"https://shopgreenway.com/" \
"https://shopgreenway.com/about-us/" \
"https://shopgreenway.com/contact-us/"; do
curl -sI -o /dev/null -w "%{http_code} %{redirect_url}" "$url"
echo
done This script revealed 3 bugs in my first iteration:
- The catch-all was overwriting the
/about-us/redirect (went to home instead of about) /blogs/was also falling into the generic catch-all/category/blog/had the same issue — I fixed all three by placing specific rules before the catch-all
Results
After corrections, all 8 redirects were 100% validated:
| Source | Destination | Status |
|---|---|---|
shopgreenway.com/ | ecoharbor.com/ | 301 ✅ |
shopgreenway.com/about-us/ | ecoharbor.com/about-us/ | 301 ✅ |
shopgreenway.com/contact-us/ | ecoharbor.com/contact/ | 301 ✅ |
shopgreenway.com/blogs/ | ecoharbor.com/blogs/ | 301 ✅ |
shopgreenway.com/3-articles/ | ecoharbor.com/blogs/category-101/ | 301 ✅ |
shopgreenway.com/category/blog/ | ecoharbor.com/blogs/ | 301 ✅ |
Lessons Learned
- Always start from the sitemap, not from what you think exists. The old site's
sitemap.xmlis your source of truth. If it's not there, it's not indexed. - Don't use a catch-all without explicit exceptions. A generic
/* → /works, but only if specific rules are evaluated first. - Validate with curl, not the browser. Browsers hide redirects. Curl gives you the exact status code and destination URL.
- Keep the old domain active for 3-4 months. Google needs time to process the 301s, update its index, and transfer link juice. Turning off the old domain early is mistake #1 in migrations.
- Document the mapping. When someone asks in 3 months "where does route X redirect to?", having a mapping document or a comment in your task management tool saves hours of debugging.
Bonus: Post-Migration Monitoring
After the migration, I monitored three things for 2 weeks:
- Google Search Console: Verify the new domain is being indexed and the old one shows the expected decline in indexed URLs
- UptimeRobot: Confirm all destination endpoints return 200, not 404
- Analytics: Compare weekly organic traffic pre vs post migration. A 10-15% dip in the first few days is normal while Google processes the 301s
Conclusion
Domain migration doesn't have to be traumatic for SEO. The process is simple in theory (map + redirect + validate), but details matter. A misprioritized catch-all, a slug that changed without notice, a forgotten category route — every small error compounds into lost traffic.
Cloudflare simplifies the technical implementation, but the real work is in the upfront analysis: understanding what URLs exist, where they should point, and validating that they actually point where you said they would.