TL;DR: Migrated 330 records (305+ products, 39 bundles) from a legacy platform (Entrepedia) to a Next.js + Prisma + MySQL stack. The code was fine — the data was the problem: slug collisions, duplicate titles, cover/deliverable mismatches, and a tangled taxonomy. Scroll to "The checklist I'd use next time" for the actionable part.
Earlier this year I migrated 305+ digital products and 39 bundles from Entrepedia to a new Next.js + Prisma + MySQL platform — idesire.io. Catalog migrations look boring until 330 records disagree with each other. Here's an honest breakdown of what broke, how we fixed it, and the checklist I'd run next time.
What broke
1. Slug collisions
Two products, one slug. The legacy platform allowed duplicate slugs across categories, so /products/planner existed twice. On the new platform the slug is the primary lookup, so imports silently overwrote each other.
Fix: namespaced slugs per category during import, then ran a uniqueness pass and 301-redirected the losers.
2. Broken legacy records
Years of manual edits left products with missing descriptions, null prices, and orphaned category IDs pointing at categories that no longer existed.
Fix: a validation report before import — every record got a health check, and anything failing went into a quarantine list for manual review instead of failing the whole batch.
3. Cover / deliverable mismatches
The cover image said "2024 Ultimate Bundle" while the attached deliverable was the 2022 version. Nobody noticed because nobody re-downloaded after uploading.
Fix: spot-checked a sample of high-traffic products and re-exported covers from the actual deliverables.
4. Taxonomy cleanup
Seven overlapping category trees ("Marketing", "marketing", "Digital Marketing", "MKTG"…) had grown over the years. Search and filtering were useless.
Fix: collapsed everything into one canonical taxonomy and mapped legacy categories with a lookup table — kept the old names as redirects so bookmarks didn't die.
5. Duplicates with exact titles
The same product imported twice under slightly different SKUs, with byte-identical titles. Customers saw the same item twice in search.
Fix: dedupe pass on normalized title + file hash before import.
6. Free pricing enforcement
"Free" products carried stale prices — $0.00 in one field and $19 in another. Checkout logic picked whichever it read first.
Fix: single source of truth — an is_free flag drives the price, and the import rejected any record where the two disagreed.
7. Thumbnail issues
Mixed aspect ratios, oversized images on listing cards, and a handful of hotlinked images that died when the old CDN was shut off.
Fix: regenerated thumbnails at fixed sizes during import and replaced hotlinks with locally hosted copies.
Root causes
Every item above traces back to one of three root causes:
- We trusted the legacy data. Years of manual edits had made the old database the least reliable source of truth — and we treated it as gospel.
- Implicit rules became import logic. The old platform tolerated duplicate slugs, dual price fields, and loose taxonomy. The new one couldn't. Nobody had written those rules down until they bit us.
- We validated after the import, not before. We found problems by reading broken pages instead of by reading a validation report.
The checklist I'd use next time
- [ ] Audit the legacy data before writing a line of migration code — export everything to a spreadsheet first
- [ ] Write a validation report: every record passes or goes to quarantine; nothing silently drops
- [ ] Normalize and dedupe on (title, file hash), not on IDs you don't trust
- [ ] Plan the slug strategy up front: uniqueness rules + 301 redirects for collisions
- [ ] Freeze the taxonomy first — one canonical tree, legacy names mapped, not merged
- [ ] Verify deliverables match their covers on a sample of top-selling products
- [ ] Make pricing a single source of truth (
is_freeflag, not two price fields) - [ ] Regenerate all media at import time — fixed sizes, local hosting, no hotlinks
- [ ] Dry-run the full import on staging, then diff staging vs. legacy record by record
- [ ] Keep the old platform read-only (not deleted) for 90 days as a reference
Migrations are 20% code and 80% data archaeology. The checklist is the part I'd never skip again.
What broke in your last migration? Share your war stories in the comments — the messier, the better. 👇
Top comments (0)