Most BigCommerce to Shopify migrations go fine on the data side. Products, customers and orders move with a migration app or a CSV import. Where stores lose traffic is the URLs.
BigCommerce lets merchants set any URL they like: /blue-linen-shirt/, /mens/shirts/, /shipping-returns/. Shopify uses fixed paths: /products/<handle>, /collections/<handle>, /pages/<handle> and /blogs/<blog>/<handle>. So almost every indexed URL changes, and each one needs a 301 redirect.
This post shows a simple, repeatable way to build that redirect map.
Step 1: collect every old URL
Don't rely on the BigCommerce product export alone. You want every URL Google knows about:
- The live sitemap. BigCommerce serves
/xmlsitemap.php, which links to sub-sitemaps for products, categories, pages, brands and blog posts. - Search Console. Export the Pages report (last 16 months) so you catch old URLs that still get impressions.
- Backlinked URLs. If you have a backlink tool, export the target URLs. Old campaign pages often show up here.
Merge the lists and remove duplicates and query strings.
import csv, re, xml.etree.ElementTree as ET import urllib.request NS = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"} def fetch(url): with urllib.request.urlopen(url) as r: return r.read() def sitemap_urls(index_url): root = ET.fromstring(fetch(index_url)) urls = [] for loc in root.findall(".//sm:loc", NS): u = loc.text.strip() if u.endswith(".xml") or "xmlsitemap.php" in u: urls += sitemap_urls(u) else: urls.append(u) return urls old = set(sitemap_urls("https://www.example-store.com/xmlsitemap.php")) with open("gsc_pages.csv") as f: for row in csv.DictReader(f): old.add(row["Top pages"].split("?")[0]) print(len(old), "old URLs") Step 2: classify each URL
Each old URL needs a type, because the type decides the new path. Pull products, categories and web pages with their custom URLs (the product CSV export includes a product URL column; for categories and pages, use the BigCommerce API or your migration tool's export) and save each as a CSV. Then join on the URL:
from urllib.parse import urlparse def load(path, url_col, name_col): with open(path) as f: return {r[url_col].strip(): r[name_col] for r in csv.DictReader(f)} products = load("bc_products.csv", "Product URL", "Product Name") categories = load("bc_categories.csv", "Category URL", "Category Name") pages = load("bc_pages.csv", "Page URL", "Page Name") def classify(url): path = urlparse(url).path if path in products: return "product", products[path] if path in categories: return "collection", categories[path] if path in pages: return "page", pages[path] return "unknown", None Your export column names may differ slightly. Check the header row first.
Step 3: work out the new Shopify path
Shopify builds a handle from the title: lowercase, spaces to hyphens, most punctuation removed. If your import keeps the BigCommerce slug as the handle (recommended, because it keeps the last part of the URL the same), you can reuse it:
def handle_from(path): return path.strip("/").split("/")[-1] PREFIX = {"product": "/products/", "collection": "/collections/", "page": "/pages/"} rows, unknown = [], [] for url in sorted(old): kind, _ = classify(url) path = urlparse(url).path if kind == "unknown": unknown.append(url) continue rows.append((path, PREFIX[kind] + handle_from(path))) Nested category paths like /mens/shirts/ become a single collection handle (/collections/shirts), so watch for clashes. Two categories called "Shirts" under different parents need different handles, such as mens-shirts and womens-shirts.
Step 4: handle the leftovers by hand
The unknown list usually holds:
- brand pages (
/brands/acme/): map them to a vendor collection - blog posts: map them to
/blogs/news/<handle> - old URLs that 404 already: redirect them to the closest category, not the homepage
Redirecting everything to the homepage is a common shortcut. Google tends to treat those as soft 404s, so the old page's signals are lost.
Step 5: import into Shopify
Shopify's URL redirects screen accepts a CSV with two columns, Redirect from and Redirect to:
with open("shopify_redirects.csv", "w", newline="") as f: w = csv.writer(f) w.writerow(["Redirect from", "Redirect to"]) w.writerows(rows) A few rules to remember:
- Shopify only follows a redirect when the old path no longer exists as a live page on the new store.
- Paths are relative (
/blue-linen-shirt/), not full URLs. - Run the import on the new store before you switch DNS, then test a sample with
curl -I.
while IFS=, read -r from to; do code=$(curl -s -o /dev/null -w "%{http_code} %{redirect_url}" "https://www.example-store.com$from") echo "$from -> $code" done < <(tail -n +2 shopify_redirects.csv | head -50) Step 6: watch the first four weeks
After launch, submit the new sitemap (/sitemap.xml on Shopify), then check Search Console's Pages report every few days. New 404s point to URLs you missed: add redirects for them in batches.
FAQ
Can I keep my BigCommerce URLs exactly as they are?
Not the paths. Shopify's /products/ and /collections/ prefixes are fixed. You can keep the last part (the handle) the same, which makes redirects easy to build.
Do I need redirects for product images?
Only if image URLs get real traffic from image search or other sites. Most stores can skip them.
How many redirects can Shopify hold?
Large stores import tens of thousands. Very large catalogues should import in batches and check for errors after each file.
Wrapping up
The redirect map is the part of a migration that protects your rankings, and it's worth doing carefully. If you'd rather hand over the whole move, Dzimark runs a Shopify migration service for stores in the USA, UK, Canada and Australia, including the redirect map and post-launch checks.


