For each page, we can choose a different render mode:
SPA
SSR
HTML-only
Pre-rendering (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 Vike 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 a UI framework Vike extensionvike-react/vike-vue/vike-solid which already integrates Render Modes. And you can use Bati to scaffold an app that uses vike-react/vike-vue/vike-solid.
SPA
Rendering a page as SPA means that the page is loaded and rendered only in the browser.
To achieve that:
We set Page's meta configenv to { server: false, client: true } instead of { server: true, client: true }.
We adapt our onRenderHtml() and onRenderClient() hooks.
1. Page meta config
By setting Page's meta config env to { client: true, server: false } we tell Vike to load +Page.js only in the browser.
vite-plugin-ssr was the previous name of Vike. Contribution welcome to fork and update the Vue example.
2. Render hooks (SPA only)
If we only have SPA pages, then we adapt our onRenderHtml() and onRenderClient() hooks like the following.
Client-side onRenderClient() hook:
// /renderer/+onRenderClient.js// Environment: browserimport { renderToDom } from 'some-ui-framework'export { onRenderClient }async function onRenderClient(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 onRenderHtml() hook:
// /renderer/+onRenderHtml.js// Environment: serverimport { escapeInject } from 'vike/server'export { onRenderHtml }async function onRenderHtml() { // 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 rendered to HTML.
This means that, with SPA, we use our server-side onRenderHtml() 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 onRenderHtml() hook to render <head>:
// /renderer/+onRenderHtml.js// Environment: serverimport { escapeInject } from 'vike/server'export { onRenderHtml }async function onRenderHtml(pageContext) { const { title, description } = pageContext.config // 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>`}
If we have both SPA and SSR pages, then we adapt our onRenderHtml() and onRenderClient() hooks like this:
// /renderer/+onRenderHtml.js// Environment: serverimport { escapeInject, dangerouslySkipEscape } from 'vike/server'import { renderToHtml } from 'some-ui-framework'export { onRenderHtml }async function onRenderHtml(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 set the Page meta config env to { server: false, client: true } instead of
{ server: true, client: true } in config.js, then pageContext.Page is
only defined in the browser.
// /renderer/+onRenderClient.js// Environment: browserimport { renderToDom, hydrateDom } from 'some-ui-framework'export { onRenderClient }async function onRenderClient(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, Vike 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) }}
// /pages/about/+Page.js// Environment: serverexport { 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/+client.js// Environment: browser// This file represents the entire browser-side JavaScript.// We can omit defining `+client.js` in which case the page has zero browser-side JavaScript.console.log("I'm the page's only browser-side JavaScript line.")