mndev
EN/UA/IT
All notes
6 min read

Shopify migration without losing rankings: 3,000 products off OpenCart

How a 3,000-product fashion catalogue moved from legacy OpenCart to a headless Shopify + Next.js stack with zero lost rankings — URL parity, redirects, hreflang and structured data.

Replatforming is the point where most e-commerce SEO dies. Not because the new stack is worse — usually it is far better — but because a migration quietly changes every URL, every title, every internal link and every piece of structured data at once. Google sees a site it does not recognise, and the rankings that took five years to earn reset in a fortnight.

MioMio carries over 170 fashion houses — Furla, Casadei, Premiata, Bikkembergs. The catalogue sat on a legacy OpenCart instance nobody wanted to maintain: no real integration points, every connection a hack, no path to modern services. The rebuild was headless Shopify with a Next.js storefront. The constraint was that 3,000 products had to move without burning five years of accumulated search equity.

They all survived. Here is the process that made that boring rather than lucky.

The rule that shaped every other decision

One rule, decided before any code was written: URL structure stays identical, 1:1. Not similar. Not improved. Identical.

This is the single highest-leverage decision in any replatform, and it is the one most often traded away — usually for a cleaner slug format, or because the new platform has opinions about URL patterns. Every one of those trades converts a free migration into a redirect project, and redirect projects leak. A 301 passes the overwhelming majority of ranking signals, but 'overwhelming majority' across 3,000 URLs is still measurable loss, and any redirect you forget is a hard 404 on a page that used to rank.

Shopify's default URL structure fights this: it wants /products/handle and /collections/handle. A headless storefront is what buys the freedom back — Next.js owns routing, Shopify is only the commerce API behind it. Product identity lives in Shopify; the URL that identity is served at is yours.

In a headless build, the CMS no longer dictates your URLs. That is not a side effect — for a migration, it is the entire reason to go headless.

Step one: inventory what Google actually knows

Before touching the new stack, build the list of URLs that matter. Not the list of URLs that exist — those are different numbers, often by an order of magnitude.

Four sources, merged:

  • Search Console: every page with impressions in the last 16 months. This is the ground truth of what Google indexed and what users clicked.
  • The existing XML sitemap, as the platform's own view of what should exist.
  • A full crawl of the live site, which catches pages the sitemap forgot — old campaign landing pages, orphaned filters, paginated collections.
  • Analytics: any URL with organic sessions, including ones long since delisted from the sitemap.

The union of those four is the migration contract. Every URL in it must resolve with a 200 or a 301 on launch day. Nothing in it may return a 404.

The useful output is a single mapping file, versioned in the repo alongside the code:

// One row per legacy URL. Reviewed by hand where the mapping is not mechanical.
export const REDIRECTS = [
  { from: "/index.php?route=product/product&product_id=482", to: "/uk/furla-metropolis-bag" },
  { from: "/women/bags/furla", to: "/uk/collections/furla" },
  // ...
] as const

Legacy query-string URLs are the ones people miss. OpenCart, Magento and older PrestaShop installs all shipped ?route= or ?id_product= style URLs that stayed indexed for years. They will not appear in a naive slug-to-slug mapping, and they are exactly the URLs with the oldest, strongest backlinks.

Step two: keep the two-language problem out of the migration

MioMio serves Ukrainian and English. Multilingual sites fail migrations in a specific way: the language variants get treated as duplicates, Google picks one arbitrarily, and half the catalogue drops out of the index in the other market.

The fix is unglamorous and absolute — every page declares the full cluster of its language variants, and every variant points back:

<link rel="canonical" href="https://example.com/uk/furla-metropolis-bag" />
<link rel="alternate" hreflang="uk" href="https://example.com/uk/furla-metropolis-bag" />
<link rel="alternate" hreflang="en" href="https://example.com/en/furla-metropolis-bag" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/furla-metropolis-bag" />

Three rules make hreflang work, and breaking any one silently disables the whole cluster:

  1. 01Reciprocity. If page A lists B as its Ukrainian variant, B must list A as its English one. One-directional annotations are ignored.
  2. 02Self-reference. Each page includes itself in its own alternate list. This is the rule people skip because it looks redundant.
  3. 03Canonical agrees with hreflang. A canonical pointing at a different language than the page's own hreflang entry is a contradiction, and Google resolves contradictions by ignoring your instructions.

Generate these from one function that takes the locale and the path. Never hand-write them per template — hand-written hreflang decays the moment someone adds a page.

Step three: structured data as a migration asset

Structured data is usually framed as a rich-results play — stars and prices in the SERP. During a migration it does something more useful: it gives the crawler an unambiguous statement that the product at the new URL is the same product it already knew.

Product schema with a stable identifier — SKU, GTIN, MPN — carried across from the old catalogue is the strongest available signal of continuity. The identifiers do not change just because the platform did.

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Furla Metropolis",
  "sku": "FRL-MTP-0412",
  "brand": { "@type": "Brand", "name": "Furla" },
  "offers": {
    "@type": "Offer",
    "priceCurrency": "UAH",
    "price": "8400",
    "availability": "https://schema.org/InStock"
  }
}

Emit it server-side. Structured data injected by client-side JavaScript is read inconsistently and adds a rendering dependency to the one signal you least want delayed.

Step four: launch day sequence

The order matters more than the speed:

  1. 01Deploy with the redirect map live from the first request. Not 'shortly after' — the first crawl after DNS cutover is the one that sets expectations.
  2. 02Submit the new sitemap in Search Console immediately, and keep the old one accessible for a few weeks so Google can reconcile the two.
  3. 03Use the URL Inspection tool on ten to fifteen representative pages: highest-traffic product, deepest collection, both language homepages, a paginated listing. Fix whatever it complains about before waiting on aggregate data.
  4. 04Watch server logs, not analytics, for the first 72 hours. Logs show what Googlebot actually requested and what status it got back. Analytics only shows the humans who made it through.

The 404 rate in the logs is the one number that matters in week one. It should approach zero within days. If it does not, the mapping file has holes, and every day they stay open is a day of decay.

What the migration actually returned

All 3,000 products went live with zero rankings lost. Page load went from 4.8s to 1.2s — four times faster on mobile, which on a fashion catalogue is where nearly all the traffic is. Conversion rose 25% in the first quarter, and Google's SEO score came back at 100/100 with structured data, hreflang and clean Core Web Vitals across the board.

Worth separating the causes: the ranking preservation came from URL parity and redirects. The conversion lift came from speed and the rebrand. They are independent wins, and conflating them is how teams end up believing a redesign is an SEO strategy.

The general lesson is that a migration is not a technical event to survive. It is a contract with a crawler that already trusts you: keep the addresses stable, keep the identity provable, and the trust transfers intact. Everything else in the rebuild — the speed, the design, the editorial content — is then pure upside on top of what you already earned.

Message on Telegram