For each page, we can choose a different render mode:
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.
Rendering a page as SPA means that the page is loaded and rendered only in the browser.
To achieve that:
.page.client.js
instead of .page.js
.render()
hooks.1. .page.js
=> .page.client.js
By defining /pages/about.page.client.js
instead of /pages/about.page
we ensure that the page is loaded only in the browser.
Example:
AaronBeaudoin/vite-plugin-ssr-example
> /pages/spa.page.client.vue
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: Node.js
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: Node.js
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: Node.js
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
, thenpageContext.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
.
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.
See Guides > Pre-rendering (SSG).
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:
.page.server.js
instead of .page.js
..page.client.js
(e.g. to add minimal amount of JavaScript surgically injecting bits of interactivity).includeAssetsImportedByServer
to true
.// /pages/about.page.server.js
// Environment: Node.js
// 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.")
// vite.config.js
import { ssr } from 'vite-plugin-ssr/plugin'
export default {
plugins: [
ssr({
includeAssetsImportedByServer: true
})
]
}
Examples:
AaronBeaudoin/vite-plugin-ssr-example
> /pages/html.page.server.vue
https://vite-plugin-ssr.com
(this website uses the HTML-only technique).