How to Migrate Your Vue 3 App to Nuxt 3

Natalia Afanaseva
6 min readJun 2, 2024

--

If you’ve been working on a project using Vue 3 and realised late in the game that it doesn’t comply with SEO principles or that you want to improve the load time of the client side, don’t worry — you’re not alone. This guide will walk you through upgrading your Vue app to take full advantage of Nuxt 3’s capabilities.

Prerequisites

First, ensure you have the correct version of Node.js (18.0.0+).

Setting Up the Project

1. Create a New Project: Start by setting up a new Nuxt 3 project with a basic configuration. You can find the installation instructions here.

2. Pages and Routing:

  • Create a pages folder and add an index.vue file as your home page.
  • For each route in your Vue 3 router, create a corresponding file in the pages folder. For dynamic routes, use square brackets for parameters (e.g., [id].vue).
// Vue
- app-name
- /src
- main.js
- /router
- index.js
- /views
- Home.vue
- Profile.vue
- Catalogue.vue
- CatalogueItem.vue

// /router/index.js

const router = createRouter({
...
routes: [
{ path: '/', component: Home },
{ path: '/profile', component: Profile },
{ path: '/catalogue',
component: Catalogue,
children: [{ path: '/:id', component: CatalogueItem }]
},
]
})


// Nuxt - the routes will be created automatically based on the pages folder
- app-name
- app.vue
- /pages
- index.vue
- profile.vue
- /catalogue
- index.vue
- [id].vue
  • Copy the content from your Vue 3 files into the new Nuxt 3 files.

3. Layouts:

  • If your app uses multiple layouts (e.g., different headers for logged-in and logged-out users), create a layouts folder.
  • Create a default.vue file for the default layout. Add other layout files as needed.
// default.vue
<template>
<LoggedOutHeader />
<slot />
<Footer />
</template>

// logged-in.vue
<template>
<LoggedInHeader />
<slot />
<Footer />
</template>
  • If you only have one layout, you can skip this step.

4. App.vue:

  • app.vue is the new entry point to your app instead of main.js in Vue. You can clear the default code from it.
  • If you have a single layout, define it here. For multiple layouts, use NuxtLayout to render the default layout.
// app.vue
// If not set otherwise, all of the pages will use the default layout
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

// page-with-custom-layout.vue
<template>
// some content
</template>

<script>
// this page will use the logged-in.vue layout
definePageMeta({
layout: 'logged-in'
})
</script>
  • Insert NuxtPage to handle routing based on the files in the pages folder.

5. Error Page:

  • Create a custom error page next to app.vue. If there’s no error.vue , Nuxt will render its own default error page.
- app-name
- app.vue
- error.vue

Configuring the App

6. Nuxt Config File:

  • Check your Vue project’s dependencies. Not all can be used the same way in Nuxt (e.g., i18n, forms, validation, Pinia, maps, etc.).
  • Refer to the official Nuxt modules or find Nuxt-compatible versions of the libraries you used. Install and configure them in the modules section of the nuxt.config file.
// nuxt.config.ts
export default defineNuxtConfig({
...
modules: [
...
"@pinia/nuxt"
]
})
  • If a library isn’t supported by Nuxt 3, search for alternatives or use the Vue 3 version for client-side code only. Wrap these components in client-only.
<script>
import ClientOnlyComponent from "client-only-library"
</script>

<template>
<client-only>
<ClientOnlyComponent />
</client-only>
</template>
  • Configure route rules to determine which pages shouldn’t be rendered on the server (by default, all are). Set ssr: false for specific routes (e.g., protected routes or those not needing indexing).
// nuxt.config.ts
export default defineNuxtConfig({
...
routeRules: {
"/protected-route": { ssr: false }
}
})
  • Include your public keys from the .env file in runtimeConfig and provide default values. Make sure you do not provide any secrets here because these values will be exposed to your users.
