Ecommerce site migration redirects: products, variants, categories and discontinued items

a shop has more urls than pages. which ones get a redirect, which ones get a wildcard, and which ones are allowed to end in a 404.

by Max Lorenz, Goodaim · updated

a shop's redirect map
  • /products/trail-runnersame handle
    /product/trail-runner
  • /collections/shoes/products/trail-runnercanonical
    /product/trail-runner
  • /products/runner-2019successor
    /product/trail-runner-2
  • /products/gift-wrap-2018gone
    410 — no equivalent

SHORT ANSWER

In an ecommerce migration every product and category that still exists gets a one-to-one 301 to its new url, including the second address many platforms give each product inside a category. Variants and filters written as query parameters need no rows of their own, because the product or category redirect catches them. Discontinued products get a 301 only to a real successor or a fitting category; when nothing comparable is left, a 404 or 410 is the honest answer, and redirecting them all to the homepage is the mistake to avoid.

Which urls does a shop have?

More than its product count suggests. A catalogue of 300 products can easily answer on several thousand urls once categories, pagination, variants and filters are counted, and only a fraction of them deserve a row in the redirect map. The names below are Shopify’s, because they are the most common; WooCommerce, Magento, Shopware and Webflow Ecommerce have the same kinds under other folder names.

kind of urlexamplewhat it gets
product/products/trail-runnerone row each, to the same product on the new shop
product via a category/collections/shoes/products/trail-runnerone row each, to the same product — or a pattern rule where the platform has one
variant/products/trail-runner?variant=4417nothing extra: the product's row catches it
category/collections/shoesone row each, to the matching category
category page 2, 3, …/collections/shoes?page=3nothing extra; a path-based /page/3 gets a wildcard to the category
filter/collections/shoes?color=red&size=42nothing extra; path-based filters get a wildcard or a 404
discontinued product/products/runner-2019successor or category if one fits, otherwise 404 / 410
cart, account, checkout, search/cart · /account/loginnothing: the new platform has its own

The list to build is therefore not “every url” but “every url that is a page in its own right”: products, categories, content pages, blog posts, and the discontinued products that still have links or traffic. Getting that list complete is its own job — the sitemap misses the category-scoped product urls and everything already delisted, so add Search Console and your backlink report (finding the urls nothing links to).

Products: one row each, matched on what they are

A product that exists on both shops is the easiest case and the most valuable one: it is where the rankings and the backlinks sit. Each one gets a one-to-one redirect from the old url to the new one, never a pattern that sends a whole range of products to one place.

When the slug survives the move — /products/trail-runner becomes /product/trail-runner — the match is mechanical, and on platforms with pattern rules the whole folder can be one wildcard row. More often a migration is also the moment someone cleans up the handles: trail-runner-mens-blue becomes trail-runner, sku suffixes disappear, a brand name moves into or out of the slug. Those pairs are found by comparing slugs, titles and h1s, not by rewriting a prefix.

the second product url — many platforms answer for a product inside a category as well: /collections/shoes/products/trail-runner on Shopify, category paths in front of the product on some Magento and WooCommerce setups. These usually carry a rel="canonical" to the short url, are missing from the sitemap, and are linked from every category page — so they are in the index and in other people’s links anyway. Each needs a redirect to the same target as its short form.

Silentfrog compares an old page by its canonical when that canonical points at the same site, so a category-scoped product url is matched as if it were the short one, and both rows end up pointing at the same new product. On an importer with pattern rules you can collapse the category-scoped urls into one rule instead; Shopify, which has none, needs them row by row (wildcard and regex redirects per platform).

EXAMPLE

say, a shop of 300 products moves from Shopify to Webflow Ecommerce. 260 handles survive and match on the slug with a high score despite the new prefix; 31 were renamed and match on title and h1 with a high score; 9 land in review because two similar products compete for the same new page. Those nine are the only rows that need a person.

Variants: usually nothing to do

Most platforms address a variant — a size, a colour — with a query parameter on the product url, ?variant=4417 on Shopify, attribute parameters on WooCommerce. Redirect engines generally match on the path and drop or pass on the query, so the product’s own row redirects every variant along with it, and Silentfrog treats /page?a=1 and /page as one page for the same reason.

Two cases need more thought:

  • variants with their own paths. Some setups give each colour its own product page, /trail-runner-blue and /trail-runner-red. If the new shop folds them into one product with options, each old path gets a row to that product. If the new shop keeps separate pages, each goes to its own counterpart.
  • a parameter that identified the product itself. Older shop systems served products at /product.php?id=417. There the query is the identity, and a path-based rule cannot tell id=417 from id=418. Those need rules on the query string, on the server: in Apache with mod_rewrite, in nginx on $request_uri.
# Apache, in .htaccess: an old query-string product url to its new path
RewriteEngine On
RewriteCond %{QUERY_STRING} (^|&)id=417(&|$)
RewriteRule ^product\.php$ /product/trail-runner [R=301,L,QSD]

QSD drops the old query string from the target, so the visitor does not arrive at /product/trail-runner?id=417.

Categories, pagination and filters

Categories map one to one like products, and they are where the most judgement goes: a relaunch often merges two thin categories, splits a large one, or renames the whole tree. A merged category is the legitimate case for many-to-one — Google’s own site-move guidance allows redirecting several old urls to a consolidated page that replaces them. A split category sends the old url to whichever new one covers most of what it used to list, or to the parent.

pagination — ?page=3 rides along with the category redirect. Path-based pagination, /shoes/page/3/, is worth one wildcard row per category or one for the whole pattern, pointing at the first page of the new category. Never a row per page.

filters and facets — colour, size, price and brand filters multiply a category into hundreds of urls. Google’s documentation on faceted navigation treats them as a crawling problem to contain, and recommends returning a 404 for a filter combination that has no results. As query parameters they follow the category redirect. Written into the path, /shoes/color-red/size-42, they get a wildcard to the category, or nothing at all if the old shop kept them out of the index.

One exception: a filter page that was deliberately indexed — a “red running shoes” landing page with its own title, text and rankings — is a page, not a filter. If the new shop has an equivalent, it gets a row like any category.

Discontinued products: redirect, 404 or 410?

Every migration surfaces products that are no longer sold, and a migration is the moment their urls have to be decided, because the old shop that kept them alive goes away. Three outcomes, chosen per product:

situationanswerwhy
a successor does the same job301 to the successorthe visitor finds what they came for; the old url's links carry over
no successor, but the category still sells that kind of thing301 to the categoryonly if the category is a real answer for that visitor
nothing comparable left404 or 410an honest answer; Google drops the url either way
only out of stock for nowkeep the page, 200mark it out of stock instead of removing it

The thing to avoid is the shortcut: sending every discontinued product to the homepage or to a single sale page. Google’s site move documentation says not to redirect many old urls to one irrelevant destination such as the homepage, because it confuses users and may be treated as a soft 404 — in which case the redirect does nothing a 404 would not have done, and annoys the visitor on top.

404 or 410 matters less than people argue about: Google’s documentation on http status codes says all 4xx errors except 429 are treated the same, and the url is removed from the index. Use 410 where the platform offers it and you want to say “gone on purpose”; a plain 404 with a useful search box and category links does the same job on platforms that have nothing else. A temporarily unavailable product is different: keep the page live and mark it out of stock, with schema.org/OutOfStock in its product markup.

How to decide which discontinued products deserve the effort: look at what they still earn. In Silentfrog, products with no counterpart end up in the unmatched bucket, and importing a Search Console page export adds a clicks column to sort by. The ones with clicks or backlinks get a successor or a category by hand; the long tail stays unmatched. The redirects file leaves unmatched rows out on purpose, and the generic csv keeps them with an empty target — that list is your 410 list. Silentfrog does not write 410 rules itself; those are set where the platform or server allows them.

traffic import

traffic data

import a search console page export (or any csv with a url column and a number column) to see which redirects actually matter — the table gets a clicks column you can sort by.

10 urls imported · 8 matched onto crawled pages

2 urls get traffic but were not found by the crawl — they still need a redirect.

  • /blog/werkstatt-tipps · 587 clicks
  • /produkte/wasserpumpenzange · 233 clicks

the traffic import panel after a search console export was read: how many rows were joined onto matches, and the list of urls that have clicks but were never found by the crawl.

a search console export joined onto the matches: clicks per old url, and the urls with traffic that no crawl or sitemap found.

What the target platform allows

The map is the same everywhere; the importer decides how it has to be written.

Shopify — no wildcards, and per its help page a redirect only fires for a url that is broken on the store, the fixed paths /products, /collections and /collections/all cannot be redirected, and neither can any url beginning with /apps, /application, /cart, /carts, /orders, /services or /shop — so an old /shop/… catalogue needs a redirect layer in front of the store. Every row is written out individually (importing into Shopify).

Webflow Ecommerce — products under /product/, categories under /category/, wildcard rows with (.*) and %1 (importing into Webflow).

WooCommerce, Magento, Shopware on your own server — a redirect plugin or extension, or server rules — which is also where query-string rules for old ?id= urls live (csv to .htaccess or nginx).

A working order for a shop migration

  • Freeze the catalogue a week before the switch, or keep a list of every product added or renamed after the map was built.
  • Collect the old urls from the crawl, the sitemap index, Search Console and the backlink report — including discontinued products still linked from elsewhere.
  • Crawl the new shop on its staging domain once products and categories are imported, so targets are real pages, not plans.
  • Match products and categories one to one; review renamed and merged ones by hand.
  • Decide the discontinued products by traffic and links: successor, category, or 404/410.
  • Leave variants, filters and pagination to the product and category rows, plus a wildcard where they lived in the path.
  • Carry over the redirects the old shop already had, resolved straight to their final new target so nothing chains.
  • After go-live, crawl the old url list against the live shop: one 301, then 200, for every row.

Silentfrog does the collecting, the matching and the file in the importer’s spelling: free for the first 50 pages of the old shop, which is enough to see how it matches your catalogue, and 150 € for a relaunch with no page limit. Try it on your shop.

In short

  • Every product and category that still exists: a one-to-one 301, including the category-scoped product urls.
  • Variants, pagination and filters as query parameters: nothing extra, the product or category row catches them.
  • Path-based pagination and filters: a wildcard to the category, never a row per combination.
  • Discontinued products: successor or fitting category if one exists, otherwise 404 or 410 — never all to the homepage.
  • Old urls whose identity lived in a query parameter: server rules on the query string.