docs
  1. SCAYLE Resource Center
  2. Developer Guide
  3. Migrations
  4. Nuxt 3
  5. Storefront Boilerplate changes

Storefront Boilerplate changes

SVG Handling

With vite you can include svg icons by simply prefixing it with the <Icon\* /> which is configured in the svgo config (default is <Svgo\* />. Otherwise you need to import the svg explicitly and use it as a component.

One issue you might stumble on is using this module you can't size your icons as you might wish to with tailwind classes. By default there is a prop fontControlled which you have to disable like so: <Icon* :fontControlled="false"/>. If the use-case is to have the flexibility of applying classes throughout the app then you need to make a change in the nuxt.config

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  svgo: {
    autoImportPath: './assets/icons',
    defaultImport: 'component', // this enables you to style things as you want: https://github.com/cpsoinos/nuxt-svgo/issues/122#issuecomment-1595264212
    componentPrefix: 'Icon', // Default is 'Svgo'
  },
})

Cypress

There are a couple of points that are important to highlight when setting up Cypress.

  • Now we use BASE_URL which comes from .env, and we also validate the url when setting up the Cypress config. Therefore, if you have not installed it beforehand, starting Cypress will not work.
  • Cypress config is now written in TypeScript
  • It is important to note that, since we are using vite, we need to set the following in the Cypress config:
module.exports = defineConfig({
  component: {
    devServer: {
      framework: 'vue',
      bundler: 'Vite',
    },
  },
})

The options above make sure that the bundler is vite and the framework that we're using is vue

Vuelidate

The approach that we're using in Nuxt 3 is almost the same as it was in Nuxt 2. When it comes to the rule message localization there is one thing to keep in mind. In Nuxt 2, when defining the validation plugin, we accessed the global i18n instance within the context argument. Now we need to access it via useNuxtApp composable.

export default defineNuxtPlugin(() => {
  const { $i18n } = useNuxtApp()
  // ...
})

Furthermore, we don't need to manually override the types as we did in Nuxt 2 with Context interface. Rule types will work now automatically.

Toast

Toast plugin with its UI components remains almost the same. The only thing that changed is that we need to use refreshNuxtData instead of $nuxt.refresh() within the reload toast action:

  onClick: () => Promise.resolve(refreshNuxtData()),

Helpers/Utils

In Nuxt 2 we had helpers folder which exported some of the helper functions. We also attached those helpers within the useContext so that we can access it through the components, composables etc. Nuxt 3 recommends using utils folder. Now we don't need to manually expose the helpers or import them explicitly because everything that exists under that folder will be auto-imported.

// utils/route.ts

type Link = 'home'

export type LinkList = Record<Link, { name: string; path: string }>

export const routeList: LinkList = {
  home: { name: 'index', path: '/' },
} as const

// In component usage
 <DefaultLink :to="{ name: routeList.home.name }" />

Constants/types

