Leadership & Cost • Technical Leadership
Leading a Legacy Frontend Migration Across a Multi-Site Platform
Quick Links
Summary
- Problem: A legacy Gatsby site had accumulated technical debt and ~2,000 lines of duplicated content-data across product and service pages, making every addition a multi-file, sync-prone chore.
- Solution: Migrated to Next.js App Router with TypeScript strict, and re-architected content around filesystem auto-discovery so pages are data + metadata files with zero registry wiring.
- Impact: ~2,000 lines of duplication removed, adding a page dropped from four edits to two, type-strict CI catches errors before deploy, and standalone Docker output simplified the deployment pipeline.
- Key Decisions: App Router over Pages Router, Tailwind + shadcn/ui for velocity, filesystem auto-discovery over manual registries, and standalone Docker output for portable deploys.
Context
The platform is a multi-site web presence for a multi-vertical auto-dealer software business — a blog plus a dozen product pages and several service pages spanning distinct dealership verticals. The original Gatsby implementation had drifted into legacy: GraphQL-first data plumbing, duplicated content-data across similar pages, and a build/deploy story heavier than it needed to be. I led the migration to a modern Next.js stack and, more importantly, re-architected how content is modeled so the platform stays cheap to extend.
Symptoms / Failure Modes
- Adding a product or service required edits across four files plus a manual registry entry
- ~2,000 lines of duplicated content-data drifted out of sync between similar pages
- Legacy GraphQL-first patterns slowed onboarding and iteration
- Deployment carried a heavier runtime footprint than the site warranted
Goals, Requirements, Constraints
Goals
- Retire legacy patterns and move to a modern, well-supported framework
- Make adding content cheap and impossible to get out of sync
- Tighten type safety so errors surface in CI, not production
- Simplify and lighten the deploy pipeline
Constraints
- Preserve SEO equity — no broken URLs or lost rankings during the cutover
- Keep all content in version control rather than a hosted CMS
- Deliver incrementally without a big-bang freeze on content updates
Non-Goals
- A visual redesign (migration preserved the existing brand and content)
- Re-platforming the backend or CMS (frontend and content model only)
- Adopting an external headless CMS (content stays in-repo as MDX)
Acceptance Criteria
- All 90+ pages render under the new stack with parity to the old site
- Legacy redirects preserved so no inbound link or ranking is lost
- Adding a new product/service requires no registry or wiring changes
- TypeScript strict mode passes in CI before any deploy
Approach
I sequenced the work in phases — stand up the Next.js App Router foundation and dynamic routing first, port the MDX content system, then rebuild the page sections and forms. The architectural centerpiece was separating structured content (per-page data files) from metadata (MDX frontmatter) and discovering both from the filesystem at build time, so there are no hand-maintained registries to fall out of sync. Legacy redirects were carried over wholesale to protect SEO, and the build was switched to standalone Docker output for a lighter, portable deploy.
Key Design Decisions
- Decision: App Router over Pages Router
Why: Server Components, streaming, and composable layouts are the platform's forward direction and map cleanly onto a content-heavy multi-site; adopting them now avoided a second migration later.
Alternatives: Pages Router was the lower-risk port but would have locked the platform into a pattern already being superseded. - Decision: Filesystem auto-discovery over manual content registries
Why: Pages become a data file plus a metadata file that the build discovers automatically, so a registry can never drift from reality and contributors add content without touching code.
Alternatives: Keeping explicit registries preserved the old mental model but was exactly the source of the duplication and sync bugs being eliminated. - Decision: Tailwind CSS + shadcn/ui for the component layer
Why: Utility-first styling with accessible Radix-based components maximized iteration speed and kept the design system consistent across dozens of pages.
Alternatives: A CSS-in-JS approach was considered but added bundle weight and slowed iteration for a content-first site. - Decision: Standalone Docker output
Why: A self-contained build artifact shrinks the image and dependency tree and makes container deploys portable and predictable.
Alternatives: A full Node runtime deploy was simpler to set up but heavier to run and ship.
Implementation
Components / Modules
- Next.js App Router foundation: Dynamic routes generating blog, product, and service pages from content via generateStaticParams.
- MDX content system: Metadata in MDX frontmatter, structured content in per-page data files, parsed and validated at build time.
- Filesystem auto-discovery: Build-time scanning of content directories so new pages appear with no registry edits.
- Shared slug utilities: A single normalize/match/extract module replacing duplicated slug logic that previously lived in several files.
Data & State
- 90+ MDX content files spanning blog posts, a dozen product verticals, and several service pages.
- Per-page structured data files separated from MDX metadata to enforce a single source of truth.
- 46+ legacy redirects ported into the framework config to preserve SEO equity.
Automation & Delivery
- TypeScript strict type-checking gates every change in CI
- Static generation builds all content routes at build time
- Standalone Docker output produces a self-contained, deployable artifact
Notable Challenges
- Untangling ~2,000 lines of duplicated content-data across product and service pages — resolved by splitting structured data from metadata and auto-discovering both, so duplication had nowhere to hide.
- Protecting SEO through a framework change — resolved by carrying all legacy redirects into config so every inbound URL kept resolving.
- Bringing the team onto App Router patterns — resolved with in-repo documentation that made the new content model self-service.
Results
Outcomes
- Maintainability: Adding a product or service went from four coordinated edits plus a registry entry to creating two files, with the build discovering them automatically.
- Code health: Roughly 2,000 lines of duplicated content-data were eliminated and slug handling consolidated into one tested module.
- Reliability: TypeScript strict mode moves a class of errors from production into CI, and ported redirects preserved SEO through the cutover.
- Cost: Standalone Docker output lightened the deploy artifact and dependency tree, simplifying the release pipeline.
Tradeoffs
- App Router required the team to learn new patterns; mitigated with in-repo documentation but a real upfront cost.
- Separating data from metadata adds discipline — data files can grow verbose — in exchange for a single source of truth.
- Static generation of 90+ pages lengthens builds; incremental regeneration is the path to keeping build times flat as content grows.
Next Steps
- Adopt incremental static regeneration so build time stays flat as content scales
- Extend the auto-discovery model to additional content types
- Layer automated Lighthouse/Core Web Vitals checks into CI