// nuxt.config.ts
export default defineNuxtConfig({
...
runtimeConfig: {
public: {
publicKey: ''
}
}
})
  • Update you local .env so that your keys start with NUXT_ or NUXT_PUBLIC_ (more information here).

7. Store:

  • Install the Nuxt Pinia plugin if you need to manage a shared store. Note that it’s not persistent by default (it may get lost on a page reload). To keep it, install the relevant plugin.
// store.ts

defineStore('store-name', {
state: ....,
persist: true
})

// app.vue
import state from 'pinia-plugin-persistedstate'
createPinia().use(state)

8. State Management:

  • Replace ref with useState for proper hydration. Use unique keys for shared components to avoid shared state issues between instances.
// This is a totally made up example, of course we can pass disabled as props

// ⛔️ Will result in two components share the same state

// button.vue
<script>
const disabled = useState('button-disabled', () => false)

const onClick = () => disabled.value = true
</script>

<template>
<button @click="onClick" :disabled="disabled">Click here</button>
</template>

// component.vue
// if we click on one button, the second one becomes disabled too
<template>
<Button />
<Button />
</template>


// ✅ Will work properly, two buttons will be independent

// button.vue
<script>
const props = defineProps<{
id: string
}>()
const disabled = useState(`button-${props.id}-disabled`, () => false)

const onClick = () => disabled.value = true
</script>

<template>
<button @click="onClick" :disabled="disabled">Click here</button>
</template>

// component.vue
<template>
<Button :id="'one'" />
<Button :id="'two'" />
</template>

9. SEO:

  • Configure desired SEO tags on each server-side rendered page. Include a “no-index” tag for pages to be ignored by search engines.
// page.vue
<script>
useHead({
....
meta: [
...
{ name: 'robots', content: 'noindex' }
]
})
</script>

10. Browser APIs:

  • Identify places where you use browser APIs (e.g., window, document, local storage). Ensure these are checked for existence or that you're on the client to avoid server-side errors.
// ssr-page.vue

// ⛔️ Will result in errors without an additional check
// because the page is rendered on the server
// without access to the browser APIs
<script>
onMounted(() => {
window.addEventListener('scroll', () => {})
})
</script>

// ✅ Will work correctly
<script>
onMounted(() => {
if (process.client || window) {
window.addEventListener('scroll', () => {})
}
})
</script>

11. API Requests:

  • Nuxt offers three options for handling API requests: useFetch, useAsyncData and $fetch. Choose your preferred method or create a custom wrapper if you have additional actions to perform before each request (e.g. checking for a token). Here’s an example of a possible implementation using one of the methods.
// api.ts

async function handleRequest(url, method, body, onError) {
try {
const { data: isTokenValid } = await useFetch('/check-token', {
method: 'POST',
body: token
})
if (!isTokenValid) {
throw Error('Token is invalid')
}
const { data } = await useFetch(url, {
method,
body
})
return data
} catch (error) {
onError(error)
}
}

12. Assets and Public Folders:

  • Decide which files to store in assets and public folders. You would probably want to store all of the favicons and other essentials in public to serve them as is and keep stylesheets, SVGs in assets so that they can be optimised by the build tools.

13. Component Optimisation:

  • Minimise JavaScript reliance in layout components (e.g., design system components) and use plain CSS to reduce shifts and load times.

14. Tags Replacement:

  • Replace <a> tags with NuxtLink (available out of the box) and <img> tags with NuxtImg (if using the module).

15. Code Cleanup:

  • Remove unnecessary imports as Nuxt auto-imports some libraries (e.g., vue-router, i18n).
// Vue
<script>
import { useRoute } from 'vue-router'

const route = useRoute()
</script>


// Nuxt
<script>
// no import needed
const route = useRoute()
</script>

Final Thoughts

Enjoy the enhanced SEO, faster load time on the client and other features of Nuxt 3! Let me know in the comments if these steps worked for you or if there’s anything I missed.

--

--