One of the major change regarding the re-usable components are the usage of constants and types. Now we introduced constants that are located under the constants folder which are representing TypeScript companion pattern approach (Exporting the same type and variable which TS smartly resolves it depends on the usage). This way we have everything encapsulated at one place and the advantage is flexibility and scalability (e.g If we want to add one more Button type, we'll just add it on one place).

Example:

// constants/ui.ts

export const Size = {
  XS: 'xs',
  SM: 'sm',
  MD: 'md',
  LG: 'lg',
  XL: 'xl',
} as const

export type Size = ValuesType<typeof Size>

// Usage example:

import { Size } from '~/constants'

const props = defineProps({
  size: {
    type: String as PropType<Size>, // TS use it here as type
    default: Size.MD, // Regular object prop usage
    validator: (val: Size) => Object.values(Size).includes(val),
  },
})

UI components

Radio Input

In Nuxt 2 we had only one Radio component and we repeated it for each radio. Now we introduced RadioGroup component, which has RadioItems in it. Now we just need to use radio group which will be v-modeled and pass items that are typed like this:

export type Item = { label: string; value: string }
  • Usage:
 <RadioGroup v-model="gender" :items="genders" title="Gender" />

Previously for our slide show / carousel components we used vue-slick-carousel. We are now moving towards using Swiper. Swiper is available as a nuxt module built on top of swiper.js. There is no need to create a custom plugin since the nuxt module is sufficient for our usage.

To migrate the following steps are needed:

Module Installation

yarn add nuxt-swiper

Module Configuration

Once installed you need to add this module to nuxt.config.ts and provide some configurations

import swiper from './config'
// nuxt.config.ts
modules: [
  ...,
    'nuxt-swiper',
  ],
...
swiper // configuration file below

Module Options

We are using smaller configuration files to provide module options, but this can also be done within the nuxt.config.ts file is so preferred. In this example I am taking the dedicated file into consideration.

// config/swiper.ts
export default {
  prefix: 'Swiper',
  modules: ['navigation', 'autoplay', 'pagination'],
}

The Prefix option can be used to provide a custom prefix and will change the module names from Swiper[ModuleName] to MyPrefix[ModuleName] for example: SwiperNavigation would change to MyPrefixNavigation in the component usage.

The modules option can be used to configure what extra functionalities you want with your swiper instance. A full list can be found here.

Usage

Once swiper has been correctly configured the components <Swiper> & <SwiperSlide> will be auto-imported and available for usage. Your custom slide needs to be wrapped with the <SwiperSlide> component

<template>
  <Swiper :modules="[...]" loop autoplay navigation>
    <SwiperSlide v-for="let slide in slides">
      <!-- Slide content here -->
    </SwiperSlide>
  </SwiperSlide>
</template>

Lazy loading

An Important note here is the Lazy loading module is no longer supported. Instead you can provide <swiper-slide lazy=true> and <img loading="lazy" /> to lazy load images.

<template>
  <Swiper>
    <SwiperSlide v-for="let slide in slides" lazy>
      <img src="source" loading="lazy" />
    </SwiperSlide>
  </SwiperSlide>
</template>

radash

We are now moving to the nuxt-lodash that's recommended by the nuxt community. It supports auto imports and it's easy to configure via nuxt config. We stick with the use prefix as it is the default setting.

// plugins/validation.ts
// Usage:
...
    messagePath: ({ $validator }) => `validation.${useSnakeCase($validator)}`,

Custom breakpoints solution

In Nuxt 3 we will use nuxt-viewport which we use for the viewport/breakpoints handling. We added the ./config/breakpoints.ts file where we have all the breakpoints defined. We use that for the tailwind and for the nuxt-viewport config so that we have those two in sync. Current usage of the breakpoint handling is different which will be seen in the example bellow:

<template>
  <div v-if="viewport.isLessThan('sm')">Content</div>
</template>

<script setup lang="ts">
import { useVuelidate } from '@vuelidate/core'

const viewport = useViewport()
// Other usage:
// const { $viewport } = useNuxtApp()
</script>

WITH parameters

Storefront config now supports withParams option so that we can pass the with parameters through the shop. This allows to set them as default parameters within certain composables (e.g useWishlist). This way we don't need an additional composable wrapper that we used in Nuxt 2 just to pass the with parameters.

export default defineNuxtConfig({
  storefront: {
    withParams,
  },
})

CMS with Storyblok

Module configuration

The module configuration for @storyblok/nuxt are identical to nuxt 2, you need to add it to the modules array and provide your storyblok access token in the module options.

import storyblok from './config'

  // nuxt.config.ts
  modules: [
    '@storyblok/nuxt',
  ],
  ...
  storyblok // configuration file below

  //config/storyblok.ts
  export default {
    accessToken: environment.STORYBLOK_ACCESS_TOKEN,
  },

Auto-imported components

Storyblok components are auto imported. You need to create a storyblok directory at the root and the components will be made available. Be mindful of component name collisions. If your component in the ~/components director is named same as the one inside ~/storyblok there can be issues with the storyblok auto-imported components.

The StoryblokComponent is also auto-imported and can be used out of the box.

useAsyncStoryblok composable

With this module the useAsyncStoryblok composable is also auto-imported and is enough to fetch content from storyblok. You do not need a plugin or custom composables for the basic implementation. If you'd need a plugin the guide can be found here.

With this composable you can provide bridge & ApiOptions in one place

const story = await useAsyncStoryblok(
  'vue',
  { version: 'draft', resolve_relations: 'Article.author' }, // API Options
  { resolveRelations: ['Article.author'], resolveLinks: 'url' }, // Bridge Options
)

Minor Note For now the useCms composable is not needed but this might change as the migration is approaches completion.

Route localization

In Nuxt 3 we introduced toLocalePath route utility which is a useLocalePath wrapper with some additional stuff. The main difference being that we centralized the localization through the DefaultLink component and localized every route helper. Through this change we don't need to repeat the localization process for each component or router action.

Unfortunately, this solution is not ideal because developer still has the responsibility to pay attention when managing the routing. Now we need to take care to always use DefaultLink or route utils. If there's some custom route that we need to use it, we'll need to manually use toLocalePath in order to have it working. Furthermore, we also introduced raw property on DefaultLink component which is basically a replacement for the whole RawLink component that was used in Nuxt 2.

// Raw link (without any styles) usage
<DefaultLink :to="{ name: 'home' }" raw>Home</DefaultLink>

// Custom link localization usage
const router = useRouter()
const customRoute = '/some-custom-route'
await router.push(toLocalePath(customRoute))

// Router action with route utility usage
await router.push(getSearchRoute(searchQuery.value))

HTTPS vs HTTP development mode

In Nuxt 2 we used the https certificates and we always used https mode for the yarn dev. As part of Nuxt 3 we introduced two scripts so that we can run the app in http or https mode:

yarn dev and yarn dev:https

In order to have the https mode work properly we need to set env variables for key and cer file paths which will need to be generated the same as we did in Nuxt 2.

HTTPS_KEY=
HTTPS_CERT=

Intersection observer

The same as in Nuxt 2 we introduced the Intersect component which handles and implements observer intersection. In the past we used our custom implementation with the native IntersectionObserver API. Now we use useIntersectionObserver composable that comes from vueuse and by doing that we simplified the solution a bit. One of the things that's worth mentioning is that now we expose stop function through the slot and event which can stop the intersection.

<Intersect :threshold="0.5" @enter="onIntersect">
  // ...
</Intersect>

<script setup lang="ts">
const onIntersect = (_: IntersectionObserverEntry, stop: () => void) => {
  if (!props.blok.promotion_id) {
    return
  }
  stop()
}
</script>

Error handling

In the Nuxt3 Demo shop we have two ways to display error pages. A global error page and displaying errors as pages.

Global error page

The global error page is defined in ~/error.vue. It is shown whenever an error happens within ~/layout/default.vue or you call the Nuxt 3 build in showError. To hide the error page again, you just need to call build in clearError function with some redirect.

Displaying errors inline

Displaying errors inline is preferable, as it allows the user to continue shopping with less friction than the global error page. To display errors inline, you just call createError and throw the returned NuxtError (e.g. throw createError(new Error('test'))). This works in every child of the default layout.

To clear the error you also need to call clearError and additionally set the error ref within ~/layout/default.vue to undefined.

Storyblok for n stories

For Nuxt 3, we have used the useAsyncStoryblok composable; there is a limitation to this composable that it isn't able to fetch multiple stories. A typical use-case would be fetching lookbooks. There is a reported issue on storyblok nuxt repository that also explains a workaround by using the useStoryblokApi composable.

// https://github.com/storyblok/storyblok-nuxt/issues/547#issuecomment-1697844103

const storyblokApi = useStoryblokApi()
const {
  data: { stories },
} = await storyblokApi.getStories({
  starts_with: folder, // matches stories eg by passing {starts_with: 'lookbooks'} you can fetch lookbooks-1, lookbooks-2, ... lookbooks-n
})

Tracking

The Nuxt 3 tracking implementation is almost identical to the Nuxt 2-based DemoShop. There are a few differences that are worth mentioning.

  • We're now using @zadigetvoltaire/nuxt-gtm, which acts as a @gtm-support/vue-gtm wrapper. The usage of this module is similar to @nuxtjs/gtm which was used for Nuxt 2.
  • Tracking composables are a bit simplified. We don't pass the shopConfig, localePath, etc. anymore, as we did for the Nuxt 2-based DemoShop for some of the composables as those have now been auto-imported.

Store

In Nuxt 2 the store was implemented out of the box. In Nuxt 3 that's not the case. There are several options on how to achieve state management (Pinia, xstate, etc). Since our needs are pretty basic and simple, we'll just use the basic useStore composable that uses useState under the hood.

const store = useStore()
// Will store "category" as pageType
store.value.pageType = 'category'

Plugins

In Nuxt 3 plugins are auto-registered. The only thing that we need to take care of is the order of plugins registration. The perfect example is that we have tracking.client plugin and routeChangeTrackingObserver that use the tracking plugin. We need to register tracking.client first because the other one depends on it. We achieve this by adding the numeration prefix within the file name:

  • plugins/
    • plugins/01.tracking.client.ts
    • plugins/02.routeChangeTrackingObserver.ts

Additions

Packages