30. Storybook. Nuxt + Storybook Setup. Frontend Workshop. Directory Structure in Nuxt

Video version
(Leave your feedback on YouTube)

TL;DR

Storybook is useful for large projects, projects with A/B testing, and projects with multiple themes.

Change the project structure so that all developer code is in a subdirectory. This will help avoid issues with caches, builds, and node_modules.

pathPrefix is a bad practice in Nuxt component naming, as it complicates refactoring.

nuxt.config.ts
export default defineNuxtConfig({
    components: [
        {
            path: '~/components',
            pathPrefix: false,
        },
    ],
})

Storing stories and tests alongside components won’t affect the production build.

Store all mock data in separate .mock files. This makes it easier to reuse and update.

Storybook encourages you to write pure components, whose state only depends on arguments (properties). This is a good practice.

Storybook Setup for Nuxt

command line
npm i --save-dev storybook @nuxtjs/storybook @storybook/addon-essentials @storybook/addon-interactions @storybook/addon-links

Run Storybook separately from the main build, with configuration that keeps it isolated from the primary project.

package.json
{
  "scripts": {
    "storybook": "IS_STORYBOOK=true storybook dev --port 6006",
    "build-storybook": "IS_STORYBOOK=true storybook build"
  }
}

Modify the Nuxt configuration depending on the presence of the IS_STORYBOOK variable:

nuxt.config.ts
export default defineNuxtConfig({
    pages: !process.env.IS_STORYBOOK,
    ogImage: {
        enabled: !process.env.IS_STORYBOOK,
    },
    vue: {
        runtimeCompiler: 'IS_STORYBOOK' in process.env,
    },
})
  • pages - fixes a Storybook navigation bug with the Nuxt route configuration.
  • ogImage - fixes an SSR bug for the ogImage package, where SSR = false by default in @nuxtjs/storybook.
  • vue.runtimeCompiler - allows component rendering on Storybook pages. Disable for the main build if it’s not needed.

Entrypoint for launching:

.storybook/main.ts
import type { StorybookConfig } from '@storybook-vue/nuxt'
import { mergeConfig } from 'vite'

const config: StorybookConfig = {
  stories: [
    '../app/**/*.mdx',
    '../app/components/**/*.stories.ts',
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-themes',
    '@storybook/addon-a11y',
  ],
  framework: {
    name: '@storybook-vue/nuxt',
    options: {},
  },
  docs: {},
  async viteFinal(config) {
    return mergeConfig(config, {
      optimizeDeps: {
        include: ['jsdoc-type-pratt-parser'],
      },
    })
  },
}
export default config
  • stories - directories for locating story files.
  • viteFinal - fixes a documentation rendering bug.

Configuration file:

.storybook/preview.ts
import type { Preview } from '@storybook-vue/nuxt'
import { withThemeByClassName } from '@storybook/addon-themes'
import { h, Suspense } from 'vue'
import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport'
import './stubs/i18n.stub'
import './stubs/nuxt-link.stub'

const preview: Preview = {
  parameters: {
    layout: 'fullscreen',
    viewport: {
      viewports: {
        desktop: {
          name: 'Desktop',
          styles: {
            height: '100%',
            width: '100%',
            overflow: 'clip',
          },
          type: 'desktop',
        },
        ...MINIMAL_VIEWPORTS,
      },
      defaultViewport: 'desktop',
    },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
  decorators: [
    withThemeByClassName({
      themes: {
        light: 'light',
        dark: 'dark',
      },
      defaultTheme: 'dark',
    }),
    (story) => {
      return {
        setup() {
          return () => h(Suspense, {}, [h(story())])
        },
      }
    },
  ],
}

export default preview
  • layout - default layout template for all stories.
  • viewport - settings for available viewports and global configuration.
  • controls - automatically identifies control elements for stories.
  • withThemeByClassName - theme setup.
  • (story) => ... Suspense - enables stories to wait for dynamic components (e.g., Nuxt Icon).

In .storybook/preview.ts, connect all global directives, components, and methods.

Adding i18n for localization:

.storybook/stubs/i18n.stub.ts
import { setup } from '@storybook/vue3'
import { createI18n } from 'vue-i18n'
import en from '../../app/locales/en.json'

setup((app) => {
  const i18n = createI18n({
    locale: 'en',
    messages: { en },
  })
  app.use(i18n)
})

Adjusting the NuxtLink component:

nuxt-link.stub.ts
import { setup } from '@storybook/vue3'
import { action } from '@storybook/addon-actions'
import { h } from 'vue'

setup((app) => {
  app.component('RouterLink', {
    props: ['to'],
    methods: {
      onClick(e) {
        e.preventDefault()
        action('switch-route')(this.to)
      },
    },
    render() {
      return h('a', { onClick: this.onClick, href: this.to }, this.$slots.default())
    },
  })
})

You’ll also need to create stubs for all global methods.