---
title: Configuring SSG
related:
  - title: Configuring quasar.config file
    path: ../quasar-config-file.md
---
> [!WARNING]
> **Warning! Beta Stage**
>
> The Quasar SSG Mode is currently in the "beta" stage. Based on the community feedback, the API may change in the future, so check the release notes each time you upgrade "@quasar/app-vite".

## quasar.config file

The `ssg` section controls generated fallback files, client-rendered routes, PWA takeover, store hydration, and advanced build hooks. Page generation itself belongs in `/src-ssg/ssg-renderer`.

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

Example "/quasar.config file":

```js
export default defineConfig(() => ({
  ssr: {
    // Also used by SSG because SSG does not override it
    clientSideRenderingRoutes: ['/admin/**']
  },

  ssg: {
    error404HtmlFilename: '404.html'
  }
}))
```

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.

A typical configuration needs only a few options:

Example "/quasar.config file":

```js
export default defineConfig(() => ({
  ssg: {
    // Keep PWA takeover disabled
    pwa: false,

    // Generate dist/ssg/404.html
    error404HtmlFilename: '404.html',

    /**
     * Exclude these routes from pre-rendering and generate csr.html
     * 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
     */
    clientSideRenderingRoutes: ['/account/**', '/admin']
  }
}))
```

The complete option reference follows. Most applications should leave the manual hydration and build-extension options at their defaults.

Example "/quasar.config file":

```ts
ssg: {
  /**
    * Defines how to handle errors encountered during the SSG rendering process.
    *
    * Possible values:
    *  - "abort": Immediately halts the build process on the first error.
    *  - "error": Accumulates all errors and fails the build at the end.
    *  - "warn": Logs the errors to the console, but the build finishes successfully.
    *  - "ignore": Silently ignores errors with a single warning at the end
    *              with how many pages failed to render.
    *  - A custom function to implement your own error handling logic.
    *
    * Function example:
    *   onSsgRendererError: ({ err, reason, ssgPage }) => {
    *     const pageId = `${ssgPage.route} ${ssgPage.label ? ` (${ssgPage.label})` : ''}`
    *     console.error(
    *       `Error rendering SSG page with route ${pageId}${reason ? `: ${reason}` : ''}`
    *     )
    *     // Optional: exit the build process with an error code
    *     // If not exiting, the build will continue and accumulate errors then fail at the end.
    *     process.exit(1)
    *   }
    *
    * @type OnSsgRendererError
    * @default 'abort'
    */
  onSsgRendererError?:
    | "abort"
    | "error"
    | "warn"
    | "ignore"
    | (({
        err,
        reason,
        ssgPage
      }: {
        err: unknown;
        reason?: string;
        ssgPage: SsgPage;
      }) => void | Promise<void>);

  /**
   * The maximum number of SSG pages to render concurrently.
   * This can speed up rendering that includes asynchronous work, such as data fetching.
   * The render jobs run concurrently in the same Node.js thread.
   * Asynchronous work from different pages can be interleaved, and any module-level
   * state is shared between all pages being rendered.
   *
   * @default 1
   */
  ssgRendererConcurrency?: number;

  /**
   * The non-negative number of times for the SSG rendering process to retry
   * rendering a page if an error occurs during the SSG rendering process.
   * Redirects and unmatched routes are not retried.
   *
   * @default 0
   */
  ssgRendererRetryCount?: number;

  /**
   * The non-negative delay in milliseconds between retries for the SSG rendering process.
   * This can help avoid overwhelming the system or external resources when retrying.
   *
   * @default 1000
   */
  ssgRendererRetryDelay?: number;

  /**
   * Controls the generated filename for a page that does not specify
   * `dir` or `filename` in its SSG page definition.
   *
   * When true, the route "/about" generates "about/index.html", which
   * most static webservers resolve for both "/about" and "/about/".
   * When false, it generates "about.html" instead.
   * The root route always generates "index.html".
   *
   * Pages that specify `dir` or `filename` are not affected.
   *
   * @default true
   */
  ssgRendererDirectoryIndexes?: boolean;

  /**
    * The name of the html file that will be used for the 404 page.
    * If set to false, no 404 page will be generated.
    *
    * You will need to properly configure the webserver to serve this
    * file for 404 errors.
    *
    * Make sure to name it so that the SSG generated html files
    * don't conflict with it!
    *
    * @default '404.html'
    */
  error404HtmlFilename?: string | false;

  /**
    * Configure this for a hybrid SSG + partial CSR (Client-Side Rendering)
    * build, where you want the client to use an empty shell html for some
    * of the pages (as if those pages are part of a SPA) and let the client-side
    * code take over and render the page.
    *
    * For production only. You will need to properly configure the webserver
    * to fallback to this html file for the pages that are not pre-rendered by SSG.
    *
    * Make sure to name it so that the SSG generated html files
    * don't conflict with it!
    *
    * If you are building a SSG+PWA app, you might want to directly use the
    * `pwaOfflineHtmlFilename` as the empty shell html file instead,
    * as it will have the same content. If you do not reuse that filename,
    * choose a different one to avoid a generated-file conflict.
    *
    * If not explicitly configured and `clientSideRenderingRoutes`
    * is not its default value (an empty array), then this option will
    * default to 'csr.html'.
    *
    * @default false | 'csr.html'
    */
  clientSideRenderingHtmlFilename?: string | false;

  /**
    * Configure this for a hybrid SSG + partial CSR (Client-Side Rendering)
    * approach, where you have some Vue Router routes that you want to be
    * rendered on the client-side exclusively.
    *
    * When not also specifying `clientSideRenderingHtmlFilename`, the default
    * value for it becomes 'csr.html'.
    *
    * For production, you will need to properly configure the webserver
    * to fallback to the `clientSideRenderingHtmlFilename` for the pages that
    * are not pre-rendered by SSG.
    *
    * 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 ssr.clientSideRenderingRoutes (when configured), otherwise []
    */
  clientSideRenderingRoutes?: string[];

  /**
   * Configure the Vue Router routes for which you don't want to inject
   * preload tags in the generated HTML by the SSG renderer process.
   *
   * 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 ssr.noPreloadTagRoutes (when configured), otherwise []
   */
  noPreloadTagRoutes?: string[];

  /**
    * Extend the Rolldown config that is used for the SSG renderer,
    * which is your /src-ssg/ssg-renderer file.
    *
    * Can be async. Can directly modify the "config" parameter or
    * return a new one that will be merged with the default one.
    */
  extendSSGRendererConf?: (
    config: RolldownOptions
  ) => void | RolldownOptions | Promise<void | RolldownOptions>;

  /**
    * 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.
    */
  extendSSGManifestJson?: (
    ssrManifest: QuasarSsrManifest
  ) => void | QuasarSsrManifest | Promise<void | QuasarSsrManifest>;

  /**
    * If a PWA should take over or just a SPA.
    * @default false
    */
  pwa?: boolean;

  /**
    * When using SSG+PWA, this is the name of the
    * PWA index html file that the client-side fallbacks to.
    * For production only.
    *
    * Make sure to name it so that the SSG generated html files
    * don't conflict with it. It may intentionally match
    * `clientSideRenderingHtmlFilename` to reuse the same application shell.
    *
    * @default ssr.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.
    */
  extendSSGGenerateSWOptions?: (
    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.
    */
  extendSSGInjectManifestOptions?: (
    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 ssr.manualStoreSerialization (when configured), otherwise false
    */
  manualStoreSerialization?: boolean;

  /**
    * Manually inject the store state into ssrContext.state
    * @default ssr.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 ssr.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 ssr.manualPostHydrationTrigger (when configured), otherwise false
    */
  manualPostHydrationTrigger?: boolean;
}
```

