---
title: Configuring SSR
related:
  - title: Configuring quasar.config file
    path: ../quasar-config-file.md
---
## 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).

Options shared by the `ssr` and `ssg` sections can be configured once. When a shared option is omitted from `ssr`, Quasar uses an explicitly configured value from `ssg`. A value specified in `ssr`, including `false` or an empty array, always takes precedence.

Example "/quasar.config file":

```js
export default defineConfig(() => ({
  ssr: {
    prodPort: 3000
  },

  ssg: {
    // Also used by SSR because SSR does not override it
    clientSideRenderingRoutes: ['/admin/**']
  }
}))
```

This applies to `pwaOfflineHtmlFilename`, `clientSideRenderingRoutes`, `noPreloadTagRoutes`, and the `manualStore*` and `manualPostHydrationTrigger` options. The `pwa` option remains mode-specific so that enabling PWA takeover for one mode does not implicitly enable it for the other. Other mode-specific options and extension hooks are not shared.

Example "/quasar.config file":

```ts
return {
  // ...
  ssr: {
    /**
     * Requires @quasar/app-vite v3.1+
     *
     * Configure this for a hybrid SSR + partial CSR (Client-Side Rendering)
     * approach, where you have some Vue Router routes that you want to be
     * rendered on the client-side exclusively.
     *
     * You can use picomatch patterns to match the routes you want to be rendered
     * on the client-side. https://www.npmjs.com/package/picomatch
     *
     * Note on picomatch patterns:
     *   "/admin" matches the exact route only
     *   "/admin/**" matches the exact route and all sub-routes of /admin
     *   "/admin/*" matches only direct sub-routes of /admin
     *   "/admin/{users,settings}" matches both exact routes /admin/users and /admin/settings
     *
     * @example ['/dashboard', '/admin/**']
     * @default ssg.clientSideRenderingRoutes (when configured), otherwise []
     */
    clientSideRenderingRoutes?: string[];

    /**
     * Requires @quasar/app-vite v3.1+
     *
     * Configure the Vue Router routes for which you don't want to inject
     * preload tags (on production only!).
     *
     * Only the route-level tags (the chunks a route needs, taken from the
     * build manifest) are left out; the modulepreload tags Vite writes into
     * the HTML template for the app entry's own static imports stay on every
     * route.
     *
     * You can use picomatch patterns to match the routes you want
     * no preload tags for. https://www.npmjs.com/package/picomatch
     *
     * Note on picomatch patterns:
     *   "/admin" matches the exact route only,
     *   "/admin/**" matches the exact route and all sub-routes of /admin,
     *   "/admin/*" matches only direct sub-routes of /admin,
     *   "/admin/{users,settings}" matches both exact routes /admin/users and /admin/settings
     *
     * @example ['/dashboard', '/admin/**']
     * @default ssg.noPreloadTagRoutes (when configured), otherwise []
     */
    noPreloadTagRoutes?: string[];

    /**
     * Manually serialize the store state and provide it yourself
     * as window.__INITIAL_STATE__ to the client-side (through a <script> tag)
     * @default ssg.manualStoreSerialization (when configured), otherwise false
     */
    manualStoreSerialization?: boolean;

    /**
     * Manually inject the store state into ssrContext.state
     * @default ssg.manualStoreSsrContextInjection (when configured), otherwise false
     */
    manualStoreSsrContextInjection?: boolean;

    /**
     * Manually handle the store hydration instead of letting Quasar CLI do it.
     *
     * For Pinia: store.state.value = window.__INITIAL_STATE__
     *
     * @default ssg.manualStoreHydration (when configured), otherwise false
     */
    manualStoreHydration?: boolean;

    /**
     * Manually call $q.onSSRHydrated() instead of letting Quasar CLI do it.
     * This announces that client-side code should takeover.
     * Needed when a Suspense boundary delays hydration (async setup());
     * call the hook from the boundary's @resolve instead.
     * @default ssg.manualPostHydrationTrigger (when configured), otherwise 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 }>;

    /**
     * Requires @quasar/app-vite v3.1+
     *
     * Extend the underlying SSR manifest file generated by Vite,
     * which is used by the server-side renderer to know which files to preload.
     *
     * Can be async. Can directly modify the "ssrManifest" parameter or
     * return a new one that will be merged with the default one.
     */
    extendSSRManifestJson?: (
      ssrManifest: QuasarSsrManifest
    ) => void | QuasarSsrManifest | Promise<void | QuasarSsrManifest>;

    /**
     * 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 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 ssg.pwaOfflineHtmlFilename (when configured), otherwise '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>;
  }
}
```

> 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](../developing-pwa/introduction.md) guide too. But most importantly, make sure you read [SSR with PWA](ssr-with-pwa.md) page.

Should you want to tamper with the Vite config for UI in /src:

Example "/quasar.config file":

```js
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 hydrates Pinia stores on the client.

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](../boot-files.md):

Example "Some boot file":

```js
// 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:

```js
// App.vue

import { onMounted } from 'vue'
import { useQuasar } from 'quasar'

export default {
  // ....
  setup () {
    // ...
    const $q = useQuasar()
    onMounted(() => {
      $q.onSSRHydrated()
    })
  }
}
```

One case where you actually need this is when a [Suspense](https://vuejs.org/guide/built-ins/suspense.html) boundary wraps a component with an async `setup()`. Vue hydrates that subtree only after the async dependencies settle, so the default trigger (which fires when the App wrapper component gets mounted) runs too early. Screen-dependent components inside the boundary (QPage, QImg, ...) then hydrate against live screen measurements instead of the values that the server rendered with, and Vue logs hydration mismatch warnings. Set `ssr.manualPostHydrationTrigger: true` and trigger the hook from the Suspense boundary instead:

```html
<!-- the template hosting your Suspense boundary -->
<router-view #default="{ Component }">
  <suspense @resolve="$q.onSSRHydrated()">
    <component :is="Component" />
  </suspense>
</router-view>
```

Calling it on every resolve is safe. Only the first call has any effect.

## 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:

- **src-ssr**
  - **server-assets/**
    _copied as-is to dist_
  - **middlewares**
    _SSR middleware files_
    - **render.js**
      _(or .ts) middleware to render pages with Vue_
  - **server.js**
    _(or .ts) SSR webserver_

You can freely edit these files. All folders are detailed in their own doc pages (check left-side menu).

Notice a few things:

1. If `/src-ssr` imports a package needed at runtime, list it in `/src-ssr/package.json > dependencies`, not `devDependencies`. Quasar copies it into the generated production `package.json` so it can be installed during deployment.
2. These files are built through a separate Rolldown config. You can extend the Rolldown configuration of these files through the `/quasar.config` file:

Example "/quasar.config file":

```js
return {
  // ...
  ssr: {
    /**
     * 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>;
  }
}
```

3. The `/src-ssr/server.js` file is detailed on the [SSR Webserver](ssr-webserver.md) page. Read it especially if you need to support serverless functions.

## Helping SEO

SSR allows crawlers to receive rendered content for each route. Use the [Quasar Meta Plugin](../../quasar-plugins/meta.md) to add the page titles, descriptions, canonical URLs, and structured data needed by search engines.

## Boot Files

In SSR mode, shared application code must run in both Node.js and browser contexts. This also applies to [Boot Files](../boot-files.md).

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:

Example "/quasar.config file":

```js
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](ssr-context.md)) on the default exported function:

Example "Some boot file":

```js
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:

Example "/index.html":

```html
<% if (ctx.mode.ssr) { %>{{ ssrContext.someProp }} <% } %>
```
