quasar.config file
This is the place where you can configure some SSR options. Like if you want the client side to takeover as a SPA (Single Page Application – the default behaviour), or as a PWA (Progressive Web App).
return {
// ...
ssr: {
/**
* If a PWA should take over or just a SPA.
* @default false
*/
pwa?: boolean;
/**
* When using SSR+PWA, this is the name of the
* PWA index html file that the client-side fallbacks to.
* For production only.
*
* Do NOT use index.html as name as it will mess SSR up!
*
* @default 'offline.html'
*/
pwaOfflineHtmlFilename?: string;
/**
* Extend/configure the Workbox GenerateSW options
* Specify Workbox options which will be applied on top of
* `pwa > extendPWAGenerateSWOptions()`.
*
* https://developer.chrome.com/docs/workbox/the-ways-of-workbox/
*
* Can be async. Can directly modify the "config" parameter or
* return a new one that will be merged with the default one.
*/
extendSSRGenerateSWOptions?: (
config: GenerateSWOptions
) => void | GenerateSWOptions | Promise<void | GenerateSWOptions>;
/**
* Extend/configure the Workbox InjectManifest options
* Specify Workbox options which will be applied on top of
* `pwa > extendPWAInjectManifestOptions()`.
*
* https://developer.chrome.com/docs/workbox/the-ways-of-workbox/
*
* Can be async. Can directly modify the "config" parameter or
* return a new one that will be merged with the default one.
*/
extendSSRInjectManifestOptions?: (
config: InjectManifestOptions
) => void | InjectManifestOptions | Promise<void | InjectManifestOptions>;
/**
* Manually serialize the store state and provide it yourself
* as window.__INITIAL_STATE__ to the client-side (through a <script> tag)
* @default false
*/
manualStoreSerialization?: boolean;
/**
* Manually inject the store state into ssrContext.state
* @default false
*/
manualStoreSsrContextInjection?: boolean;
/**
* Manually handle the store hydration instead of letting Quasar CLI do it.
*
* For Pinia: store.state.value = window.__INITIAL_STATE__
*
* @default false
*/
manualStoreHydration?: boolean;
/**
* Manually call $q.onSSRHydrated() instead of letting Quasar CLI do it.
* This announces that client-side code should takeover.
* @default false
*/
manualPostHydrationTrigger?: boolean;
/**
* The default port (3000) that the production server should use
* (gets superseded if process.env.PORT is specified at runtime)
* @default 3000
*/
prodPort?: number;
/**
* List of middleware files in src-ssr/middlewares
* Order is important.
*/
middlewares?: string[];
/**
* Add/remove/change properties of SSR production generated package.json
*
* Can be async. Can directly modify the "pkgJson" parameter or
* return a new one that will be merged with the default one.
*/
extendSSRPackageJson?: (pkgJson: { [index in string]: any }) =>
| void
| { [index in string]: any }
| Promise<void | { [index in string]: any }>;
/**
* Extend the Rolldown config that is used for the SSR webserver
* (which includes the SSR middlewares).
*
* Can be async. Can directly modify the "config" parameter or
* return a new one that will be merged with the default one.
*/
extendSSRWebserverConf?: (
config: RolldownOptions
) => void | RolldownOptions | Promise<void | RolldownOptions>;
/**
* The named exports to use for the production generated SSR index.js script.
* Works with `false` (no named exports), a single string (one named export),
* or an array of strings (multiple named exports).
*
* Useful for serverless environments where you might want to export the
* handler function. It creates one or more named exports from the
* object returned by the defineSsrListen() function in /src-ssr/server file.
*
* @default false
*
* @example
* prodScriptNamedExport: ['handler', 'ssr']
* export const listen = defineSsrListen(() => {
* if (import.meta.env.QUASAR_PROD) {
* return { handler, ssr }
* }
* })
*
* This will generate an SSR index.js with the following exports:
* const { handler, ssr } = await listen({...})
* export { handler, ssr }
*
* @example
* prodScriptNamedExport: 'default'
* export const listen = defineSsrListen(({ app }) => {
* if (import.meta.env.QUASAR_PROD) {
* return { default: app }
* }
* })
*
* This will generate an SSR index.js with the following exports:
* const listenResult = await listen({...})
* export default listenResult?.default
*
* @example
* prodScriptNamedExport: 'app'
* export const listen = defineSsrListen(({ app }) => {
* if (import.meta.env.QUASAR_PROD) {
* return { app }
* }
* })
*
* This will generate an SSR index.js with the following exports:
* const { app } = await listen({...})
* export { app }
*
* @example 'renderSsrContext' (special case)
*
* This will generate an SSR index.js with the following export:
* export { render as renderSsrContext }
* where "render" is the same function used in
* the /src-ssr/middlewares/render file
*/
prodScriptNamedExport?: false | string | string[];
}
}If you decide to go with a PWA client takeover (which is a killer combo), the Quasar CLI PWA mode will be installed too. You may want to check out the Quasar PWA guide too. But most importantly, make sure you read SSR with PWA page.
Should you want to tamper with the Vite config for UI in /src:
export default defineConfig(ctx => {
return {
build: {
extendViteConf(viteConf, { isClient, isServer }) {
if (ctx.mode.ssr) {
// do something with viteConf
// or return an object to deeply merge with current viteConf
}
}
}
}
})Manually triggering store hydration
By default, Quasar CLI takes care of hydrating the Pinia stores (if you use it) on client-side.
However, should you wish to manually hydrate it yourself, you need to set quasar.config file > ssr > manualStoreHydration: true. One good example is doing it from a boot file:
// MAKE SURE TO CONFIGURE THIS BOOT FILE
// TO RUN ONLY ON CLIENT-SIDE
import { defineBoot } from '#q-app'
export default defineBoot(({ store }) => {
// For Pinia
store.state.value = window.__INITIAL_STATE__
})Manually triggering post-hydration
By default, Quasar CLI wraps your App component and calls $q.onSSRHydrated() on the client-side when this wrapper component gets mounted. This is the moment that the client-side takes over. You don’t need to configure anything for this to happen.
However should you wish to override the moment when this happens, you need to set quasar.config file > ssr > manualPostHydrationTrigger: true. For whatever your reason is (very custom use-case), this is an example of manually triggering the post hydration:
// App.vue
import { onMounted } from 'vue'
import { useQuasar } from 'quasar'
export default {
// ....
setup () {
// ...
const $q = useQuasar()
onMounted(() => {
$q.onSSRHydrated()
})
}
}Node.js Webserver
Adding SSR mode to a Quasar project means a new folder will be created: /src-ssr, which contains SSR specific files for the actual Node.js webserver:
You can freely edit these files. All folders are detailed in their own doc pages (check left-side menu).
Notice a few things:
If you import anything from node_modules in /src-ssr, then make sure that the package is specified in /src-ssr/package.json > “dependencies” (runtime deps) and NOT in “devDependencies” (build system deps). The “dependencies” will be embedded into your dist/.
These files are built through a separate Rolldown config. You can extend the Rolldown configuration of these files through the
/quasar.configfile:
return {
// ...
ssr: {
// ...
extendSSRWebserverConf(rolldownConf) {
// tamper with rolldownConf here
}
}
}- The
/src-ssr/server.jsfile is detailed in SSR Webserver page. Read it especially if you need to support serverless functions.
Helping SEO
One of the main reasons when you develop a SSR instead of a SPA is for taking care of the SEO. And SEO can be greatly improved by using the Quasar Meta Plugin to manage dynamic html markup required by the search engines.
Boot Files
When running on SSR mode, your application code needs to be isomorphic or “universal”, which means that it must run both on a Node.js context and in the browser. This applies to your Boot Files too.
However, there are cases where you only want some boot files to run only on the server or only on the client-side. You can achieve that by specifying:
return {
// ...
boot: [
'some-boot-file', // runs on both server and client
{ path: 'some-other', server: false }, // this boot file gets embedded only on client-side
{ path: 'third', client: false } // this boot file gets embedded only on server-side
]
}Just make sure that your app is consistent, though.
When a boot file runs on the server, you will have access to one more parameter (called ssrContext) on the default exported function:
import { defineBoot } from '#q-app'
export default defineBoot(({ app, ..., ssrContext }) => {
// You can add props to the ssrContext then use them in the /index.html.
// Example - let's say we ssrContext.someProp = 'some value', then in index template we can reference it:
// {{ ssrContext.someProp }}
})When you add such references into your /index.html, make sure you tell Quasar it’s only valid for SSR builds:
<% if (ctx.mode.ssr) { %>{{ ssrContext.someProp }} <% } %>