vite-plugin-ssr has been renamed Vike, see migration guide.

Vue Tour


Similarly to Nuxt, we define a new page by creating a new .page.vue file.

<!-- /pages/index.page.vue -->
<!-- Environment: browser and server -->

  This page is rendered to HTML and interactive: <Counter />

<script setup>
import Counter from '../components/Counter.vue'

By default, vite-plugin-ssr does Filesystem Routing.

FILESYSTEM                  URL
/pages/index.page.vue        /
/pages/about.page.vue        /about

We can also define a page's route with a Route String (for parameterized routes such as /movies/@id) or a Route Function (for full programmatic flexibility).

// /pages/index.page.route.js

// Note how the two files share the same base `/pages/index.page.`; this is how `vite-plugin-ssr`
// knows that `/pages/index.page.route.js` defines the route of `/pages/index.page.vue`.

// Route Function
export default pageContext => pageContext.urlPathname === '/'

// If we don't create a `.page.route.js` file then vite-plugin-ssr does Filesystem Routing

Render Control

Unlike Nuxt, we control how our pages are rendered.

// /renderer/_default.page.server.js
// Environment: server

import { createSSRApp, h } from 'vue'
import { renderToString } from '@vue/server-renderer'
import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr/server'

export { render }

async function render(pageContext) {
  const { Page, pageProps } = pageContext
  const app = createSSRApp({
    render: () => h(Page, pageProps)

  const appHtml = await renderToString(app)

  const title = 'Vite SSR'

  return escapeInject`<!DOCTYPE html>
        <div id="app">${dangerouslySkipEscape(appHtml)}</div>
// /renderer/_default.page.client.js
// Environment: browser

import { createSSRApp, h } from 'vue'

export { render }

async function render(pageContext) {
  const { Page, pageProps } = pageContext
  const app = createSSRApp({
    render: () => h(Page, pageProps)

You may see import { createSSRApp, h } from 'vue' for the first time, but every SSR app actually uses createSSRApp and h.

Nuxt abstracts this away from us and gives us a slightly faster getting started. But this also means that we lose control over a central piece of our app architecture and we'll eventually lose more time circumventing Nuxt's limiting black box.

This control enables us to easily and naturally integrate any tool we want (Vuex, GraphQL, Service Worker, ...).

There are four page file suffixes:

  • .page.js: runs in the browser as well as in Node.js
  • .page.client.js: runs only in the browser
  • .page.server.js: runs only in Node.js
  • .page.route.js: defines the page's Route String or Route Function.

Instead of creating a .page.client.js and .page.server.js file for each page, we can create /renderer/_default.page.client.js and /renderer/_default.page.server.js which apply as default for all pages.

The last two files we created are actually /renderer/_default.page.client.js and /renderer/_default.page.server.js, which means that we can now create a new page just by defining a new .page.vue file (the .page.route.js file is optional).

The _default.page. files can be overridden. For example, we can override our render() hooks for rendering some of our pages with a completely different UI framework such as React.

Data Fetching

Let's now have a look at how to fetch data.

<!-- /pages/star-wars/movie.page.vue -->
<!-- Environment: browser and server -->

  <p>Release Date: {{movie.release_date}}</p>
  <p>Director: {{movie.director}}</p>

<script lang="js">
const pageProps = ['movie']
export default { props: pageProps }
// /pages/star-wars/movie.page.route.js
// Environment: server

// Route String
export default '/star-wars/@movieId'
// /pages/star-wars/movie.page.server.js
// Environment: server

import fetch from 'node-fetch'

export async function onBeforeRender(pageContext) {
  // The route parameter of `/star-wars/@movieId` is available at `pageContext.routeParams`
  const { movieId } = pageContext.routeParams

  // `.page.server.js` files always run in Node.js; we could use SQL/ORM queries here.
  const response = await fetch(`https://swapi.dev/api/films/${movieId}`)
  let movie = await response.json()

  // Our render and hydrate functions we defined earlier pass `pageContext.pageProps` to
  // the root Vue component `Page`; this is where we define `pageProps`.
  const pageProps = { movie }

  // We make `pageProps` available as `pageContext.pageProps`
  return {
    pageContext: {

// By default `pageContext.*` are available only on the server. But our hydrate function
// we defined earlier runs in the browser and needs `pageContext.pageProps`; we use
// `passToClient` to tell `vite-plugin-ssr` to serialize and make `pageContext.pageProps`
// available to the browser.
export const passToClient = ['pageProps']

That's it for the tour and we have actually already seen most of the interface; not only is vite-plugin-ssr flexible but it's also simple to use!

$ npm init vite-plugin-ssr
Run $ npm init vite-plugin-ssr to scaffold a new Vite/vite-plugin-ssr app, or add vite-plugin-ssr to your existing app by following the instructions here.