Nuxt 2 to Astro 3 Replatforming – from Setup to Production
I keep hearing great things about Astro, and just so happened to have a personal project that I needed to move off of Nuxt 2 as its end of life date is rapidly approaching.
Here’s my journey from never using Astro to pushing the refactored codebase live. Luckily, there’s a handy Nuxt to Astro migration guide that I’ll be using to get started.
Initial Setup#anchor
Install#anchor
Thanks Astro installer for the reminder that I’m on an embarrassingly old version of Node. Got the new shiny Node LTS version 20.9.0 installed and ready to roll.
First impressions matter, and the Astro installer was really great. The step by step questions to get the project set up using npm create astro@latest
was a great touch. I didn’t bother with adding TypeScript support for this, sorry TS fam but this project ain’t it.
Opened the demo index.astro
file and realized I didn’t have any code formatting. Quick search and – of course – there’s a whole page for IDE setup. I installed the Astro VS Code Extension and I was up and running. Also ended up adding the Astro-specific Prettier plugin too.
Initial Struggles#anchor
Web Components#anchor
As this was the first bigger topic I tackled (and struggled with), I ended up blogging about this particular topic separately over at Web Components in Astro.
Check that out for a deeper dive of my initial struggles wrapping my mind around astro components and how they can play nice with native web components.
Avoiding TypeScript#anchor
Little did I know that the “I didn’t bother with adding TypeScript support for this” comment above would come back to haunt me. Turns out, there’s no way to truly opt out of TypeScript and if you’re using Astro VS Code Extension you’re also locked into TS linting that surfaces TS errors too.
I did a whole lot of increasingly desperate searches, trolled through the Astro Language Tools Github issues, and even joined the official Discord to finally come to this conclusion. I was particularly bummed to find out that the previous V1 version for the Astro VS Code plugin had an option for this, and it had to be removed (I think) due to the language server swap to Volar.
For sake of completeness, here’s some of what I tried unsuccessfully to disable TypeScript linting in VS Code for this Astro project:
- Set
"typescript.validate.enable": false,
in my VS Code workspace settings & restarted - In the
tsconfig.json
file- Set
"exclude": ["src"]
- Set
{ "compilerOptions": { "skipLibCheck": true } }
- Set
- In a newly created
tslint.json
file- Set
"linterOptions": { "exclude": [ "src" ] }
- Set
I’m unsure why the standard advice of adding rules to workspace settings, tsconfig, and tslint didn’t work, but I have a feeling it’s due to the linting coming from another extension.
The Only Thing That Worked Disabling TS Errors#anchor
Because this is a relatively small project and I was thoroughly defeated with all the above research and attempts, I finally gave up. Here’s what I’m using to be able to move forward without errors:
- Adding
// @ts-nocheck
at the top of files that I wanted to ensure the whole file ignored TS errors. - In smaller use cases, I’m using
// @ts-ignore
for individual statements to ignore.
I’m not sure why file-based ignoring works where project and IDE workspace settings didn’t, so if you have any explanation I’d love to hear about it. Will update this post if I do get some clarification.
Layouts & Styles#anchor
I figured, while I’m tearing everything apart I might as well overhaul the styles a bit to take advantage of fluid type and space, design tokens, and to simplify the overall setup.
Without considering that refactoring, this was the most straightforward part of the process. I copy/pasted all the style folders into a new src/assets/css
folder and imported the app.css file in my primary layout and it just worked. I opted not to use the src/styles
convention because it wasn’t required and I typically use an assets directory in other projects so that felt more comfortable.
Migrating Content#anchor
Static Content#anchor
Static content goes from /static
to the /public
folder, simple enough! This basically consisted of the code examples (which are saved as independent minimal HTML, CSS, and JavaScript files) and images including the favicon.
Page Content#anchor
For actual site pages, I didn’t have a whole lot more than plain HTML in my page components so moving them consisted of:
- copying the
.vue
files over and renaming them to.astro
- removing the
<template>
container and replacing with<Layout>
- Moving the page title from the Vue component to the Layout container.
- Changing links from
<nuxt-link>
to plain<a>
links.
For example, here’s some pseudocode that encompasses all my pages structured in Nuxt .vue
components, using the Accessibility Statement as an example.
<template>
<article>
<h1>Accessibility Statement</h1>
<p>Page Content.</p>
</article>
</template>
<script>
export default {
data() {
return { title: 'Accessibility Statement' };
},
head() {
return { title: this.title };
},
};
</script>
This page moved to Astro then looked like this.
---
import Layout from '../layouts/Layout.astro';
---
<Layout title="Accessibility Statement">
<article class="page-container">
<h1>Accessibility Statement</h1>
<p>Page Content.</p>
</article>
</Layout>
For any Nuxt links in the content, I did a find and replace and that worked out just fine. Nuxt links like <nuxt-link to="/solutions/focus/">Focus</nuxt-link>
changed to <a href="/solutions/focus/">Focus</a>
instead.
Dynamic Routes#anchor
I would say this ended up being the vast majority of my work in migrating over, dynamic routes is how static content is compiled into pages based on the content inside /content
and the route templates in /pages
.
I referred back to the Convert NuxtJS Syntax to Astro reference quite a bit for these routes.
Lack of Content Subfolder Support#anchor
The biggest difference between Nuxt and Astro is that it appears Astro doesn’t allow for nested content collections (source), so I needed to pull subfolders up to the root content folder all together under “solutions”. Not my favorite, but I’m going with it so I don’t have to figure out a customized workaround to do it.
Another interesting tidbit is that content slugs have to all be unique, even if they’re in different content subfolders.
Migrating Components#anchor
Vue Components#anchor
At first, I had grand ambitions of not pulling Vue into this project and going vanilla JS for everything. But for the sake of time to get everything ported over and online as soon as possible, I opted to keep some Vue components.
To add Vue support on an existing Astro installation, you can use the Astro Add command. So in my case I did npx astro add vue
and it loaded everything up including the necessary configuration updates.
Luckily, I didn’t have to change much in my Vue components. Even though I was also upgrading from Vue 2 to Vue 3 as part of this process. Woohoo!
For me, it mostly consisted of swapping from v-bind.sync
to v-model
. If you’re jumping from Vue 2 to Vue 3 as part of this too, I recommend reading through the migration guide for additional breaking changes.
Vue + Web Components#anchor
I had a bit of trouble getting Vue to play nice with my native web components, and realized I’ve not had to address this before. Vue will try to find a registered Vue component for any custom tags it encounters, throwing a “failed to resolve component” error for any that don’t match.
After some research I found out that there’s a really elegant solution to this with a configuration update, setting isCustomElement
to be anything with a dash (or in my case ending with a -l
) in the compilerOptions
is all it takes.
To Astro-ify the above solution, these same settings can be added to the Vue portion of the Astro config at astro.config.mjs
. Mine ended up looking like the following:
import { defineConfig } from 'astro/config';
import vue from '@astrojs/vue';
// https://astro.build/config
export default defineConfig({
integrations: [
vue({
template: {
compilerOptions: {
// treat all tags with a -l as custom elements
isCustomElement: (tag) => tag.includes('-l'),
},
},
}),
],
});
Finishing Touches#anchor
Great, I’ve got all my pages loading again! Now for a few remaining items that I had before and wanted to maintain in this new codebase.
Site Search#anchor
Nuxt 2 had a full text search option built in, that was what I used for the site search functionality before. I’m unsure if there is something comparable for Nuxt 3 without relying on an third party provider like Algolia.
I really didn’t want to have to give up the full site search functionality, and I didn’t see anything built in to help pull this off. I did a bit of searching and found a couple options, ended up sticking with Pagefind through CloudCannon. I really liked this walkthrough by Reuben Tier for getting it set up specifically in Astro.
That worked really great, and it’s part of the build process so it can’t get much more set-it-and-forget-it than that.
Sitemap Generator#anchor
Now that the site itself is looking pretty good, I set my sights to cleaning it up for a production launch. The first thing I look at when doing that is making sure I have an accurate sitemap.
Astro sites don’t come with a sitemap by default, but there is a really easy integration for adding it here. Essentially, you just need to run through that installer and then add the root site URL to the config. When it was done, mine ended up looking like this
export default defineConfig({
site: 'https://a11y-solutions.stevenwoodson.com',
integrations: [vue({
template: {
compilerOptions: {
// treat all tags with a -l as custom elements
isCustomElement: tag => tag.includes('-l')
}
}
}), sitemap()]
});
View Transitions#anchor
There’s a neat built-in feature of Nuxt that lets you do cross-fade page transitions. I thought, since Astro would be static generated, I would lose that little nice to have. Turns out I was wrong! Astro View Transitions to the rescue, I imported and added the <ViewTransitions />
to my primary layout and it worked.
Only thing I noticed, was that the main navigation would briefly lose its style every time I clicked on a new page. It looks like that may be because it’s a Vue component loaded in an astro island? Utilizing the transition:persist
property to persist its state worked in this case, thankfully. I had experimented with moving the CSS of that component to the global styles which also helped, but I much prefer the portability of keeping styles with the component so I’m sticking with the persist property instead.
Final Thoughts#anchor
This was a great exercise for me to learn about and explore Astro, and besides the few snags I hit early on it was as smooth of a process as I could have hoped. Here’s some general notes now that I’m done.
Ideal Projects for Astro#anchor
I now feel like I have a better understanding of what kind of projects will benefit most from this framework. I think the sweet spot would be sites that are mostly static, but have some mild to moderate interactivity that could benefit from a more robust library.
For example, as noted above in the Vue Components section, there were cases where I tried to do the JavaScript heavy lifting within an Astro component with the ultimate goal being to remove Vue altogether.
But, all that vanilla JS added up quickly. Shifting back to Vue for only the component pieces that made sense shrunk the code in those components to less than half.
Performance#anchor
This is a relatively small site, and I was able to get most of it statically generated both in Nuxt and Astro, so I didn’t notice a whole lot of performance change between the two versions.
Looks like Lighthouse agreed with that sentiment, both received a 99 of 100. That 1 is likely due to some cumulative layout shift that I really should dig into.
I do, however, feel like I had a much better control and understanding of components that could potentially affect performance and more control over how their handled because of the concept of Astro Islands.
Time to Complete Replatform#anchor
While this blog post is rather long, I do feel like I’ve glossed over the more time intensive parts which may give a the wrong impression regarding how long this actually took me. So I’m going to try to describe the replatforming efforts that took the most time, and then estimate how long it all took me to complete.
Replatforming Accessibility Solutions included the following:
- Learning Astro
- Nuxt 2 to Astro 3 conversion
- Deep dive into Web Components within Astro
- Vue 2 to Vue 3 conversion
- SCSS to vanilla CSS
- Styles overhauled to use custom properties and fluid typography & spacing
- Native search to Pagefind
With all the above, I’d estimate this took me around 80 hours to complete. Inclusive of reading documentation, coding, troubleshooting, testing, and releasing.
Need help with an upcoming project?
I'd love to hear more about what you have coming up and how I can help bring it to life! My earliest availability is currently Q1 2025.
Get in Touch