← Blog

Migration

Magento 1 to Shopify: the two-day migration playbook

Magento 1 hit end-of-software-support in June 2020. If your shop is still on it, the way most agencies move you off loses the URLs that earn money and the reviews that close the sale.

Jacob Molkenboer· Founder · A Brand New Company· 23 May 2024· 10 min
Closed leather ledger with brass key on cream card, iron shipping tag with green ribbon on ivory paper.

It is Friday at 4pm in Rotterdam. Your client's webshop has been on Magento 1.9 since 2016. Adobe stopped patching it five years ago. The hosting bill still arrives every month because nobody wants to be the one to touch it. Twelve thousand product pages have been crawled, ranked, and reviewed for nine straight years. Marketing automation references those product URLs in 1,400 outbound emails per quarter. The owner wants Shopify by Monday morning. You have a Sunday window.

This is the playbook we use when a client asks us to leave Magento 1 over one weekend without dropping the URLs that earn money or the reviews that close the sale. It assumes you have already chosen the Shopify plan, locked the theme, and matched payment processors. The work below is the part nobody tells you about.

The three things that break

Three things hurt during a Magento to Shopify move, and they hurt at different times.

Product URLs break first, because Magento's URL rewrite table is structured nothing like Shopify handles. Reviews break second, because they live in vendor-specific tables and the Shopify side wants vendor-specific apps. SEO authority breaks third, on a six-week delay, because Google needs time to recrawl and rebuild its model of your site. The migration that goes wrong loses one or two of these. The migrations we like to remember preserve all three.

If you want the official cut-off date for Magento 1, Adobe's own end-of-software-support note puts it at 30 June 2020. The shops still running past it usually pay a third party for PCI patches. Most of those contracts now cost more per year than Shopify Plus.

Eight blocks of six hours

We split the weekend into eight blocks of roughly six hours. Two people split them. One drives the export and the data engineering, the other drives the Shopify side, the theme, and verification. Sleep in shifts.

Block 1 (Friday 17:00–23:00): freeze and snapshot

Stop everything that writes to Magento. Put the admin behind HTTP basic auth. Disable order capture on the front-end. Pause every cron that touches inventory. Then take three snapshots:

  • A mysqldump of the full database with --single-transaction --routines --triggers.
  • A tarball of /media/catalog/product/.
  • A static crawl of the live site to disk using wget --mirror as a fallback.

The static crawl is the safety net. If Monday goes wrong, you can serve the crawl from a bucket while you figure out what happened. Do not skip it because it feels paranoid.

Block 2 (Friday 23:00–Saturday 05:00): the URL map

This is the most important block of the weekend. Build a CSV with three columns: the old URL, the new URL, and a status. Pull it from core_url_rewrite in the Magento database.

SELECT
  CONCAT('/', request_path) AS old_path,
  product_id,
  category_id
FROM core_url_rewrite
WHERE is_system = 1
  AND product_id IS NOT NULL
  AND store_id = 1
ORDER BY product_id;

The raw output is not the map. Shopify normalises handles differently (lowercase, ASCII, hyphens only) and category-nested URLs like /men/shoes/blue-runner.html collapse to /products/blue-runner in Shopify, because the platform does not nest products under collections in its canonical URLs.

So you generate the candidate, then run it through a transform that mirrors Shopify's handle rule. The most reliable approach is to write the transform in code and apply it to every row:

function shopifyHandle(slug) {
  return slug
    .toLowerCase()
    .replace(/\.html$/, '')
    .replace(/[^\w\s-]/g, '')
    .replace(/\s+/g, '-')
    .replace(/-+/g, '-')
    .replace(/^-|-$/g, '');
}

Save the result as url-map.csv. You will use it three times before Monday.

Block 3 (Saturday 05:00–11:00): products

Export Magento's products in the format Shopify's bulk importer expects. The cleanest path on the Shopify side is Matrixify (formerly Excelify), which speaks Shopify's import schema natively and will accept a CSV that includes images, variants, SEO fields, and metafields.

The Magento export is the harder side. catalog_product_entity joins with all of its EAV attribute tables, which is the part of Magento 1 nobody enjoys. Use n98-magerun for sane CLI access:

n98-magerun.phar db:console < queries/products_full_export.sql > products_raw.tsv

Then map every Magento attribute to a Shopify column. Watch for these gotchas. Magento's description field is often stuffed with inline styles from old WYSIWYG sessions; Shopify will render them, but they will look broken on the new theme. Strip them. Magento variants live as separate parent and child rows; Shopify variants are nested inside one product. The Matrixify template handles this correctly if you pass Variant Position consistently.

Block 4 (Saturday 11:00–17:00): reviews

This is the block agencies skip. Nine years of reviews are not something you throw away because the export was annoying. The pull is one SQL query against review, review_detail, and rating_option_vote:

SELECT
  r.review_id,
  r.created_at,
  rd.title,
  rd.detail,
  rd.nickname AS author_name,
  rd.customer_id,
  rov.value AS rating,
  cpe.sku
FROM review r
JOIN review_detail rd ON rd.review_id = r.review_id
JOIN rating_option_vote rov ON rov.review_id = r.review_id
JOIN catalog_product_entity cpe ON cpe.entity_id = r.entity_pk_value
WHERE r.status_id = 1
ORDER BY r.created_at;

If the shop runs Yotpo, Trustpilot, or Reviews.io on top of Magento, those vendors hold their own copy. Open a ticket with the vendor on Friday afternoon and ask for a full export against SKU. Most of them deliver within 24 hours if you ask in writing.

On the Shopify side, our default is Judge.me, because the CSV importer accepts the exact shape above with a small column rename, and because it renders review HTML server-side so the schema shows up in the page source.

Warning

