The store ran on Bluepark, a UK-hosted ecommerce platform. It was a specialist B2C and trade retailer with years of trading history and a catalogue built around products with complex multi-dimensional variants. The export told the real story before any work began:
- 5,578 products in the full catalogue
- Only 265 still active and for sale
- 1,523 historical orders
- A product structure built around variants across three or more dimensions (colour, diameter, length, and more)
The first lesson of any migration: the database is always bigger and messier than the storefront suggests. Most of those 5,578 products were discontinued lines. Migrating all of them would have buried 265 live products under five thousand draft records, slowed every import, and cluttered every report from day one.
Decision one was scope: migrate the 265 active products only. Discontinued lines stay behind, with their URLs handled separately via redirects.
Reading the Bluepark export
Bluepark exports products and orders to CSV - but, critically, it does not export customers natively. That alone reshapes the project plan: customer data requires a direct support request to Bluepark before any account or order history can follow the migration.
The product CSV was the bigger challenge. Bluepark encodes variants as a flat, pipe-delimited list representing a Cartesian product of every option combination. A single fencing product might carry:
- Colour (2 values)
- Diameter (4 values)
- Length (13 values)
That's 2 × 4 × 13 = 104 possible combinations, listed in fixed order, with empty slots for combinations that don't exist. Decode the column layout wrong and prices land on the wrong variants. We mapped the exact structure - SKU, prices, cost, stock flag, column ordering - before writing a single product to Shopify.
Several quirks surfaced that no documentation warns about:
- "SPO" stock values meant "special order", not a quantity. These needed to map to zero stock with "continue selling when out of stock" enabled - not a literal number.
- Placeholder SKUs like
Colour:Greyappeared where a single-value option existed. These had to fall back to the product-level SKU and price rather than being treated as real variant identifiers. - Grouped products referenced child SKUs whose prices lived on separate, now-inactive records - leaving genuine gaps we refused to guess at and flagged for manual resolution instead.
Transforming to Shopify's format
Shopify's product model differs from Bluepark's in three ways that matter for any serious catalogue migration:
- Shopify allows a maximum of three options per product. Three products in this catalogue used four. Those were pulled out for manual rebuilding rather than forced into a lossy mapping.
- Shopify's CSV import is row-per-variant, not Bluepark's row-per-product. Every variant for a product needs its own row, with product-level data repeated.
- Images are referenced by public URL during import. This only works if the old site stays live throughout the migration - Shopify pulls images at import time.
The transform script split the output into three buckets:
- 257 clean products - ready to import
- 5 products with no price anywhere in the source - held for manual pricing
- 3 products with too many options - held for manual rebuild
Splitting clean data from edge cases is the most important structural decision in a bulk migration. Never let three broken products block 257 good ones from going live. Import what's clean, resolve what isn't, and keep a clear record of what was skipped and why.
The clean 257 imported without errors. The edge cases got the human attention they needed, not a forced mapping that would have produced bad data on a live store.
Connecting the product page to dynamic data
The new Shopify theme had a properly designed product page - feature icons, "what's included" lists, specification accordions, downloadable PDFs, colour swatches, and more. All of it was hard-coded. None of it read from the products.
The fix is Shopify's custom data layer: metafields (custom fields on products and variants) and metaobjects (reusable structured content that multiple products can reference). We mapped every block on the page to a data source and built the schema:
- 13 product metafields: what's included, features, FAQs, specifications, warranty details, downloads, video URL, guarantee length, dispatch time
- 4 variant metafields: colour swatch, length, width, height
- 2 metaobject definitions: Feature icons and FAQ entries
- 12 reusable Feature records: Rot Proof, Zero Maintenance, 100% Recycled, and others used across multiple products
Then we recovered what we could from the old data: 145 "what's included" lists, 23 product videos, and over 2,000 colour swatch values pulled from the hex codes Bluepark had stored. In total 2,211 metafield values backfilled, leaving only genuinely missing data - dimensions, PDFs - for the client to supply.
One principle that applies to every migration: do not publish recovered source data unchecked. A scan of the recovered copy found a corrupted line - years-old bad data that had been sitting in the database since it was entered. One bad record in 155 is a good ratio. But it's exactly the kind of thing that ends up live on a product page if nobody looks.
Collections and navigation
The original category tree had 27 top-level categories - a mix of real product types, audience segments, seasonal promotions, and admin litter accumulated over years. Mirroring all of it would have rebuilt the mess on a new platform.
Instead: 18 smart collections - product types (Fencing & Gates, Seating, Planks & Profiles, and so on), two audience collections (trade and education), and a Sale collection driven automatically by sale price.
Because they're rule-based on product tags, any product added later joins the right collection automatically - no manual collection management. This is the right model for any catalogue that will keep growing: define the rules once, let Shopify do the upkeep.
The navigation followed: a Home link, a Shop dropdown grouping every product type, then the audience and sale collections. Clean, browsable, maintainable.
What the redirects had to cover
5,578 products in the source catalogue meant roughly 5,300 URLs that would no longer exist in Shopify. For discontinued lines, those URLs still have some chance of carrying inbound links or ranking signals - sending them to a 404 writes off whatever equity they hold.
The redirect strategy:
- Active products - map old Bluepark product URL to new Shopify product URL, one-to-one
- Discontinued products - redirect to the closest matching category/collection, not the homepage. A visitor landing on a discontinued fence post URL should land on the fencing collection, not a generic landing page.
- Old category URLs - redirect to the matching new Shopify collection
Shopify's URL redirect import accepts a CSV with source/destination pairs. The redirect file for this migration ran to thousands of rows - generated from the transform script rather than built manually.
Lessons for any platform migration
- Scope to what sells. Active products first. Discontinued lines are archive and redirects - don't let them slow or clutter the live store.
- Decode the source format before writing code. Variant encodings, stock flags, and placeholder values are where silent errors hide. Map them on paper first.
- Keep the old site live during import so image URLs resolve during Shopify's import process.
- Split clean data from edge cases. Never block a clean import on a handful of broken records. Flag them, import the rest, resolve manually.
- Model your metafields before touching the theme. Metafields and metaobjects are what turn a static Liquid template into a dynamic one. Design the schema first, build the theme against it.
- Audit recovered content. Years-old databases carry years-old typos, corrupted values, and data that made sense in 2018 and doesn't make sense now.
- Plan redirects early and generate them from the data. A 5,000-product catalogue means 5,000 redirect decisions. That's a script, not a spreadsheet.
The heavy lifting in a migration like this isn't the import button. It's the decisions made before you press it - and the verification work done after.