Redesigning a website from scratch sounds exciting.
You get a clean codebase, modern components, better UX, and the opportunity to finally remove all those hacks that have been sitting in production for years.
But there is one problem:
The old website is already working.
Thousands of users might depend on it. Search engines already know its URLs. Analytics are configured. Marketing links are everywhere. APIs expect a particular structure. And somewhere in the codebase, there is probably a !important that nobody remembers adding.
That was the situation I faced when redesigning a legacy website.
The goal wasn't simply to make it look better.
The real goal was:
Replace the experience without breaking the business.
Here's how I approached it.
The Legacy Website Problem
The existing website had accumulated years of changes.
Some parts were modern.
Some parts were old.
And some parts were a combination of both.
The architecture looked roughly like this:
Browser
|
v
Legacy UI
|
+---- Old components
|
+---- New components
|
+---- API calls
|
+---- Third-party scripts
|
+---- Analytics
The biggest mistake would have been treating the project as a completely new application.
The legacy system wasn't just code.
It was also:
- Existing URLs
- Existing users
- Existing SEO rankings
- Existing analytics
- Existing API contracts
- Existing business rules
- Existing integrations
- Existing browser behavior
So before writing new UI code, I needed to understand what I was actually replacing.
Step 1: I Mapped the Existing Application
Before touching the UI, I created a simple inventory.
I wanted answers to questions like:
- Which pages are most important?
- Which URLs receive the most traffic?
- Which components are reused?
- Which API endpoints are critical?
- Which pages contain business logic?
- Which third-party scripts are running?
- What can users actually do on each page?
I created a basic table:
| Area | Current State | Risk | Replacement |
|---|---|---|---|
| Homepage | Legacy | High | New |
| Navigation | Legacy | High | New |
| Search | Mixed | High | Incremental |
| Account pages | Legacy | High | Later |
| Blog | Stable | Low | Keep |
| Checkout | Critical | Very High | Don't touch initially |
This exercise changed the project completely.
I realized I didn't need to rewrite everything.
I needed to identify what actually needed changing.
Step 2: I Defined the "Do Not Break" List
A redesign needs boundaries.
Without them, it's easy to focus entirely on visual improvements while accidentally changing behavior.
I created a list of things that had to continue working.
Functional requirements
- Login must continue working
- Search must return the same results
- Forms must submit correctly
- Existing APIs must remain compatible
- Existing navigation paths must work
- Checkout must remain untouched
- Existing integrations must continue loading
Technical requirements
- No breaking API changes
- No unnecessary database changes
- No URL changes unless required
- No large production migration
- No big-bang deployment
Business requirements
- No significant SEO loss
- No analytics data disappearing
- No conversion-critical flows changing unexpectedly
This became our definition of "safe."
Step 3: I Built the New UI Beside the Old UI
Instead of replacing the entire application, I introduced the new design incrementally.
The architecture became:
Application
|
+---------+---------+
| |
Legacy UI New UI
| |
+---------+---------+
|
Shared
Services
|
APIs / Data
The important part was that both systems could temporarily coexist.
That gave us something incredibly valuable:
a rollback strategy.
If the new implementation failed, we could switch users back to the old one.
Step 4: I Started With the Design System
One of the biggest mistakes in redesign projects is starting with individual pages.
I started with reusable building blocks instead.
For example:
Design System
│
├── Colors
├── Typography
├── Spacing
├── Buttons
├── Inputs
├── Cards
├── Modals
├── Navigation
└── Feedback states
Then pages were composed from those pieces.
Instead of:
Homepage
├── custom button
├── custom card
└── custom form
Dashboard
├── different button
├── different card
└── different form
we moved toward:
Shared Components
|
+---- Homepage
|
+---- Dashboard
|
+---- Settings
|
+---- Search
This made the redesign much easier to scale.
More importantly, it reduced the chance that every page would slowly develop its own version of the new design.
Step 5: I Kept the Data Layer Stable
The temptation during a redesign is to modernize everything at once.
New frontend.
New API.
New database schema.
New authentication.
New deployment system.
That sounds productive.
It is also a fantastic way to create a very difficult debugging session.
Instead, I separated UI changes from data changes.
The new interface could consume the existing APIs.
For example:
async function getUserProfile(userId) {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error("Failed to load profile");
}
return response.json();
}
The UI could change completely while the underlying contract stayed stable.
That gave us a useful rule:
Change one layer at a time.
Step 6: I Used Feature Flags
One of the most useful techniques during the migration was feature flags.
Instead of:
if (newDesign) {
renderNewPage();
} else {
renderOldPage();
}
everywhere in the application, we centralized the decision:
const useNewHomepage = featureFlags.newHomepage;
Then:
return useNewHomepage
? <NewHomepage />
: <LegacyHomepage />;
This gave us control over the rollout.
For example:
Internal users
↓
New homepage
5% of users
↓
New homepage
25% of users
↓
New homepage
50% of users
↓
New homepage
100% of users
↓
New homepage
If something went wrong, we didn't need to deploy another version.
We could turn the feature off.
Step 7: I Tested the Critical Paths First
A redesign can look perfect and still be broken.
Visual testing wasn't enough.
I identified the flows that mattered most.
For example:
Homepage
↓
Search
↓
Product
↓
Add to cart
↓
Checkout
↓
Confirmation
And:
Login
↓
Dashboard
↓
Settings
↓
Logout
These became our critical paths.
Before expanding the rollout, we tested them repeatedly.
The key question wasn't:
"Does the new page look correct?"
It was:
"Can a real user still accomplish what they came here to do?"
Step 8: I Paid Attention to URLs
This was one of the easiest things to overlook.
A visual redesign doesn't necessarily require changing URLs.
If the old site had:
/products/123
there was little reason to suddenly change it to:
/catalog/product?id=123
Changing URLs can affect:
- Search engines
- Bookmarks
- External links
- Analytics
- Marketing campaigns
- Browser history
- Internal references
So whenever possible, I kept existing URLs intact.
When a URL genuinely needed to change, we treated the migration as a separate problem rather than hiding it inside the redesign.
Step 9: I Watched Production Metrics
Once the new UI reached real users, screenshots weren't enough.
We monitored production.
The important metrics depended on the application, but typically included:
- Error rates
- JavaScript exceptions
- API failures
- Page load performance
- Conversion rates
- Search usage
- Form completion
- Bounce rates
- User feedback
A redesign is successful when the experience improves without damaging the metrics that matter.
Sometimes a page can look dramatically better and perform worse.
That's why production data matters.
Step 10: I Released in Small Pieces
The final rollout looked more like this:
Phase 1
↓
Design system
Phase 2
↓
New navigation
Phase 3
↓
Homepage
Phase 4
↓
Search
Phase 5
↓
Secondary pages
Phase 6
↓
Remove legacy components
Not:
6 months of development
↓
Friday 5 PM
↓
DEPLOY EVERYTHING 🚀
↓
production explodes
Small releases made problems easier to isolate.
If something broke after the new navigation was deployed, we knew where to look.
If everything changed simultaneously, debugging would have been much harder.
What I Didn't Rewrite
This might be the most important lesson.
I didn't rewrite everything.
Some parts of the application were old but stable.
And stable code isn't automatically bad code.
If a component:
- Worked reliably
- Had good test coverage
- Didn't block the redesign
- Didn't create measurable problems
then I left it alone.
Technical debt should be addressed based on its cost, not simply because it is old.
Sometimes the best engineering decision is:
Don't touch it yet.
The Biggest Mistakes I Avoided
1. Big-bang rewrites
Rewriting the entire application creates a huge gap between development and production.
The longer the project takes, the more the two systems drift apart.
Incremental migration keeps that gap smaller.
2. Mixing unrelated refactors
During the redesign, there were plenty of opportunities to say:
"Since we're here, let's also rewrite authentication."
No.
That's how scope explodes.
I tried to keep changes focused.
A redesign should not automatically become an excuse to rewrite every subsystem.
3. Treating visual similarity as success
A page can match the design perfectly and still have:
- Broken keyboard navigation
- Slow performance
- Missing analytics
- Incorrect API behavior
- Poor mobile behavior
- Broken error states
Pixel-perfect isn't the same as production-ready.
4. Removing legacy code too early
New code doesn't become trustworthy the moment it is deployed.
I kept the old implementation available until the new version had proven itself.
Only then did we remove the old code.
The Architecture After the Migration
Eventually, the application moved toward something like:
┌───────────────┐
│ Browser │
└───────┬───────┘
│
┌───────▼───────┐
│ New UI │
└───────┬───────┘
│
┌──────────▼──────────┐
│ Shared Components │
│ + Services │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Existing APIs │
└──────────┬──────────┘
│
┌───────▼───────┐
│ Existing Data │
└───────────────┘
The important thing is that the migration happened around the existing production system, not by destroying it first.
What I Learned
The biggest lesson wasn't about React, CSS, JavaScript, or any particular framework.
It was about risk management.
A legacy redesign is fundamentally a migration problem.
You are moving users from one system to another while the business continues operating.
That means the best strategy is usually:
- Understand what exists.
- Identify what cannot break.
- Separate UI changes from backend changes.
- Build reusable components.
- Introduce the new experience incrementally.
- Use feature flags.
- Monitor real production behavior.
- Roll out gradually.
- Keep rollback possible.
- Remove the old implementation only after the new one is proven.
The goal isn't to make the old code disappear as quickly as possible.
The goal is to make the new system trustworthy enough that you no longer need the old one.
Final Takeaway
If you're about to redesign a legacy website, don't ask:
"How can I rebuild this from scratch?"
Ask:
"How can I move users from the old experience to the new one without creating unnecessary risk?"
That small change in perspective can completely change your architecture, deployment strategy, and development process.
A successful redesign isn't the one with the most rewritten code.
It's the one where users get a better experience and nobody gets paged at 2 AM because production went down.
And honestly, that's the kind of redesign I'm happy to ship.
Top comments (0)