If reviews on the new shop render only via client-side JavaScript, your structured-data rich results will disappear within two weeks. Check the rendered HTML for aggregateRating in view-source on a product page before you cut over, not after.

Block 5 (Saturday 17:00–23:00): customers, orders, history

Customer accounts need their email and address book. They do not need their hashed password; Shopify cannot accept a Magento bcrypt hash, so every customer resets on first login. Frame this in the launch email as a security upgrade. It is one.

Orders are trickier. You do not strictly need them in Shopify to operate, but you need them findable when a customer emails support. Two approaches: import the last 24 months into Shopify and keep the rest in a read-only archive, or keep all orders in the archive and route support there. We do the second. Shopify's plan tiers nudge you toward smaller order tables, and a 9-year archive is mostly noise.

Block 6 (Sunday 00:00–06:00): theme and product import

Push the products into Shopify. For 12,000 SKUs, Matrixify will take roughly three to five hours to run, so kick it off at the start of this block and use the time to finish the theme. The theme work is mostly about making sure the product page renders the right metafields, the collection pages use the right faceting, and the cart picks up shipping rates from your zones.

Reserve the last hour of this block for spot-checking. Open 30 random product URLs from url-map.csv and confirm they render with images, variants, and price.

Block 7 (Sunday 06:00–12:00): the redirect layer

Shopify's built-in URL redirects work, but they cap at 100,000 entries and they are slow to bulk-update. For 12,000 product URLs plus categories, blog posts, CMS pages, and parametric variants, you want the redirect to happen at the edge before the request hits Shopify.

Our default is a Cloudflare Worker with the URL map loaded into KV. The shape is small:

export default {
  async fetch(req, env) {
    const url = new URL(req.url);
    const target = await env.URL_MAP.get(url.pathname);
    if (target) {
      return Response.redirect(`https://${url.hostname}${target}`, 301);
    }
    return fetch(req);
  }
};

Load the KV namespace from url-map.csv in one batch upload. Worker plus KV bills out at single-digit euros per month for a shop this size. If you want to skip Cloudflare, Shopify's redirects.csv bulk import via Matrixify is the second-best option, with the same data shape.

Three categories of URLs deserve their own pass: query-string filters (?color=blue on category pages), CMS pages (/about-us, /shipping), and the homepage of any sub-language store whose slug structure changed. Walk each by hand. There are not that many of them.

Block 8 (Sunday 12:00–18:00): DNS, verify, monitor

Cut DNS over to Shopify when the redirect layer is verified and the product spot-check passes. TTL should already be at 60 seconds because you lowered it on Friday afternoon. Watch the propagation, then run three verifications:

  1. A status check against every URL in url-map.csv. curl -o /dev/null -s -w "%{http_code} %{url_effective}\n" in a loop is fine.
  2. A Lighthouse pass against the top-30 trafficked pages. Note LCP and CLS. They will look worse than the Magento numbers on day one and recover within a week.
  3. A live search for one of your top-ranked queries. Confirm the result still resolves.

Submit the new sitemap to Google Search Console as the last action. Keep the old property open. You will watch crawl-error volume there for the next 30 days to catch URLs the map missed.

The thirty-day tail

The migration is not over when DNS flips. The first month is when you find the URLs nobody tracked: deep-linked product variants from emails sent in 2019, blog posts that linked to category pages with filter params nobody documented, the affiliate that points at a vanity URL the original developer forgot to write down. Watch Search Console's coverage report daily for the first two weeks and weekly for the next two. Every new 404 it finds is a row you add to the KV redirect map.

Reviews on the new platform start scoring rich-result eligibility within roughly two crawls of the page. If you do not see star ratings back in your SERP within four weeks, the most common cause is that Judge.me (or whichever app you picked) is rendering schema in JavaScript only. Switch the widget to the server-rendered variant.

One real client, one unexpected drag

When we ran this migration for a Dutch outdoor-gear shop earlier this year, the unexpected drag was not the catalog. It was a 2014 email-marketing platform that had 800 product URLs hardcoded into automation flows nobody had logged in to since the founder's daughter set them up. We solved it by promoting those 800 URLs to priority entries in the Cloudflare Worker, so the click-through never touched Shopify until the email vendor's content team rewrote the templates a month later.

The five-minute audit

Before you commit to a migration window, run one query against your Magento database. Count the rows in core_url_rewrite where is_system = 1 and store_id = 1. That number is your URL map size. Then count rows in review where status_id = 1. That is your review-preservation problem. If either number is bigger than you expected, build in an extra block before you book the weekend.

Key takeaway

Build the URL map first, the product import second, and the review import third. Anything done out of that order will cost you organic revenue you cannot recover.

FAQ

What is the biggest risk in a Magento 1 to Shopify migration?

Losing the canonical URLs that earn organic traffic. Build the URL map first, before touching anything else, and serve the redirects at the CDN edge, not inside Shopify.

Can I import customer passwords from Magento to Shopify?

No. Magento's bcrypt hashes are not compatible with Shopify's auth. Every customer resets on first login. Frame this in the launch email as a security upgrade, because it is one.

How do I preserve nine years of product reviews?

Export from the review, review_detail, and rating_option_vote tables in Magento, then import into Judge.me or a similar Shopify app that renders the review widget server-side so the schema survives.

Does Shopify handle 12,000 SKUs without issues?

Yes, but the bulk importer is slow. Plan for three to five hours per import pass with Matrixify, and run smaller batches when you can to recover faster from mistakes.

Should I use Shopify redirects or a Cloudflare Worker?

For shops under 1,000 redirects, Shopify's built-in redirects are fine. Above that, a Cloudflare Worker with KV scales better and lets you ship updates without re-running a CSV import.

migrationmagentoe-commerceseolegacy sitesarchitecture

Building something?

Start a project