Rebuilding My Website: From Next.js to Astro
Why change something that isn’t broken? That’s what I thought for a long time, but the idea of improvement was always at the back of my mind. After trying Cursor, a new AI code editor, I realized how much I could streamline my development process. So, I decided to give it a shot - after all, how much work could it be?
As it turns out, still quite a bit.
The Migration Scope
This project was more than just a blog migration. It included interactive tutorials, small projects, and tools that I use regularly.
The migration involved several major changes:
- Framework: NextJS 11 → Astro 5.0
- Styling: CSS Modules → TailwindCSS
- UI Library: React → SolidJS
- Language: JavaScript → TypeScript
These changes were driven by two main goals: better performance and improved maintainability.
Results
The most significant achievement was eliminating 11k lines of custom code, primarily from custom remark plugins that were replaced by Astro’s built-in functionality.
Another major improvement was the reduction in JavaScript bundle size:
- Before: 98kB (minified + gzipped)
- After: 57kB (minified + gzipped)
This 42% reduction in bundle size directly translates to faster load times and better user experience. What makes this even more impressive is that I added more features and interactivity in the process, making the actual improvement even more substantial.
Cool Technical Highlights
AI-Powered Migration
One of the most fascinating aspects was leveraging Cursor to automate the conversion of JavaScript React components to TypeScript SolidJS.
My systematic process for each component was:
- Use Cursor to convert to TypeScript
- Review and debug the conversion
- Use Cursor to convert to SolidJS
- Review and debug the final output
Cursor achieved a 90% success rate in JavaScript to TypeScript conversions.
For SolidJS conversions, the results were mixed. While it handled simple components flawlessly, it only achieved a 40% success rate with complex/lengthy files. This limitation largely stemmed from the fundamental differences between React’s hooks and SolidJS’s signals paradigms.
Doing this manually would have been a nightmare. It would have been nice to do this in bulk, but I wanted to make sure the conversion was correct.
Modern Animation Techniques
I decided to add some modern animation techniques:
- Gradient text clipping and SVG graphics animations on the home page
- Spring physics-based animations using the Motion library for the theme toggle
It’s quite amazing to see that the field of web design improved so much over the last 2 years. I took a lot of inspiration from Jhey, Emil Kowalski and others.
Challenges and Learnings
Time Investment
The migration timeline was interesting:
- 80% of the work was completed in 2 days
- The remaining 20% and polishing took 7 days
This follows the classic 80/20 rule, where the last mile often requires the most effort.
Technical Hurdles
Font Performance
Font optimization proved to be more complex than anticipated. My initial approach of using an unoptimized variable font resulted in a hefty 189kB file size.
The optimization process involved several challenges:
- Implementing efficient variable font subsetting
- Determining optimal font loading strategies (preload vs swap vs optional)
- Implementing seamless font fallbacks during loading
I’ll be diving deeper into these optimizations in a future post.
The Modern Web Stack
Web development tools change fast, but one thing remains constant: its complexity.
Content Processing Stack:
remark
: Parses Markdown into an Abstract Syntax Tree (AST)rehype
: Transforms the AST into HTMLMDX
: Enables React/Solid components in Markdown
The challenge here is that each tool has its own plugin ecosystem, and they don’t always play nicely together.
For example, remark-smartypants
is a remark plugin that does a bunch of typographic improvements, like converting straight quotes into smart quotes (e.g., "boring"
to “boring”
).
But, MDX feeds it one AST node at a time, and smart quotes need to be converted in pairs, so it fails in links (Astro issue #4448).
A specific example I encountered was with code syntax highlighting. I wanted to use the same code blocks during server-side rendering and client-side rendering, which necessitates that the syntax highlighting gets done in Astro, and I can use SolidJS to make it interactive.
The pipeline looks like this, I think:
MDX text → remark → shiki-remark (remark plugin) → rehype → Astro → MDX components → SolidJS → Final Output
If I replaced the shiki remark plugin with a tree-sitter remark plugin, in Astro I only get the syntax highlighted HTML, which is not what I want.
If I do it in MDX, I get raw text, but embedded in <code>
html tags, and I lose the language attribute.
I gave up on this for now and just use the shiki remark plugin, but I believe to make it work, I will need a remark plugin to pass the metadata attributes like lang
to MDX,
then use tree-sitter parser in Astro during build time to get the AST, then feed it to SolidJS.
Build Stack:
rollup
: The core bundler, handling module resolution and code splittingvite
: Provides the dev server and modern build tooling on top of rollupastro
: Orchestrates everything, adding its own layer of static site generation
The complexity arises when these tools interact. For instance:
- Astro’s HTML generation and Vite’s
transformIndexHtml
hook are both trying to manipulate the HTML. Astro’s at dev and build time, and Vite’s only at build time. - Astro has build hooks, Vite has build hooks, and Rollup has build hooks. Where, when, and for which layer do you do your transformations?
A roadblock I hit here was improving the font performance. I initially used the most SEO-ed Astro font plugin, but it preloaded all fonts, which is suboptimal.
In order to do better, I forked the plugin and realized it was doing everything from scratch on every page load, including fetching Google fonts and parsing the variable font.
This could all be done during the initialization phase, so it seemed like an astro or vite plugin would be the “right” solution. As it turns out, to pass config from build time to runtime,
you need to provide a virtual file in vite, then import it in an astro component. I also couldn’t simply modify the HTML <head>
, because astro
is in charge of generating the HTML during development, not vite.
In the end, I hit a realization: If I could let go of the idea of passing the config to the runtime, it’s a build-time problem. I just needed a build-time script to generate the font, CSS, and Astro file before anything starts. So I created a simple script that does that.
Looking Forward
This migration has been more than just a technical upgrade—it’s been a journey through the modern web development landscape. While the process had its challenges, the end result is a faster, more maintainable, and more feature-rich blog.
Stay tuned for more detailed posts about optimizations and the intricacies that went into it!