A website migration checklist, filled in
Example
Penhallow Garden Supply is a made-up nursery and garden shop moving off WordPress and WooCommerce. This is the checklist we’d run for that move, phase by phase, with the verification log and a sample redirect map you can copy.
Example. Penhallow Garden Supply is fictional. URLs, counts and dates are invented to show the shape of the work. The checklist is a template for your own migration, not a record of a real one.
What a migration has to protect
A safe website migration comes down to four things: a full list of what you have before you start, a redirect from every old URL to its new home, content and customer data checked rather than assumed, and a cutover in one planned step with a way back. Rankings survive when nothing that search engines know about goes missing.
Below is the checklist we work from, filled in for a fictional garden shop. The tick boxes are as they stood at the end of the build, two weeks before cutover. The verification log and the redirect map follow it, and the map is also a CSV you can copy.
- WordPress 6 with WooCommerce, 23 plugins, shared hosting
- Next.js on Vercel, Sanity for content, headless Shopify for products, orders and customers
- Tuesday 9 February 2027, 6am
- Morwenna Trevail, Operations
6am is the quietest hour in the shop’s analytics, and February is outside the spring rush.
Phase 1: audit
What we have, and what has to survive.
URLs
- Done: Crawl of the live site saved (every URL, status code, title, canonical): 1,416 URLs.
- Done: Sitemap URLs compared with the crawl. 61 URLs in the sitemap weren’t linked from anywhere; listed for a decision.
- Done: Search Console export: every URL with impressions in the last 16 months.
- Done: Backlink export: every URL with external links pointing at it.
- Done: Analytics export: top 200 landing pages by sessions over 12 months.
- Done: The URLs that matter merged into one list: anything with traffic, impressions, links or orders. 402 URLs.
Content
- Done: Counts by type: 640 products, 38 product categories, 212 blog posts, 24 pages, 9 plant care guides, 4,800 media files.
- Done: Content that won’t move, agreed with Morwenna: 140 discontinued products with no traffic and no links (allowed to return 410 Gone; discontinued products that do have traffic or links get a row in the redirect map), and 31 thin tag archives (redirected to the blog).
- Done: Shortcodes and page builder blocks listed, with what each becomes in Sanity.
Shop data
- Done: Customers: 11,300 accounts. Passwords can’t move; customers will be invited to set a new one.
- Done: Order history: 38,000 orders, to be imported into Shopify for customer accounts and reporting.
- Done: Open orders on cutover day: process to finish them in WooCommerce agreed (see Phase 4).
- Done: Gift cards and store credit: 212 active balances, to be recreated in Shopify. Keeping the same codes depends on the Shopify plan; if it can’t, holders get new codes by email on cutover day.
- Done: Subscriptions: “seed of the month”, 340 subscribers. Moving to a Shopify subscription app; re-consent emails drafted.
- Done: Reviews: 2,900 product reviews, moving to the Shopify reviews app with dates and names kept.
Integrations
- Done: Every plugin listed with what replaces it, or why it isn’t needed (23 plugins: 9 replaced, 14 dropped).
- Done: Delivery: local delivery zones by postcode and the courier integration.
- Done: Email: Mailchimp audience and the abandoned basket flow.
- Done: Accounting: Xero sync.
- Done: Analytics and Tag Manager container.
What ranks
- Done: Top 100 queries and the page each one lands on, recorded with average positions as the baseline.
- Done: Structured data on the current site recorded (products, breadcrumbs, reviews).
Phase 2: plan
The written plan
- Done: New URL structure agreed. Products move from
/product/<slug>/to/plants/<slug>or/tools/<slug>; categories from/product-category/<a>/<b>/to/<a>/<b>. - Done: Redirect map written for all 402 URLs that matter, plus pattern rules for the long tail (see the sample below).
- Done: Data mapping written: every WooCommerce field to its Shopify or Sanity field, including custom fields (hardiness zone, sowing months, pot size).
- Done: Cutover date chosen and agreed: Tuesday 9 February 2027, 6am.
- Done: Rollback plan written: DNS back to the old host. The old site stays up for 30 days, closed to new orders, while open orders are finished there.
- Done: Freeze dates agreed: content freeze on the old site from Friday 5 February; orders keep flowing until cutover.
- Done: Fixed price agreed in writing. (This example leaves the figure out.)
Phase 3: build alongside the old site
Staging
- Done: New site built on a staging domain with noindex and password protection.
- Done: Real content loaded early (week 2), so problems show up in demos, not at launch.
- Done: Products imported into Shopify; images re-exported at full resolution from the originals, not the resized WordPress copies.
- Done: Every product page checked against the old one by script: title, price, stock, variants, description length. Differences listed and fixed.
- Done: Titles and meta descriptions carried across; rewritten only where the old one was missing or duplicated.
- Done: Structured data on product, category, article and breadcrumb templates; validated.
- Done: Canonicals point at the new URLs on the live domain, not the staging domain.
- Done: Internal links in blog posts rewritten to new URLs, so they don’t rely on redirects.
- Done: Redirect map loaded and tested on staging: every row returns one permanent redirect to a 200 page, with no chains.
- Done: Checkout tested end to end with test card payments in Shopify’s test mode: delivery zones, gift cards, discount codes, subscriptions.
- Done: Xero sync tested with test orders.
- Done: Accessibility and device testing.
- To do: Customer re-invite email signed off by Morwenna.
- To do: Order import dry run on a copy of the live data, counts reconciled.
Phase 4: cutover day
Before 6am
- To do: Final order import of everything placed since the dry run.
- To do: Old site set to accept no new orders at 5:45am, with a “back in a few minutes” banner.
- To do: Open orders exported; the old site stays up for 30 days, closed to new orders, while staff finish them in WooCommerce’s admin.
6am
- To do: DNS switched to the new site (TTL lowered to 5 minutes the week before).
- To do: noindex removed and password protection off on production.
- To do: Redirects live on the production domain.
First hour
- To do: Every row of the redirect map tested against the live domain by script.
- To do: Ten real URLs from the backlink list tested by hand.
- To do: Test order placed, refunded, and checked in Shopify and Xero.
- To do: New sitemap submitted in Search Console; old sitemap left in place for two weeks so search engines find the redirects.
- To do: Error monitoring watched for 404s and checkout errors.
Go or roll back by 8am. If checkout or redirects are failing and can’t be fixed by 8am, DNS goes back to the old site and we try again another morning.
Phase 5: the 90 days after
| When | Check | Action if it fails |
|---|---|---|
| Daily, days 1 to 14 | 404 report from server logs and Search Console | Missed URL added to the redirect map |
| Daily, days 1 to 14 | Checkout errors, failed payments, Xero sync | Fixed; Morwenna told |
| Day 3 | Search Console: new sitemap read, pages being indexed | Resubmit; check for noindex or canonical mistakes |
| Days 1 to 7 | Customer re-invite emails, sent in batches | Pause batches if the support queue backs up |
| Days 7 and 14 | Subscription re-consent reminders | Morwenna calls anyone left by day 21 |
| Weekly, weeks 1 to 6 | Baseline queries from Phase 1: position and clicks, page by page | Any page down more than a few places for two weeks running gets its redirect, title, content and internal links checked |
| Weekly, weeks 1 to 6 | Indexed pages count: old URLs dropping out, new ones coming in | Investigate any old URL still indexed after four weeks |
| Day 30 | Old hosting: has anything needed it? | Archive database and media, then cancel |
| Day 30 | Core Web Vitals on the new templates, lab and field where available | Fix list for any template outside the “good” range |
| Monthly, months 2 and 3 | Search Console clicks and impressions against the same months last year | Written note to Morwenna either way |
| Day 90 | Close-out: remaining 404s, ranking comparison, redirect map frozen | Pattern rows reviewed; redirects stay in place permanently |
Some movement in rankings while search engines recrawl is normal. A lasting drop usually means something was missed, which is what the 404 report and the weekly comparison are there to catch.
Verification log
Every test is logged with when it ran and what it found. This extract covers the staging run on 26 January 2027 and the first hour of cutover.
| Date and time | Check | Result | Notes |
|---|---|---|---|
| 26 Jan, 10:12 | All 402 mapped URLs against staging | 396 pass, 6 fail | 4 chains (old URL to old URL to new), 2 to a 404 |
| 26 Jan, 14:40 | 6 failures fixed, re-run | 402 pass | Chains collapsed to one hop |
| 26 Jan, 15:05 | 1,416 crawled URLs against pattern rules | 1,416 resolve as planned | None to a 404. The 140 agreed products return 410 Gone; 31 tag archives go to /journal |
| 26 Jan, 15:30 | Product parity script, 500 products | 497 match | 3 had prices changed in WooCommerce since import; re-imported |
| 27 Jan, 09:00 | Test checkout, 12 scenarios | 12 pass | Delivery zones, gift card, discount code, subscription |
| 9 Feb, 06:04 | DNS resolves to new host | Pass | Checked from three networks |
| 9 Feb, 06:10 | All 402 mapped URLs against production | 402 pass | |
| 9 Feb, 06:18 | 10 backlink URLs by hand | 10 pass | |
| 9 Feb, 06:25 | Live test order, refund, Xero | Pass | Order and refund both in Xero by 06:40 |
| 9 Feb, 06:45 | Sitemap submitted | Pass | |
| 9 Feb, 07:50 | Go or roll back | Go | No checkout errors; two 404s from an old newsletter, added to the map |
A sample redirect map
30 rows of the 402, plus the pattern rules. All redirects are permanent (308). Specific rows always win over pattern rules. Marketing parameters (utm_*, gclid, fbclid) are carried through to the destination on every row, so campaign tracking survives; every other query string is handled by a row of its own or dropped on purpose.
The sample is also a spreadsheet you can copy, with no sign-up: download the redirect map as a CSV.
Specific rows
| # | Old URL | New URL | Case | Source | Notes |
|---|---|---|---|---|---|
| 1 | /shop/ | /shop | Trailing slash | Crawl | Trailing slashes dropped site-wide; the new site serves one version only |
| 2 | /product-category/plants/ | /plants | Path change | Search Console | Top category by impressions |
| 3 | /product-category/plants/perennials/ | /plants/perennials | Path change | Search Console | |
| 4 | /product-category/plants/perennials/page/2/ | /plants/perennials?page=2 | Pagination | Crawl | Page numbers kept, so deep links to page 2 and 3 still land on the same products |
| 5 | /product-category/plants/perennials/page/3/ | /plants/perennials?page=3 | Pagination | Crawl | As row 4 |
| 6 | /product-category/tools/hand-tools/ | /tools/hand-tools | Path change | Analytics | |
| 7 | /product/hydrangea-annabelle-3l/ | /plants/hydrangea-annabelle?size=3l | Merged variants | Backlinks | Pot sizes are now variants of one product; the size parameter preselects the right one |
| 8 | /product/hydrangea-annabelle-5l/ | /plants/hydrangea-annabelle?size=5l | Merged variants | Crawl | As row 7 |
| 9 | /product/hydrangea-annabelle-10l/ | /plants/hydrangea-annabelle?size=10l | Merged variants | Crawl | As row 7 |
| 10 | /product/felco-2-secateurs/ | /tools/felco-2-secateurs | Path change | Backlinks | 14 linking domains |
| 11 | /product/peat-free-compost-50-litre/ | /garden-care/peat-free-compost-50l | Slug change | Analytics | Slug shortened and moved category |
| 12 | /product/lavender-hidcote-9cm-pot/ | /plants/lavandula-hidcote | Slug change | Search Console | Renamed to the botanical name Morwenna wants used; old common name kept in the title |
| 13 | /product/blue-glazed-pot-30cm/ | /pots/glazed | Discontinued | Crawl | No stock since 2025, no direct replacement; to its category |
| 14 | /product/rose-feed-1kg/ | /garden-care/rose-feed-2kg | Discontinued | Backlinks | Replaced by the 2kg pack; closest real equivalent |
| 15 | /?p=1843 | /journal/when-to-plant-dahlias | Query string ID | Backlinks | WordPress short link, used in old newsletters |
| 16 | /?page_id=27 | /help/delivery | Query string ID | Crawl | Old default page link still in a supplier’s footer |
| 17 | /?product=felco-2-secateurs | /tools/felco-2-secateurs | Query string ID | Crawl | WooCommerce’s plain-permalink product link |
| 18 | /?s=hydrangea | /search?q=hydrangea | Search | Analytics | Covered by the pattern rule below; listed because it’s the top internal search |
| 19 | /2023/04/when-to-plant-dahlias/ | /journal/when-to-plant-dahlias | Path change | Search Console | Top blog post by impressions |
| 20 | /2024/09/autumn-bulb-planting-guide/ | /guides/autumn-bulbs | Merged content | Backlinks | Merged into the bulb guide |
| 21 | /2024/10/autumn-bulbs-part-2/ | /guides/autumn-bulbs#naturalising | Merged content | Crawl | To the section it became |
| 22 | /plant-care/ | /guides | Path change | Crawl | |
| 23 | /delivery-information/ | /help/delivery | Path change | Analytics | |
| 24 | /my-account/ | /account | Platform route | Crawl | Shopify customer accounts |
| 25 | /cart/ | /basket | Platform route | Crawl | |
| 26 | /checkout/ | /basket | Platform route | Crawl | Can’t resume a WooCommerce checkout; to the basket instead |
| 27 | /wp-content/uploads/2024/03/penhallow-seed-catalogue-2024.pdf | /files/seed-catalogue-2024.pdf | Media | Backlinks | 9 linking domains, including two gardening clubs |
| 28 | /wp-content/uploads/2025/02/planting-calendar.pdf | /guides/planting-calendar | Media to page | Search Console | PDF replaced by a web page; ranks for “cornwall planting calendar” |
| 29 | /wp-content/uploads/2023/06/hydrangea-annabelle-1024x768.jpg | /plants/hydrangea-annabelle | Media | Image search | Image ranking in Google Images; to the product rather than a bare image |
| 30 | /feed/ | /journal/rss.xml | Feed | Crawl | Two feed readers still subscribed |
Pattern rules, applied after the specific rows
| Old pattern | New URL | Case | Notes |
|---|---|---|---|
/:path*/ | /:path* | Trailing slash | Strips the slash, then the specific rows apply. One hop: the rule is merged with the row, never chained |
/YYYY/MM/:slug/ | /journal/:slug | Dated posts | Every remaining blog post |
/tag/:tag/ | /journal | Thin archives | 31 tag pages with no traffic |
/product-category/:a/:b/page/:n/ | /:a/:b?page=:n | Pagination | Any category page not listed |
/?s=:q | /search?q=:q | Search | Internal search links |
/?add-to-cart=:id | /shop | Basket links | Old basket links in emails |
/wp-content/uploads/:file | The same file on the new image host if it exists; otherwise 410 Gone | Media | Unlinked, untrafficked images are allowed to go |
/product/:slug/ not in the map | 410 Gone | Discontinued products | Only the 140 agreed in Phase 1, with no traffic and no links. Never redirected to search or the home page, which search engines treat as a soft 404 |
Checks on every row: one hop only; the destination returns 200; the destination isn’t itself redirected; marketing parameters survive; the mapping makes sense to a person following an old link.
Have something to build?
Tell us what you're working on. We'll tell you how we'd approach it.
We reply within one working day.