Vue Tour

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

<!-- /pages/ -->
<!-- Environment: Browser, Node.js -->

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

import Counter from '../components/Counter.vue'
export default { components: { Counter } }

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

FILESYSTEM                  URL
pages/        /
pages/        /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/
// Environment: Node.js (and Browser if we choose Client Routing)

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

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

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

Unlike Nuxt, we define how our pages are rendered.

// Environment: Node.js

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

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>
// Environment: Browser

import { createSSRApp, h } from 'vue'
import { getPage } from 'vite-plugin-ssr/client'


async function hydrate() {
  const pageContext = await getPage() // (`pageContext` is preloaded in production)
  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 much more time circumventing Nuxt's limiting black box as we scale.

The boilerplate $ npm init vite-plugin-ssr@latest provides a functioning setup that will require only small adaptations for most users.

The render() hook in gives us full control over how our pages are rendered to HTML, and gives us full control over the browser-side code. This control enables us to easily and naturally integrate any tool we want (Vuex, GraphQL, Service Worker, ...).

There are four suffixes:

  • .page.js: exports the page's root Vue component.
  • .page.client.js: defines the page's browser-side code.
  • .page.server.js: exports page hooks (always run in Node.js).
  • .page.route.js: exports 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 and which apply as default for all pages.

The last two files we created are actually and, 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 files can be overridden. For example, we can create a page with a different browser-side code than our other pages.

// /pages/

// This file is empty which means that the `/about` page has zero browser-side JavaScript.
<!-- /pages/ -->

  This page is only rendered to HTML.

By also overriding the render() hook we can even render a page with a completely different UI framework, e.g. React or another Vue version (for progressive Vue version upgrade).

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

<!-- /pages/star-wars/ -->
<!-- Environment: Browser, Node.js -->

  <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/
// Environment: Node.js

// Route String
export default '/star-wars/:movieId'
// /pages/star-wars/
// Environment: Node.js

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(`${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; vite-plugin-ssr is not only flexible but 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.