> If you enable PWA client takeover, Quasar CLI installs PWA mode too. Read [SSG with PWA](ssg-with-pwa.md) and the [Quasar PWA guide](../developing-pwa/introduction.md).

> If you want certain routes of your app to be rendered exclusively on the client side, [Hybrid SSG + partial CSR](hybrid-ssg-with-partial-csr.md) is here for you.

To extend the Vite configuration used to build application code under `/src`, use the normal `build.extendViteConf` hook and check for SSG mode:

Example "/quasar.config file":

```js
export default defineConfig(ctx => {
  return {
    build: {
      extendViteConf(viteConf, { isClient, isServer }) {
        if (ctx.mode.ssg) {
          // do something with viteConf
          // or return an object to deeply merge with current viteConf
        }
      }
    }
  }
})
```

### Manually triggering store hydration

By default, Quasar CLI serializes the Pinia state into the generated HTML and hydrates the store on the client. See [State serialization](../developing-ssr/writing-universal-code.md#state-serialization) for which value types survive the trip.

Set `ssg.manualStoreHydration: true` only when you need to replace that client-side hydration step. For example, use a client-only [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.

For an advanced integration that must control this timing, set `ssg.manualPostHydrationTrigger: true` and call the hook yourself after mounting:

```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 `ssg.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.

## SSG Renderer

Adding SSG mode to a Quasar project means a new folder will be created: `/src-ssg`, which contains SSG specific files, like the ssg renderer script:

- **src-ssg**
  - **ssg-renderer.js**
    _(or .ts) SSG generator script_
  - **package.json**
    _helps install SSG only deps directly under /src-ssg_

Important details:

1. A package imported directly by `/src-ssg/ssg-renderer` must be listed in `/src-ssg/package.json` and installed in that folder.
2. The renderer is built with a separate Rolldown configuration. Extend it through `/quasar.config` only when the renderer needs custom build behavior:

Example "/quasar.config file":

```ts
ssg: {
  /**
    * Extend the Rolldown config that is used for the SSG renderer,
    * which is your /src-ssg/ssg-renderer file.
    *
    * Can be async. Can directly modify the "config" parameter or
    * return a new one that will be merged with the default one.
    */
  extendSSGRendererConf?: (
    config: RolldownOptions
  ) => void | RolldownOptions | Promise<void | RolldownOptions>;
}
```

3. See [SSG Renderer](ssg-renderer.md) for the renderer API and page examples.

## Helping SEO

Use the [Quasar Meta Plugin](../../quasar-plugins/meta.md) to include route-specific titles, descriptions, canonical links, and social metadata in generated HTML. See [SEO for SSG](seo-for-ssg.md).

## Boot Files

In SSG mode, application code must be universal: it runs in Node.js during the production build and again in the browser during hydration. This also applies to [Boot Files](../boot-files.md).

Mark browser-only or build-time-only boot files explicitly:

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
  ]
}
```

Server-only boot files run at build time for every rendered page. They do not run on the production static host.

When a boot file runs during server-side generation, its callback receives the [ssrContext](../developing-ssr/ssr-context.md):

Example "Some boot file":

```js
import { defineBoot } from '#q-app'

export default defineBoot(({ ssrContext }) => {
  ssrContext.someProp = 'some value'
})
```

When `/index.html` references a custom value, guard it by mode and ensure every applicable page definition provides that value:

Example "/index.html":

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