Why did I rewrite my website from scratch to make it (almost) identical?
This is not a text (only) about Next.js or React, but about the hidden complexities of software engineering decisions.
Given the need to add a new section to my personal website kaic.me to host my published articles (like this one!), I concluded that it would be best to rewrite it from scratch to make this happen. In the process, in addition to eliminating some technical debts, I also took the opportunity to made some visual changes - something that could have been done in the previous architecture.
In software development, redesigning an entire system to do something that could be done "with less effort" or to appear virtually unchanged to the end user may seem counterintuitive. However, such decisions often arise from complex technical considerations that go beyond aesthetics or technical neatness.
Here, I will explore the reasons behind a complete rewrite of my website "just" to add a new section, resulting in an interface that, to a casual observer, looks almost identical (taking out the new section part) to the previous one, or to a developer could have been built on the already existing codebase. I will also discuss the typical tradeoffs that developers face, the difference between refactoring and rewriting code, and how these choices impact the maintainability of the code.
The goal is to provide a critical perspective on when it makes more sense to refactor and when a complete rewrite is the best alternative to address deeper technical issues.
The Initial Scenario
Initially, the site's operation relied on direct manipulation of the index.html to update content. To add new sections, I had to update view.js file (along with index.html) with logic to insert the correct HTML. Here is an example of what the new Articles section would look like based on one that already existed on the site:
import { insertHTML } from './view'
import articles from '../assets/jsons/articles.json'
const articlesHtml = `
<div class="...">
${articles.map(article => {
return `
<div class="article-container">
<h3>${article.title}</h3>
...
</div>`
}).join('')}
<hr />
</div>`
insertHTML('articles', articlesHtml)Complexity and Technical Debts
That file view.js became a single point for content updates, but over time, this became harder to maintain. Each time a new section was added, more logic had to be inserted (in addition to manipulating index.html), resulting in repeated code and increasing the risk of errors.
This approach mirrors Martin Fowler's concept of Technical Debt from his book Refactoring: Improving the Design of Existing Code. In simple terms, technical debt refers to the long-term costs of using quick, suboptimal solutions that accumulate over time, making the code harder to maintain and scale.
In my case, manually editing the view.js and and manipulate the index.html for each new section created technical debt in two key ways:
Maintainability: Whenever I needed to add new features, update the layout, or modify content, I had to dig into a bloated HTML file with a mix of static and dynamic elements. This made even simple changes become more complex and time-consuming.
Scalability: As the website grew and I added more sections, I had to keep updating the two files and ensure each dynamic part was manually linked and loaded. This became error-prone, especially when maintaining consistent structure and behavior across the site.
Fowler stresses that, as technical debt accumulates, even small changes become costly and clunky. In my case, I would spend more time managing the complexity of the codebase than actually adding value to it by adding the new section. The challenge was no longer just about content, but about navigating increasing amounts of technical debt and the friction it caused.
The Decision to Rewrite: Analysis
Maintaining the manual approach was becoming increasingly costly. Along with issues such as deployment, style management, and internationalization, I recognized the urgent need to upgrade to a more modern tech stack. This realization led me to the decision to completely rewrite the site using Next.js.
This move was planned to tackle existing technical debts more effectively while significantly enhancing scalability and maintainability. By leveraging React, I could build modular and dynamic components that would simplify content management and facilitate seamless future expansions. This approach was undeniably the right fit for my evolving needs.
Several key factors strongly influenced my decision to undertake this rewrite:
1. Maintainability: Despite the original codebase being relatively modular and effective for a considerable time, the effort required to add new functionalities had become overwhelming. The increasing volume of boilerplate code for updates created a significant burden, making maintenance increasingly challenging.
2. Scalability: Anticipated growth in content and features necessitated a more robust architecture to handle future demands.
3. Modernization: To incorporate contemporary development practices and tools, a foundational overhaul was essential.
A risk assessment indicated that implementing revealed that pursuing an incremental refactor would likely exacerbate existing issues and prolong the transition period. In contrast, a complete rewrite presented an opportunity to develop a scalable, and maintainable solution from the ground up, setting the stage for the future.
Navigating Trade-offs in Software Engineering
Every engineering decision involves trade-offs, and choosing between refactoring and a complete rewrite is no exception. The challenges I faced—such as technical debt, scalability issues, and maintainability concerns—illustrate a broader set of common dilemmas in software development:
Short-term fixes vs. Long-term sustainability – Quick solutions speed up releases but often lead to accumulated technical debt.
Technical debt vs. Delivering value – Rushing features may provide immediate gains but slow down future development.
Performance vs. Flexibility – Highly optimized code can be rigid, while more flexible architectures may introduce inefficiencies.
Familiarity vs. Modernization – Sticking with known technologies ensures stability, but sometimes a new stack is necessary for growth.
In my situation, continued incremental refactoring would have prolonged existing issues. In contrast, a complete rewrite allowed for a more scalable, maintainable, and future-proof solution. Recognizing and assessing these trade-offs is vital when making architectural decisions, as they influence not only the codebase but also the speed and quality of future development.Case Study: Features Rewritten
Case study: Rewritten features
Feature 1: Article Section
The feature that made the article materialize
Before: The articles section was practically non-existent. It just had text.
After: The addition of a feature that generated the entire rewrite of the site in the end was one of the simplest things to do due to the gains with the new stack. I created a dynamic component using that automatically loads articles from a structured list.
Every time I need to add a new article, I just update the list and deploy it
//src/app/Articles/components/ArticleLinks.tsx
export interface IArticle {
title: string;
description?: string;
date: string;
url: string;
}
const articlesList: IArticle[] = [
{
title: "Be your tool's best friend, not its enemy",
description: "i.e. don't spend the day fighting against your computer.",
date: '2024-12-22',
url: 'https://kaicbento.substack.com/p/be-your-tools-best-friend-not-its?r=5vxpr',
},
];
return (
<article id='articles'>
...
<ul className='list-inside text-sm text-center sm:text-left'>
{articlesList.map((article, index) => (
<li key={index} className='mb-4'>
<ArticleLink article={article}></ArticleLink>
</li>
))}
</ul>
...
</article>
);
//src/app/Articles/components/ArticleLinks.tsx
export const ArticleLink = ({ article }: ArticleLinkProps) => {
return (
<div className='p-4 text-left block'>
<h3 className='text-lg font-bold text-white mb-2'>{article.title}</h3>
{article.description && (
<p className='text-gray-200 text-base mb-2'>{article.description}</p>
)}
<p className='text-gray-200 text-sm mb-2'>{article.date}</p>
...
</div>
);
};Feature 2: Internationalization
Before: One of the key differences in the rewrite was how internationalization was handled. The previous implementation relied on manually mapping text elements to translation HTML elements IDs and injecting translated text into the DOM. While functional, this approach became inefficient as the site grew, requiring extensive manual handling for every new text element.
// nav bar links example in index.hml
<div id="nav-links">
<ul class="header-links-list text-center">
<li><a id='t-articles-link' href="#articles"></a></li>
<li><a id='t-about-link' href="#sobre"></a></li>
<li><a id='t-contact-link' href="#contato"></a></li>
<li><button id='translate-btn'></button></li>
</ul>
</div>
//view.js
const headerTextElements = [{ id: 't-articles-link', args: null }, { id: 't-about-link', args: null }, { id: 't-contact-link', args: null },]
export const loadTexts = async () => {
await loadArticleTexts('header', headerTextElements)
...
}After: With the rewrite, I transitioned to using next-intl, a React-based internationalization library. This allowed translations to be dynamically retrieved and rendered directly within components, eliminating the need for manual mapping.
//locales/en.json
{
"Articles": {
"section-name": "My Articles"
}
...
}
//src/app/Articles/Articles.tsx
return (
<article id='articles'>
<div className='...'>
<h2 className='...'>{t('section-name')}</h2>
...
</div>
</article>
);The new implementation also intelligently detects the user's browser locale on the first visit, automatically displaying content in their preferred language. When users manually switch languages via the language toggle, their preference is stored in localStorage, ensuring their language choice persists across sessions. This creates a more personalized experience without requiring any additional effort from the user.
//pages/_app.tsx
useEffect(() => {
// Try to get saved locale from localStorage first
const savedLocale = localStorage.getItem('preferred-locale');
if (!savedLocale) {
// If no saved preference, detect from browser
const browserLocale = navigator.language.toLowerCase().startsWith('pt')
? 'pt-br'
: 'en';
localStorage.setItem('preferred-locale', browserLocale);
setLocale(browserLocale);
} else {
setLocale(savedLocale);
}
}, []);This approach offers a significantly improved user experience while also making the codebase more maintainable as new content is added.
Feature 3: Styling and Theming
Before: CSS classes were spread out across SCSS files without much standardization, requiring media queries and mixins to handle responsiveness manually.
After: Fully migrated to Tailwind CSS, reducing boilerplate styles and ensuring a consistent, scalable, and maintainable styling approach with utility classes.
😮 I didn't have to write a single line of CSS.
//src/app/Articles/Articles.tsx
<article id='articles'>
<div className='grid grid-rows-[20px_1fr_20px] p-8 pb-0 gap-12 sm:p-20'>
<h2 className='text-2xl font-bold'>{t('section-name')}</h2>
<ul className='list-inside text-sm text-center sm:text-left'>
{articlesList.map((article, index) => (
<li key={index} className='mb-4'>
<ArticleLink article={article}></ArticleLink>
</li>
))}
</ul>
</div>
</article>Feature 3 Deployment
Before: Deployment was handled manually using a script that pushed built files to GitHub Pages.
//bin/gh-pages.js
require('dotenv').config()
const ghpages = require('gh-pages');
(async () => {
await ghpages.publish('dist', { branch: 'main', message: `Auto-generated commit - ${new Date().toLocaleString()}` }, err => {
if (err) console.error(err);
console.log('Published to GitHub Pages 🚀');
});
})();After: Replaced manual deployment with a GitHub Actions workflow, which executes the deploy on each push to the main branch. automating the entire process, reducing human intervention, and ensuring reliable, reproducible deployments.
This migration eliminated manual errors, made deployments more efficient, and provided a scalable, automated, and reproducible workflow.
These enhancements addressed underlying issues that were previously invisible to users but crucial for long-term site health.
Framework for Decision - Making in Future Projects
The decision to rewrite my website was motivated by several factors, including technical debt, scalability, and the need for modernization. For future projects, consider these key questions to help you decide between refactoring and rewriting:
Is the current architecture limiting new feature development?
If introducing new features is becoming increasingly challenging, it may be time for a rewrite.
Are structural limitations affecting performance?
An outdated architecture could be a significant performance bottleneck. A new approach might enhance efficiency.
Is the code compliant with modern security standards?
Rewriting it using a new technology stack may not always be the best option, especially if we can update the existing code to meet these security standards.?
Is technical debt growing faster than it can be managed?
When technical debt accumulates to a point where it becomes unmanageable, a rewrite may be the only viable solution.
Could a rewrite provide significant advantages and improve the developer experience?
A new codebase could address technical issues and enhance productivity and collaboration.
Carefully evaluating these points will help you determine whether a rewrite is more suitable than ongoing incremental refactoring. Always prioritize user experience while strengthening the system's internal robustness.
Conclusion
Rebuilding my website from the ground up, though it might seem counterintuitive just to add a new section, was a strategic choice motivated by the need to address technical debt, overcome scalability challenges, and modernize the site. This process enhanced the site's maintainability but also removed some technical debts and allowed me to make some visual updates more quickly. Additionally, it enabled the adoption of modern practices, improved the developer experience, and established a more scalable architecture.
By sharing this experience, I hope to emphasize the importance of recognizing when a complete rewrite is the best solution rather than a refactoring and how to make decisions taking into account an analysis of what you want to achieve in addition to the tradeoffs involved.
Have you ever done a major rewrite for a seemingly small feature? What was your biggest challenge?
Feel free to visit my website and see the final result at kaic.me!





