vite-plugin-ssr
Do-One-Thing-Do-It-Well, Flexible, Simple.
Introduction
Vue Tour
React Tour
Get Started
• Boilerplates
• Manual Installation
Guides
• Data Fetching
• Routing
• Auth Data
• Store
• Markdown
• Pre-rendering
• Page Redirection
API
• *.page.js
• *.page.client.js
• import { getPage } from 'vite-plugin-ssr/client'
• *.page.route.js
• Route String
• Route Function
• *.page.server.js
• export { addContextProps }
• export { setPageProps }
• export { prerender }
• export { render }
• import { html } from 'vite-plugin-ssr'
• _default.*
• _404.page.js
• Filesystem Routing
• import { createRender } from 'vite-plugin-ssr'
• import vitePlugin from 'vite-plugin-ssr'
Introduction
vite-plugin-ssr
gives you a similar experience than Nuxt/Next.js, but with Vite's wonderful DX, and as a do-one-thing-do-it-well tool: vite-plugin-ssr
doesn't interfere with your stack and can be used with any tool you want.
- Do-One-Thing-Do-It-Well. Only takes care of SSR and works with: other Vite plugins, any view framework (Vue 3, Vue 2, React, Svelte, Preact, ...), and any server framework (Express, Koa, Hapi, Fastify, ...).
- Render Control. You control how your pages are rendered enabling you to easily and naturally integrate tools such as Vuex and Redux.
- Routing. Supports Filesystem Routing for basic needs, Route Strings for simple parameterized routes, Route Functions for full flexibility, and can be used with Vue Router or React Router for client-side dynamic nested routes.
- Pre-render / SSG / Static Websites. Deploy your app to a static host by pre-rendering your pages.
- Scalable. Thanks to Vite's lazy transpiling, Vite apps can scale to thousands of modules with no hit on dev speed.
- Fast Production Cold Start. Your pages' server-side code is lazy loaded so that adding pages doesn't increase cold start.
- Code Splitting. Each page loads only the browser-side code it needs.
- Simple Design. Simple overall design resulting in a small & robust tool that is easy to use.
To get an idea of what it's like to use vite-plugin-ssr
, checkout the Vue Tour or React Tour.
Vue Tour
Similarly to SSR frameworks, pages are defined by page files.
<!-- /pages/index.page.vue -->
<!-- Environment: Browser, Node.js -->
<template>
This page is rendered to HTML and interactive:
<button @click="state.count++">Counter {{ state.count }}</button>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({ count: 0 })
return { state }
}
}
</script>
By default, vite-plugin-ssr
does filesystem routing:
FILESYSTEM URL
pages/index.page.vue /
pages/about.page.vue /about
You can also use Route Strings (for parameterized routes such as /movies/:id
) and Route Functions (for full programmatic flexibility).
// /pages/index.page.route.js
// Environment: Node.js
export default '/'
Unlike SSR frameworks, you define how your pages are rendered.
// /pages/_default.page.server.js
// Environment: Node.js
import { createSSRApp, h } from 'vue'
import { renderToString } from '@vue/server-renderer'
import { html } from 'vite-plugin-ssr'
export { render }
async function render({ Page, pageProps }) {
const app = createSSRApp({
render: () => h(Page, pageProps)
})
const appHtml = await renderToString(app)
return html`<!DOCTYPE html>
<html>
<head>
<title>Vite w/ SSR Demo</title>
</head>
<body>
<div id="app">${html.dangerouslySetHtml(appHtml)}</div>
</body>
</html>`
}
// /pages/_default.page.client.js
// Environment: Browser
import { createSSRApp, h } from 'vue'
import { getPage } from 'vite-plugin-ssr/client'
hydrate()
async function hydrate() {
// (In production, the page is `<link rel="preload">`'d.)
const { Page, pageProps } = await getPage()
const app = createSSRApp({
render: () => h(Page, pageProps)
})
app.mount('#app')
}
The render()
hook in pages/_default.page.server.js
gives you full control over how your pages are rendered,
and pages/_default.page.client.js
gives you full control over the browser-side code.
This control enables you to easily and naturally:
- Use any tool you want such as Vue Router and Vuex.
- Use any Vue version you want.
Note how the files we created so far end with .page.vue
, .page.route.js
, .page.server.js
, and .page.client.js
.
.page.js
: defines the page's view that is rendered to HTML / the DOM..page.client.js
: defines the page's browser-side code..page.server.js
: defines the page's hooks (always run in Node.js)..page.route.js
: defines the page's Route String or Route function.
Using vite-plugin-ssr
consists simply of writing these four types of files.
Instead of creating a .page.client.js
and .page.server.js
file for each page, you can create _default.page.client.js
and _default.page.server.js
which apply as default for all pages.
We already defined our _default.*
files above,
which means that we can now create a new page simply by defining a new .page.vue
file.
(The .page.route.js
file is optional and only needed if we want to define a parameterized route.)
The _default.*
files can be overridden. For example, you can create a page with a different browser-side code than your other pages.
// /pages/about.page.client.js
// This file is empty which means that the `/about` page has zero browser-side JavaScript.
<!-- /pages/about.page.vue -->
<template>
This page is only rendered to HTML.
</template>
By overriding _default.page.server.js
you can
even render some of your pages with an entire different view framework such as React.
Note how files are collocated and share the same base /pages/about.page.*
;
this is how you tell vite-plugin-ssr
that /pages/about.page.client.js
is the browser-side code of /pages/about.page.vue
.
Let's now have a look at how to fetch data for a page that has a parameterized route.
<!-- /pages/star-wars/movie.page.vue -->
<!-- Environment: Browser, Node.js -->
<template>
<h1>{{movie.title}}</h1>
<p>Release Date: {{movie.release_date}}</p>
<p>Director: {{movie.director}}</p>
</template>
<script lang="ts">
const pageProps = ['movie']
export default { props: pageProps }
</script>
// /pages/star-wars/movie.page.route.js
// Environment: Node.js
export default '/star-wars/:movieId'
// /pages/star-wars/movie.page.server.js
// Environment: Node.js
import fetch from 'node-fetch'
export { addContextProps }
export { setPageProps }
async function addContextProps({ contextProps }) {
// Route parameters are available at `contextProps`
const { movieId } = contextProps
// We could also use SQL/ORM queries here
const response = await fetch(`https://swapi.dev/api/films/${movieId}`)
const movie = await response.json()
return { movie }
}
// The `contextProps` are available only on the server, and only the `pageProps` are
// serialized and passed to the browser.
function setPageProps({ contextProps }) {
// We select only the data we need in order to minimize what it sent over the network
const { title, release_date, director } = contextProps.movie
const movie = { title, release_date, director }
const pageProps = { movie }
return pageProps
}
The addContextProps()
hook always runs in Node.js,
which means SQL/ORM queries can be used to fetch data.
That's it, and we have actually already seen most of vite-plugin-ssr
's interface.
Thanks to the render()
hook
you keep full control over how your pages are rendered,
and thanks to *.page.client.js
,
you keep full control over the entire browser-side code.
This makes it easy and natural to use vite-plugin-ssr
with any tool you want.
In short: vite-plugin-ssr
is not only the most flexible, but also the easiest SSR tool out there.
React Tour
Similarly to SSR frameworks, pages are defined by page files.
// /pages/index.page.jsx
// Environment: Browser, Node.js
import React, { useState } from "react";
export { Page };
function Page() {
return <>
This page is rendered to HTML and interactive: <Counter />
</>;
}
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((count) => count + 1)}>
Counter {count}
</button>
);
}
By default, vite-plugin-ssr
does filesystem routing:
FILESYSTEM URL
pages/index.page.jsx /
pages/about.page.jsx /about
You can also use Route Strings (for parameterized routes such as /movies/:id
) and Route Functions (for full programmatic flexibility).
// /pages/index.page.route.js
// Environment: Node.js
export default "/";
Unlike SSR frameworks, you define how your pages are rendered.
// /pages/_default.page.server.jsx
// Environment: Node.js
import ReactDOMServer from "react-dom/server";
import React from "react";
import { html } from "vite-plugin-ssr";
export { render };
async function render({ Page, pageProps }) {
const viewHtml = ReactDOMServer.renderToString(
<Page {...pageProps} />
);
return html`<!DOCTYPE html>
<html>
<head>
<title>Vite w/ SSR Demo</title>
</head>
<body>
<div id="page-view">${html.dangerouslySetHtml(viewHtml)}</div>
</body>
</html>`;
}
// /pages/_default.page.client.jsx
// Environment: Browser
import ReactDOM from "react-dom";
import React from "react";
import { getPage } from "vite-plugin-ssr/client";
hydrate();
async function hydrate() {
// (In production, the page is `<link rel="preload">`'d.)
const { Page, pageProps } = await getPage();
ReactDOM.hydrate(
<Page {...pageProps} />,
document.getElementById("page-view")
);
}
The render()
hook in pages/_default.page.server.jsx
gives you full control over how your pages are rendered,
and pages/_default.page.client.jsx
gives you full control over the browser-side code.
This control enables you to easily and naturally:
- Use any tool you want such as React Router or Redux.
- Use Preact, Inferno, Solid or any other React-like alternative.
Note how the files we created so far end with .page.jsx
, .page.route.js
, .page.server.jsx
, and .page.client.jsx
.
.page.js
: defines the page's view that is rendered to HTML / the DOM..page.client.js
: defines the page's browser-side code..page.server.js
: defines the page's hooks (always run in Node.js)..page.route.js
: defines the page's Route String or Route function.
Using vite-plugin-ssr
consists simply of writing these four types of files.
Instead of creating a .page.client.js
and .page.server.js
file for each page, you can create _default.page.client.js
and _default.page.server.js
which apply as default for all pages.
We already defined our _default.*
files above,
which means that we can now create a new page simply by defining a new .page.jsx
file.
(The .page.route.js
file is optional and only needed if we want to define a parameterized route.)
The _default.*
files can be overridden. For example, you can create a page with a different browser-side code than your other pages.
// /pages/about.page.client.js
// This file is empty which means that the `/about` page has zero browser-side JavaScript.
// /pages/about.page.jsx
export { Page };
function Page() {
return <>This page is only rendered to HTML.<>;
}
By overriding _default.page.server.js
you can
even render some of your pages with an entire different view framework such as Vue.
Note how files are collocated and share the same base /pages/about.page.*
;
this is how you tell vite-plugin-ssr
that /pages/about.page.client.js
is the browser-side code of /pages/about.page.jsx
.
Let's now have a look at how to fetch data for a page that has a parameterized route.
// /pages/star-wars/movie.page.jsx
// Environment: Browser, Node.js
import React from "react";
export { Page };
function Page(pageProps) {
const { movie } = pageProps;
return <>
<h1>{movie.title}</h1>
<p>Release Date: {movie.release_date}</p>
<p>Director: {movie.director}</p>
</>;
}
// /pages/star-wars/movie.page.route.js
// Environment: Node.js
export default "/star-wars/:movieId";
// /pages/star-wars/movie.page.server.js
// Environment: Node.js
import fetch from "node-fetch";
export { addContextProps };
export { setPageProps };
async function addContextProps({ contextProps }) {
// Route parameters are available at `contextProps`
const { movieId } = contextProps;
// We could also use SQL/ORM queries here
const response = await fetch(`https://swapi.dev/api/films/${movieId}`);
const movie = await response.json();
return { movie };
}
// The `contextProps` are available only on the server, and only the `pageProps` are
// serialized and passed to the browser.
function setPageProps({ contextProps }) {
// We select only the data we need in order to minimize what it sent over the network
const { title, release_date, director } = contextProps.movie;
const movie = { title, release_date, director };
const pageProps = { movie };
return pageProps;
}
The addContextProps()
hook always runs in Node.js,
which means SQL/ORM queries can be used to fetch data.
That's it, and we have actually already seen most of vite-plugin-ssr
's interface.
Thanks to the render()
hook
you keep full control over how your pages are rendered,
and thanks to *.page.client.js
,
you keep full control over the entire browser-side code.
This makes it easy and natural to use vite-plugin-ssr
with any tool you want.
In short: vite-plugin-ssr
is not only the most flexible, but also the easiest SSR tool out there.
Boilerplates
Scaffold a Vite app that uses vite-plugin-ssr
.
With NPM:
npm init vite-plugin-ssr
With Yarn:
yarn create vite-plugin-ssr
Then choose between vue
, vue-ts
, react
, and react-ts
.
Manual Installation
If you already have an existing Vite app:
-
Add
vite-plugin-ssr
to yourvite.config.js
. -
Integrate
createRender()
with your server (Express.js, Koa, Hapi, Fastify, ...). -
Define your
_default.page.client.js
and_default.page.server.js
. -
Create your first
index.page.js
. -
Add the
dev
andbuild
scripts to yourpackage.json
.
Data Fetching
⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.
You fech data by using two hooks: addContextProps()
and setPageProps()
. The async function addContextProps()
fetches data, while the function setPageProps()
(not async
) specifies what data is serialized and passed to the browser.
Hooks are called in Node.js, which means that you can use ORM/SQL database queries in your addcontextprops()
hook.
Hooks are defined in .page.server.js
.
// /pages/movies.page.server.js
// Environment: Node.js
import fetch from "node-fetch";
export { addContextProps }
export { setPageProps }
async function addContextProps({ contextProps }) {
const response = await fetch("https://api.imdb.com/api/movies/")
const { movies } = await response.json()
return { movies }
}
function setPageProps({ contextProps: { movies } }) {
// We only select data we need: `vite-plugin-ssr` serializes and passes `pageProps`
// to the client and we want to minimize what it sent over the network.
movies = movies.map(({ title, release_date }) => ({title, release_date}))
const pageProps = { movies }
return pageProps
}
The pageProps
are:
- Passed to your
render()
hook. - Serialized and passed to the client-side.
// /pages/_default.page.server.js
// Environment: Node.js
import { html } from 'vite-plugin-ssr'
import { renderView } from 'some-view-library'
export { render }
async function render({ Page, pageProps }) {
// `Page` is defined below in `/pages/movies.page.js`.
const pageHtml = await renderView(<Page {...pageProps} />)
return html`<html>
<div id='view-root'>
${html.dangerouslySetHtml(pageHtml)}
</div>
</html>`
}
// /pages/_default.page.client.js
// Environment: Browser
import { getPage } from 'vite-plugin-ssr/client'
import { hydrateView } from 'some-view-library'
hydrate()
async function hydrate() {
const { Page, pageProps } = await getPage()
await hydrateView(<Page {...pageProps} />, document.getElementById('view-root'))
}
// /pages/movies.page.js
// Environment: Browser, Node.js
export { Page }
function Page(pageProps) {
const { movies } = pageProps
/* ... */
}
Routing
⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.
By default vite-plugin-ssr
does Filesystem Routing.
FILESYSTEM URL
pages/index.page.js /
pages/about.page.js /about
pages/faq/index.page.js /faq
For more control, you can define route strings in .page.route.js
files.
// /pages/product.page.route.js
export default '/product/:productId'
The productId
value is available at contextProps.productId
so that you can fetch data in async addContextProps({contextProps})
which is explained at Data Fetching.
For full programmatic flexibility, you can define route functions.
// /pages/admin.page.route.js
// Route functions allow us to implement advanced routing such as route guards.
export default async ({ url, contextProps }) => {
if (url==='/admin' && contextProps.user.isAdmin) {
return { match: true }
}
}
For detailed informations about Filesystem Routing, route strings, and route functions:
Auth Data
⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.
Information about the authenticated user can be added to contextProps
at the server integration point
createRender()
.
The contextProps
are available to all hooks and route functions.
const render = createRender(/*...*/)
app.get('*', async (req, res, next) => {
const url = req.originalUrl
// The `user` object, which holds user information, is provided by your
// authentication middleware, for example the Express.js Passport middleware.
const { user } = req
const contextProps = { user }
const html = await render({ url, contextProps })
if (!html) return next()
res.send(html)
})
Store
⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.
Even complex integrations, such as Vuex or Redux, are simple and straightforward to implement. Because you control how your pages are rendered, integration is just a matter of following the official guide of the tool you want to integrate.
While you can follow the official guides exactly as they are (including serializing and injecting the initial state into HTML),
you can also leverage vite-plugin-ssr
's pageProps
to make your life slightly easier,
as shown in the following examples.
Markdown
⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.
You can use vite-plugin-ssr
with any Vite markdown plugin.
For Vue you can use vite-plugin-md
:
For React you can use @brillout/vite-plugin-mdx
:
Pre-rendering
⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.
⚠️ Pre-rendering is currenlty being worked on. If you want an ETA, open a GitHub issue.
*️⃣ What is pre-rendering? Pre-rendering means to render the HTML of all your pages at once. Normally, the HTML of a page is rendered at request-time (when your user goes to your website). With pre-rendering, the HTML of a page is rendered at build-time instead (when yun run
vite-plugin-ssr prerender
). Your app then consists only of static assets (HTML, JS, CSS, images, ...) and you can deploy your app to so-called "static hosts" such as GitHub Pages or Netlify. Without pre-rendering, you need to use a Node.js server that will render your pages' HTML at request-time.
To pre-render your pages, run npx vite && npx vite --ssr && npx vite-plugin-ssr prerender
. (Or with Yarn: yarn vite && yarn vite --ssr && yarn vite-plugin-ssr prerender
.)
For pages with a parameterized route (e.g. /movie/:movieId
), you'll have to use the prerender()
hook.
The prerender()
hook can also be used to prefetch data for multiple pages at once.
Page Redirection
⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.
Your render()
hook doesn't have to return HTML and can, for example, return { redirectTo: '/some/url' }
in order to do a URL redirect.
export { render }
function render({ contextProps }) {
// If the user goes to `/movie/42` but there is no movie with ID `42` then
// we redirect the user to `/movie/add` so he can add a new movie.
if (contextProps.movieId === null) {
return { redirectTo: '/movie/add' }
} else {
// The usual render stuff
// ...
}
}
const render = createRender(/*...*/)
app.get('*', async (req, res, next) => {
const url = req.originalUrl
const contextProps = {}
const renderResult = await render({ url, contextProps })
if (renderResult?.redirectTo) {
res.redirect(307, '/movie/add')
} else if (typeof renderResult === 'string') {
res.send(renderResult)
} else {
next()
}
})
*.page.js
Environment: Browser
, Node.js
Ext Glob: /**/*.page.*([a-zA-Z0-9])
A *.page.js
file should have a export { Page }
. (Or a export default
.)
Page
represents the page's view that is rendered to HTML / the DOM.
vite-plugin-ssr
doesn't do anything with Page
and just passes it untouched to:
- Your
render({ Page })
hook. - The client-side.
// *.page.js
// Environment: Browser, Node.js
export { Page }
// We export a JSX component, but we could as well export a Vue/Svelte/... component,
// or even export some totally custom object since vite-plugin-ssr doesn't do anything
// with `Page`: it just passes it to your `render()` hook and to the client-side.
function Page() {
return <>Hello</>
}
// *.page.server.js
// Environment: Node.js
import { html } from 'vite-plugin-ssr'
import renderToHtml from 'some-view-library'
export { render }
// `Page` is passed to the `render()` hook
async function render({ Page }) {
const pageHtml = await renderToHtml(Page)
return html`<html>
<body>
<div id="root">
${html.dangerouslySetHtml(pageHtml)}
</div>
</body>
</html>`
}
// *.page.client.js
// Environment: Browser
import { getPage } from 'vite-plugin-ssr/client'
import { hydrateToDom } from 'some-view-library'
hydrate()
async function hydrate() {
// `Page` is available in the browser.
const { Page } = await getPage()
await hydrateToDom(Page)
}
The *.page.js
file is lazy-loaded only when needed, that is when an HTTP request matches the page's route.
*.page.client.js
Environment: Browser
Ext Glob: /**/*.page.client.*([a-zA-Z0-9])
A .page.client.js
file is a .page.js
-adjacent file that defines the page's browser-side code.
It represents the entire browser-side code. This means that if you create an empty .page.client.js
file, then the page has zero browser-side JavaScript.
(Except of Vite's dev code when not in production.)
This also means that you have full control over the browser-side code: not only can you render/hydrate your pages as you wish, but you can also easily integrate browser libraries.
// *.page.client.js
import { getPage } from 'vite-plugin-ssr/client'
import { hydrateView } from 'some-view-library'
import GoogleAnalytics from '@brillout/google-analytics'
main()
async function main() {
analytics_init()
analytics.event('[hydration] begin')
await hydrate()
analytics.event('[hydration] end')
}
async function hydrate() {
const { Page, pageProps } = await getPage()
await hydrateView(<Page {...pageProps} />, document.getElementById('view-root'))
}
let analytics
function analytics_init() {
analytics = new GoogleAnalytics('UA-121991291')
}
import { getPage } from 'vite-plugin-ssr/client'
Environment: Browser
The async getPage()
function provides Page
and pageProps
for the browser-side code .page.client.js
.
// /pages/demo.page.client.js
import { getPage } from 'vite-plugin-ssr/client'
hydrate()
async function hydrate() {
const { Page, pageProps } = await getPage()
/* ... */
}
Page
is theexport { Page }
(orexport default
) of the/pages/demo.page.js
file.pageProps
is the value returned by yoursetPageProps()
function (which you define and export in the adjacentpages/demo.page.server.js
file).
The pageProps
are serialized and passed from the server to the browser with devalue
.
In development getPage()
dynamically import()
the page, while in production the page is preloaded (with <link rel="preload">
).
*.page.route.js
Environment: Node.js
Ext Glob: /**/*.page.route.*([a-zA-Z0-9])
The *.page.route.js
files enable further control over routing with:
- Route Strings
- Route Functions
Route String
For a page /pages/film.page.js
, a route string can be defined in a /pages/film.page.route.js
adjacent file.
// /pages/film.page.route.js
// Match URLs `/film/1`, `/film/2`, ...
export default '/film/:filmId'
If the URL matches, the value of filmId
is available at contextProps.filmId
.
The syntax of route strings is based on path-to-regexp
(the most widespread route syntax in JavaScript).
For user friendlier docs, check out the Express.js Routing Docs
(Express.js uses path-to-regexp
).
Route Function
Route functions give you full programmatic flexibility to define your routing logic.
// /pages/film/admin.page.route.js
export default async ({ url, contextProps }) {
// Route functions allow us to implement advanced routing such as route guards.
if (! contextProps.user.isAdmin) {
return {match: false}
}
// We can use RegExp and any JavaScript tool we want.
if (! /\/film\/[0-9]+\/admin/.test(url)) {
return {match: false}
}
filmId = url.split('/')[2]
return {
match: true,
// Add `filmId` to `contextProps`
contextProps: { filmId }
}
}
The match
value can be a (negative) number which enables you to resolve route conflicts.
The higher the number, the higher the priority.
For example, vite-plugin-ssr
internally defines _404.page.js
's route as:
// node_modules/vite-plugin-ssr/.../_404.page.route.js
// Ensure lowest priority for the 404 page
export default () => ({match: -Infinity})
*.page.server.js
Environment: Node.js
Ext Glob: /**/*.page.server.*([a-zA-Z0-9])
A .page.server.js
file is a .page.js
-adjacent file that exports the page's hooks:
export { addContextProps }
export { setPageProps }
export { render }
export { prerender }
The *.page.server.js
file is lazy-loaded only when needed.
export { addContextProps }
The addContextProps()
hook is used to provide further contextProps
values.
The contextProps
are passed to all hooks (which are defined in .page.server.js
) and to the route function (if there is one defined in .page.route.js
).
You can provide initial contextProps
values at your server integration point: const render = createRender(/*...*/); render({ url, contextProps })
.
Which you usually use to pass information about the authenticated user,
see Auth Data guide.
The addContextProps()
hook is usually used in conjunction with the setPageProps()
hook to fetch data, see Data Fetching guide.
Since addContextProps()
is always called in Node.js, ORM/SQL database queries can be used.
// /pages/movies.page.server.js
import fetch from "node-fetch";
export { addContextProps }
async function addContextProps({ contextProps, Page }){
const response = await fetch("https://api.imdb.com/api/movies/")
const { movies } = await response.json()
/* Or with an ORM:
const movies = Movie.findAll() */
/* Or with SQL:
const movies = sql`SELECT * FROM movies;` */
return { movies }
}
Page
is theexport { Page }
(orexport default
) of the.page.js
file.contextProps
is the initial accumulation of:- The
contextProps
you provided in your the server integration pointcreateRender()
. - The route parameters (such as
contextProps.movieId
for a page with a route string/movie/:movieId
).
- The
export { setPageProps }
The setPageProps()
hook provides the pageProps
which are consumed by Page
.
The pageProps
are serialized and passed from the server to the browser with devalue
.
It is usally used in conjunction with the addContextProps()
hook: data is fetched in addContextProps()
and then made available to Page
with setPageProps()
.
// /pages/movies.page.server.js
// Environment: Node.js
import fetch from "node-fetch";
async function addContextProps({ contextProps }) {
const response = await fetch("https://api.imdb.com/api/movies/")
const { movies } = await response.json()
return { movies }
}
function setPageProps({ contextProps: { movies } }) {
// We remove data we don't need: `vite-plugin-ssr` serializes and passes `pageProps`
// to the client and we want to minimize what it sent over the network.
movies = movies.map(({ title, release_date }) => ({title, release_date}))
const pageProps = { movies }
return pageProps
}
// /pages/movies.page.js
// Environment: Browser, Node.js
export { Page }
function Page(pageProps) {
const { movies } = pageProps
/* ... */
}
export { prerender }
*️⃣ Check out the Pre-rendering Guide to get an overview about pre-rendering.
The prerender()
hook enables parameterized routes (e.g. /movie/:movieId
) to be pre-rendered:
by defining the prerender()
hook you provide the list of URLs (/movie/1
, /movie/2
, ...) and (optionally) the contextProps
of each URL.
If you don't have any parameterized route,
then you can prerender your app without defining any prerender()
hook.
You can, however, still use the prerender()
hook
to increase the effeciency of pre-rendering as
it enables you to fetch data for multiple pages at once.
// /pages/movie.page.route.js
export default '/movie/:movieId`
// /pages/movie.page.server.js
export { prerender }
async function prerender() {
const movies = await Movie.findAll()
const moviePages = (
movies
.map(movie => {
const url = `/movie/${movie.id}`
const contextProps = { movie }
return {
url,
// Beacuse we already provide the `contextProps`, vite-plugin-ssr will *not* call
// the `addContextProps()` hook.
contextProps
}
// We could also return `url` wtihout `contextProps`. In that case vite-plugin-ssr would
// call `addContextProps()`. But that would be wasteful since we already have all the data
// of all movies from our `await Movie.findAll()` call.
// return { url }
})
)
// We can also return URLs that don't match the page's route.
// That way we can provide the `contextProps` of other pages.
// Here we provide the `contextProps` of the `/movies` page since
// we already have the data.
const movieListPage = {
url: '/movies', // The `/movies` URL doesn't belong to the page's route `/movie/:movieId`
contextProps: {
movieList: movies.map(({id, title}) => ({id, title})
}
}
return [movieListPage, ...moviePages]
}
The prerender()
hook is only used when pre-rendering:
if you don't call
vite-plugin-ssr prerender
then no prerender()
hook is called.
export { render }
The render()
hook renders Page
to an HTML string.
Note that the render()
hook can also return something else than HTML,
for example an object { redirectTo: '/some/url' }
in order to do Page Redirection.
// *.page.server.js
import { html } from 'vite-plugin-ssr'
import renderToHtml from 'some-view-library'
export { render }
async function render({ Page, pageProps, contextProps }){
const pageHtml = await renderToHtml(<Page {...pageProps} />)
const title = contextProps.title || 'My SSR App'
return html`<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
</head>
<body>
<div id="page-root">${html.dangerouslySetHtml(pageHtml)}</div>
</body>
</html>`
}
Page
is theexport { Page }
(orexport default
) of the.page.js
file.pageProps
is the value returned by thesetPageProps()
hook.contextProps
is the accumulation of:- The
contextProps
you passed toconst render = createRender(/*...*/); render({ url, contextProps })
. - The route parameters (such as
contextProps.movieId
for a page with a route string/movie/:movieId
). - The
contextProps
you returned in youraddContextProps()
hook (if you defined one).
- The
import { html } from 'vite-plugin-ssr'
Environment: Node.js
The html
tag sanitizes HTML (to prevent XSS injections).
It is usually used in your render()
hook defined in .page.server.js
.
// *.page.server.js
import { html } from 'vite-plugin-ssr'
export { render }
async function render() {
const title = 'Hello<script src="https://devil.org/evil-code"></script>'
const pageHtml = "<div>I'm already <b>sanitized</b>, e.g. by Vue/React</div>"
// We're safe because `html` sanitizes `title`
return html`<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
</head>
<body>
<div id="page-root">${html.dangerouslySetHtml(pageHtml)}</div>
</body>
</html>`
}
All strings, e.g. title
, are automatically sanitized (technically speaking: HTML-escaped)
so that you can safely inject untrusted strings
such as user-generated text.
The html.dangerouslySetHtml(str)
function injects the string str
as-is without sanitizing.
It should be used with caution and
only for HTML strings that are guaranteed to be already sanitized.
It is usually used to include the HTML generated by React/Vue/Solid/... as these frameworks generate sanitized HTML.
If you find yourself using html.dangerouslySetHtml()
in other situations be extra careful as you run into the risk of creating a security breach.
_default.*
The _default.page.server.js
and _default.page.client.js
files are like regular .page.server.js
and .page.client.js
files, but they are special in the sense that they don't apply to a single page file; instead, they apply as a default to all pages.
There can be several _default.*
files.
marketing/_default.page.server.js
marketing/_default.page.client.js
marketing/index.page.js
marketing/about.page.js
marketing/jobs.page.js
admin-panel/_default.page.server.js
admin-panel/_default.page.client.js
admin-panel/index.page.js
The marketing/_default.*
files apply to the marketing/*.page.js
files, while
the admin-panel/_default.*
files apply to the admin-panel/*.page.js
files.
The _default.page.server.js
and _default.page.client.js
files are not adjacent to any .page.js
file, and
defining _default.page.js
or _default.page.route.js
is forbidden.
_404.page.js
The _404.page.js
page is like any other page with the exception that it has a predefined route.
// node_modules/vite-plugin-ssr/.../_404.page.route.js
// Ensure lowest priority for the 404 page
export default () => ({match: -Infinity})
Filesystem Routing
By default a page is mapped to a URL based on where its .page.js
file is located.
FILESYSTEM URL COMMENT
pages/about.page.js /about
pages/index/index.page.js / (`index` is mapped to the empty string)
pages/HELLO.page.js /hello (Mapping is done lower case)
The pages/
directory is optional and you can save your .page.js
files wherever you want.
FILESYSTEM URL
user/list.page.js /user/list
user/create.page.js /user/create
todo/list.page.js /todo/list
todo/create.page.js /todo/create
The directory common to all your *.page.js
files is considered the routing root.
For more control over routing, define route strings or route functions in *.page.route.js
.
import { createRender } from 'vite-plugin-ssr'
Environment: Node.js
The createRender()
is the integration point between your server and vite-plugin-ssr
.
const render = createRender({ viteDevServer, isProduction, root })
app.get('*', async (req, res, next) => {
const url = req.originalUrl
const contextProps = {}
const html = await render({ url, contextProps })
if (!html) return next()
res.send(html)
})
isProduction
is a boolean. When set totrue
,vite-plugin-ssr
loads already-transpiled code fromdist/
instead of on-the-fly transpiling code.root
is a string holding the absolute path of your app's root directory. All your.page.js
files should be a descendent of the root directory.viteDevServer
is the value returned byconst viteDevServer = await vite.createServer(/*...*/)
.
Since render({ url, contextProps})
is agnostic to Express.js, you can use vite-plugin-ssr
with any server framework such as Koa, Hapi, Fastify, or vanilla Node.js.
Examples:
import vitePlugin from 'vite-plugin-ssr'
Environment: Node.js
The Vite plugin has no options.
// vite.config.js
const ssr = require("vite-plugin-ssr");
module.exports = {
plugins: [ssr()]
};