Skip to content

yooouuri/vite-plugin-vue-ssr

Repository files navigation

vite-plugin-vue-ssr

Vite plugin to develop Vue SSR apps

Features

  • Vue Router
  • State management
  • Teleports
  • Unhead support
  • Based on H3

Quick Setup

Installation

pnpm install vite-plugin-vue-ssr -D

Install devalue if you need to hydrate the state

vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueSsr from 'vite-plugin-vue-ssr/plugin'
import { fileURLToPath } from 'node:url'

export default defineConfig({
  plugins: [
    vue(),
    vueSsr(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

Usage

Add the build commands to your package.json file.

{
  "scripts": {
    "dev": "vite",
    "build": "pnpm run build:client && pnpm run build:server",
    "build:client": "vite build --ssrManifest --outDir dist/client",
    "build:server": "vite build --ssr src/main.ts --outDir dist/server"
  }
}

This will build a client and server bundle.

Use the vite command to start a SSR enabled dev server.

Disabling SSR in vite will enable to build a SPA version.

The main.ts file should export the imported vueSSR function.

import { vueSSR } from 'vite-plugin-vue-ssr'

import App from '@/App.vue'

const Counter = () => import('@/Counter.vue')

const routes = [
  {
    path: '/',
    name: 'counter',
    component: Counter,
  }
]

export default vueSSR(App, { routes })

State management

Pinia/Vuex is supported by using the app and state property inside the callback.

export default vueSSR(App, { routes }, ({ app, state }) => {
  const pinia = createPinia()

  app.use(pinia)

  if (import.meta.env.SSR) {
    state.value = pinia.state.value
  } else {
    pinia.state.value = state.value
  }
})

The state will be persisted on window.__INITIAL_STATE__ property and serialized using devalue

Router

It's possible to make changes to the router, use the router property in the callback.

const routes = [
  {
    path: '/',
    name: 'counter',
    component: Counter,
  },
]

export default vueSSR(App, { routes }, ({ router }) => {
  router.beforeEach(async (to, from) => {
    if (
      !isAuthenticated &&
      to.name !== 'Login'
    ) {
      return { name: 'Login' }
    }
  })
})

To customize the router, just return the router instance.

The routes parameter is omitted, because we create a fresh router instance in the method.

export default vueSSR(App, {}, async ({ app }) => {
  const router = createRouter({
    history: import.meta.env.SSR ? createMemoryHistory('/') : createWebHistory('/'),
    routes: [
      {
        path: '/',
        name: 'counter',
        component: Counter,
      },
    ],
  })

  return {
    router,
  }
})

H3

H3 is the underlaying server. During development it injects as an middleware.

The event param is used to access the H3 composables.

NOTE: only works in SSR

import { getRequestURL } from 'h3'

export default vueSSR(App, { routes }, ({ event }) => {
  if (import.meta.env.SSR) {
    console.log(getRequestURL(event)) // "https://example.com/path"
  }

  console.log(event) // undefined
})

In a Vue component, use the useH3Event() composable

import { useH3Event } from 'vite-plugin-vue-ssr'
import { getRequestURL } from 'h3'

if (import.meta.env.SSR) {
  const event = useH3Event()

  console.log(getRequestURL(event)) // "https://example.com/path"
}

See https://h3.unjs.io/utils for more composables.

Teleports

Using Teleport is supported, but requires a little bit of setup. Targeting body is not supported (in SSR), use #teleports instead.

<template>
  <Teleport to="#teleports">
    <button @click="store.increment">Increment</button>
  </Teleport>
</template>

During SSR, the Teleport component will be rendered as a div with the id set to the to property.

License

MIT