⚠️ The
vite-plugin-ssr
project has been renamed
Vike.
- If you are already using vite-plugin-ssr then migrate to Vike.
- For new projects, don't use vite-plugin-ssr but use Vike instead.
Render Modes (SPA, SSR, SSG, HTML-only)
For each page, we can choose a different render mode:
- SPA
- SSR
- HTML-only
- Pre-rendering (aka SSG)
For example, we can render an admin panel as SPA while rendering marketing pages with SSR.
What "SPA", "SSR", "HTML-only" and "SSG" mean, and which one should be used, is explained at SPA vs SSR (and more).
The vite-plugin-ssr
boilerplates do SSR by default, which is a sensible default that works for most apps.
Instead of manually integrating render modes yourself, you can use vike-*
packages and Bati: vike-*
packages include an ssr: boolean
config and Bati helps you scaffold projects. Bati and vike-*
packages are going to get out of beta soon.
SPA
Rendering a page as SPA means that the page is loaded and rendered only in the browser.
To achieve that:
- We define
.page.client.js
instead of .page.js
.
- We adapt our
render()
hooks.
1. .page.js
=> .page.client.js
By defining /pages/about.page.client.js
instead of /pages/about.page.js
we ensure that the page is loaded only in the browser.
Example:
2. render()
hooks (SPA only)
If we only have SPA pages, then we adapt our render()
hooks like the folllowing.
Client-side render()
hook:
// /renderer/_default.page.client.js
// Environment: browser
import { renderToDom } from 'some-ui-framework'
export { render }
async function render(pageContext) {
const { Page } = pageContext
// UI frameworks usually have two methods, such as `renderToDom()` and `hydrateDom()`.
// Note how we use `renderToDom()` and not `hydrateDom()`.
await renderToDom(document.getElementById('root'), Page)
}
See What is Hydration? for understanding the difference between "rendering to the DOM" and "hydrating the DOM".
We also adapt our server-side render()
hook:
// /renderer/_default.page.server.js
// Environment: server
import { escapeInject } from 'vite-plugin-ssr/server'
export function render() {
// Note that `div#root` is empty
return escapeInject`<html>
<body>
<div id="root"></div>
</body>
</html>`
}
This is the key difference between SPA and SSR: in SPA div#root
is empty, whereas with SSR div#root
contains our Page's root component pageContext.Page
rendererd to HTML.
This means that, with SPA, we use our server-side render()
hook to generate HTML that is just an empty shell: the HTML doesn't contain the page's content.
For production, we usually want to pre-render the HTML of our SPA pages in order to remove the need for a production Node.js server.
We can also use our server-side render()
hook to render <head>
:
// /renderer/_default.page.server.js
// Environment: server
import { escapeInject } from 'vite-plugin-ssr/server'
export function render(pageContext) {
const { title, description } = pageContext.exports.meta
// Even though we load and render our page's components only in the browser,
// we define our page's `<title>` and `<meta name="description">` on the server-side.
return escapeInject`<html>
<head>
<title>${title}</title>
<meta name="description" content="${description}" />
</head>
<body>
<div id="root"></div>
</body>
</html>`
}
pageContext.exports
is explained at Guides > Custom Exports/Hooks.
// /pages/about.page.js
export const meta = {
title: 'About | My App',
description: 'My App is ...'
}
Note how we define pageContext.exports.meta
in about.page.js
instead of about.page.client.js
. That's because we need to be able to access pageContext.exports.meta
from the server-side.
This means that we define both about.page.js
(defining meta data) and about.page.client.js
(defining our page's root component pageContext.Page
).
2. render()
hooks (SPA + SSR)
If we have both SPA and SSR pages, then we adapt our render()
hooks like this:
// /renderer/_default.page.server.js
// Environment: server
import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr/server'
import { renderToHtml } from 'some-ui-framework'
export function render(pageContext) {
let pageHtml
if (pageContext.Page) {
// For SSR pages
pageHtml = renderToHtml(pageContext.Page)
} else {
// For SPA pages
pageHtml = ''
}
return escapeInject`<html>
<body>
<div id="root">${dangerouslySkipEscape(pageHtml)}</div>
</body>
</html>`
}
If we define a page's root component (pageContext.Page
) in .page.client.js
instead of .page.js
, then pageContext.Page
is only defined in the browser.
// /renderer/_default.page.client.js
// Environment: browser
import { renderToDom, hydrateDom } from 'some-ui-framework'
export async function render(pageContext) {
const { Page } = pageContext
const root = document.getElementById('root')
if (
// We detect SPAs by using the fact that `innerHTML === ''` for the first render of an SPA
root.innerHTML === '' ||
// Upon Client Routing page navigation, vite-plugin-ssr sets `pageContext.isHydration`
// to `false`.
!pageContext.isHydration
) {
// - SPA pages don't have any hydration steps: they need to be fully rendered.
// - Page navigation of SSR pages also need to be fully rendered (if we use Client Routing)
await renderToDom(root, Page)
} else {
// The first render of SSR pages is merely a hydration (instead of a full render)
await hydrateDom(root, Page)
}
}
React example: /examples/render-modes/.
Vue Example: GitHub > AaronBeaudoin/vite-plugin-ssr-example
.
SSR
The vite-plugin-ssr
boilerplates and documentation use SSR by default.
So, if we only have SSR pages, then there is nothing for us to do: we simply follow the boilerplates/docs.
If we want to have both SSR and SPA pages, then see the SPA section.
Pre-rendering
See Guides > Pre-rendering (SSG).
HTML-only
⚠️ Using modern UI frameworks (React/Vue/...) to render pages only to HTML is a novel technique and should be considered experimental.
To render a page to HTML-only:
- We define
.page.server.js
instead of .page.js
.
- (Optional) We define
.page.client.js
(e.g. to add minimal amount of JavaScript surgically injecting bits of interactivity).
// /pages/about.page.server.js
// Environment: server
// Usually `Page` is defined in `*.page.js` but for HTML-only we
// define `Page` in `*.page.server.js` instead.
export { Page }
function Page() {
return <>
<h1>HTML-only page</h1>
<p>
This page is rendered only to HTML. (It's not loaded/rendered in the browser-side.)
</p>
</>
}
// /pages/about.page.client.js
// Environment: browser
// This file represents the entire browser-side JavaScript.
// We can omit defining `.page.client.js` in which case the page has zero browser-side JavaScript.
console.log("I'm the page's only browser-side JavaScript line.")
Examples: