Home

Awesome

Static Site Generator App Extension for Quasar v2, the Vue.js Framework

A Quasar v2 App Extension to generate static site AKA JAMstack.

:new: Supports Vite since v4.4.0.

npm GitHub code size in bytes GitHub repo size npm Commitizen friendly

This project was created to fill this Feature Request from Quasar.

:warning: Looking for Quasar v1 with Vue 2 ? See corresponding quasar-app-extension-ssg v2 documentation instead of latest version.

Live Demo | Installing | Uninstalling | Upgrading | Developing | Usage | Configuration | Infos

Live Demo

A live demo built from a fresh new Quasar CLI project (with Vite) is available at https://quasar-app-extension-ssg-vite.netlify.app.

The demo achieves a Google PageSpeed ​​Insights score of 100 for mobile and desktop platforms.

<details> <summary>View mobile report</summary>

mobile report

</details> <details> <summary>View desktop report</summary>

desktop report

</details>

Installing

Run this command into the Quasar project:

quasar ext add ssg

This will find and install the extension’s module. Once the installation is complete, interactive prompts will wait for responses.

Prompts

<details> <summary>Vite</summary> </details> <details> <summary>Webpack</summary> </details>

Uninstalling

quasar ext remove ssg

Upgrading

This is done with the same command used for installation:

echo y | quasar ext add ssg

Note: It is recommended to overwrite files when requested, so as not to miss any updates.

Usage

Generate

To generate a static site run this command from the quasar project folder:

quasar ssg generate

Generate Options

Dev

:new: Added in v4.2.0

Starts the app in development mode (live reloading, error reporting, etc):

quasar ssg dev

The development server allows to develop the app by compiling and maintaining code in-memory. A web server will serve the app while offering live-reload out of the box. Running in-memory offers faster rebuilds when the code is changed.

The server can be configured by editing the `/quasar.config.js’ file:

devServer: {
  host: '...',
  port: ...
}

Dev Options

Serve

This extension provides a command to create a server to locally test the generated static site:

quasar ssg serve <dist-folder>

Notes: This server is based on the Quasar cli server adapted for static site. It serves the SPA fallback file (404.html) when a page has not been generated for a given route.

Serve Options

Inspect

This command can be used to inspect the Webpack config generated by this app extension.

quasar ssg inspect

Inspect Options

<details> <summary>Vite</summary> </details> <details> <summary>Webpack</summary> </details>

Configuration

Options can be passed with ssg key in /quasar.config.js file.

// quasar.config.js

module.exports = function (/* ctx */) {
  return {
    // ...

    ssg: {
      // pass options here
    },

    // ...
  };
};

Webpack

<details> <summary>See all available options</summary>

concurrency

Type: Number

Default: 10

Page generation is concurrent, ssg.concurrency specifies the amount of page generation that runs in one thread.

interval

Type: Number

Default: 0

Interval in milliseconds between two batches of concurrent page generation to avoid flooding a potential API with calls to the API from the web application.

Notes:

This option is intended to be used in conjunction with the concurrency option. For example, setting concurrency to 10 and interval to 5000 will execute the generation of 10 pages in parallel every 5 seconds.

routes

Type: String[] or Function

Default: []

A list of routes to generate the corresponding pages.

Note: As of quasar-app-extension-ssg v2.0.0 this option is optionnal due to the crawler feature and the ability to include static routes from the app's router using the ssg.includeStaticRoutes option.

If the app has unlinked pages (such as secret pages) and these also need to be generated, the ssg.routes property can be used.

Example:

ssg: {
  routes: ["/", "/about", "/users", "/users/someone"];
}

With a Function which returns a Promise:

// quasar.config.js

const axios = require("axios");

module.exports = function (/* ctx */) {
  return {
    // ...

    ssg: {
      routes() {
        return axios.get("https://my-api/users").then((res) => {
          return res.data.map((user) => {
            return "/users/" + user.id;
          });
        });
      },
    },

    // ...
  };
};

With a Function which returns a callback(err, params):

// quasar.config.js

const axios = require("axios");

module.exports = function (/* ctx */) {
  return {
    // ...

    ssg: {
      routes(callback) {
        axios
          .get("https://my-api/users")
          .then((res) => {
            const routes = res.data.map((user) => {
              return "/users/" + user.id;
            });
            callback(null, routes);
          })
          .catch(callback);
      },
    },

    // ...
  };
};

includeStaticRoutes

:new: Added in v4.0.0

Type: Boolean

Default: true

Include the application's router static routes to generate the corresponding pages.

Note: In case of warnings issued when initializing routes, this option can be disabled. Then the crawler feature and the ssg.routes options can be used to provide the static and dynamic routes.

distDir

:new: Added in v4.2.0

Type: String

Default: '<project-folder>/dist/ssg'

Folder where the extension should generate the distributables. Relative path to project root directory.

buildDir

Type: String

Default: '<project-folder>/node_modules/.cache/quasar-app-extension-ssg' or '<project-folder>/.ssg-build' if cache is set to false.

The webpack compilation output folder from where the extension can prerender pages.

cache

Type: Object or false

Default:

{
  ignore: [
    join(conf.ssg.distDir, '/**'), // dist/ssg
    join(conf.ssg.buildDir, '/**'), // node_modules/.cache/quasar-app-extension-ssg
    ...conf.build.distDir ? [join(conf.build.distDir, '/**')] : [],
    'dist/**',
    'public/**',
    'src-ssr/**',
    'src-cordova/**',
    'src-electron/**',
    'src-bex/**',
    'src/ssg.d.ts',
    'node_modules/**',
    '.**/*',
    '.*',
    'README.md'
  ],
  globbyOptions: {
    gitignore: true
  }
}

This option caches the compilation output folder and skips recompilation when no tracked file has changed.

fallback

Type: String

Default: '404.html'

The name of the SPA/PWA fallback file intended to be served when an index.html file does not exist for a given route.

Notes:

crawler

:new: Added in v2.0.0

Type: Boolean

Default: true

Crawls html links as each page is generated to find dynamic and static routes to add to the page generation queue.

exclude

:new: Added in v2.0.0

Type: String[] | Regexp[]

An array of routes or regular expressions matching them to prevent corresponding pages from being generated.

Example with an Array of String:

ssg: {
  exclude: ["/my-secret-page"];
}

With an Array of Regexp:

ssg: {
  exclude: [
    /^\/admin/, // path starts with /admin
  ];
}

shouldPreload(file, type, ext, isLazilyHydrated)

:new: Added in v3.3.0

Type: Function

A function to control what files should have <link rel="preload"> resource hints generated.

By default, no assets will be preloaded.

Example to preload assets:

ssg: {
  shouldPreload: (file, type, ext) => {
    // type is inferred based on the file extension.
    // https://fetch.spec.whatwg.org/#concept-request-destination
    if (type === "script" || type === "style") {
      return true;
    }
    if (type === "font" && ext === "woff2") {
      // only preload woff2 fonts
      return file;
    }
    if (type === "image") {
      // only preload important images
      return file === "hero.jpg";
    }
  };
}

shouldPrefetch(file, type, ext, isLazilyHydrated)

:new: Added in v3.3.0

Type: Function

A function to control what files should have <link rel="prefetch"> resource hints generated.

By default no assets will be prefetched. However this is possible to customize what to prefetch in order to better control bandwidth usage. This option expects the same function signature as shouldPreload.

inlineCriticalCss

Type: Boolean or Object

Default: true

Uses Beastcss to inline critical CSS and async load the rest for each generated page.

The default beastcss options can be customized by passing them to inlineCriticalCss.

Example:

ssg: {
  inlineCriticalCss: {
    internal: false,
    merge: false,
  };
}

Notes:

The value is forced to false when using the dev command.

inlineCssFromSFC

:new: Added in v3.3.0

Type: Boolean

Default: false

Inline css from Vue Single-File Component (SFC) <style> blocks.

Note: This option works even if build.extractCSS is set to true in quasar.config.js file.

Notes:

The value is forced to true when using the dev command.

onRouteRendered(html, route, distDir)

Type: Function

Hook executed after pre-rendering a page just before writing it to the filesystem.

This function must return the html string.

Can use async/await or directly return a Promise.

afterGenerate(files, distDir)

Type: Function

Hook executed after all pages has been generated.

Can use async/await or directly return a Promise.

Note: The files parameter is an Array of all generated page paths + filenames (including the fallback file).

</details>

Vite

<details> <summary>See all available options</summary>

concurrency

Type: Number

Default: 10

Page generation is concurrent, ssg.concurrency specifies the amount of page generation that runs in one thread.

interval

Type: Number

Default: 0

Interval in milliseconds between two batches of concurrent page generation to avoid flooding a potential API with calls to the API from the web application.

Notes:

This option is intended to be used in conjunction with the concurrency option. For example, setting concurrency to 10 and interval to 5000 will execute the generation of 10 pages in parallel every 5 seconds.

routes

Type: String[] or Function

Default: []

A list of routes to generate the corresponding pages.

Note: This option is optionnal due to the crawler feature and the ability to include static routes from the app's router using the ssg.includeStaticRoutes option.

If the app has unlinked pages (such as secret pages) and these also need to be generated, the ssg.routes property can be used.

Example:

ssg: {
  routes: ["/", "/about", "/users", "/users/someone"];
}

With a Function which returns a Promise:

// quasar.config.js

const axios = require("axios");

module.exports = function (/* ctx */) {
  return {
    // ...

    ssg: {
      routes() {
        return axios.get("https://my-api/users").then((res) => {
          return res.data.map((user) => {
            return "/users/" + user.id;
          });
        });
      },
    },

    // ...
  };
};

With a Function which returns a callback(err, params):

// quasar.config.js

const axios = require("axios");

module.exports = function (/* ctx */) {
  return {
    // ...

    ssg: {
      routes(callback) {
        axios
          .get("https://my-api/users")
          .then((res) => {
            const routes = res.data.map((user) => {
              return "/users/" + user.id;
            });
            callback(null, routes);
          })
          .catch(callback);
      },
    },

    // ...
  };
};

includeStaticRoutes

Type: Boolean

Default: true

Include the application's router static routes to generate the corresponding pages.

Note: In case of warnings issued when initializing routes, this option can be disabled. Then the crawler feature and the ssg.routes options can be used to provide the static and dynamic routes.

distDir

Type: String

Default: '<project-folder>/dist/ssg'

Folder where the extension should generate the distributables. Relative path to project root directory.

compilationDir

Type: String

Default: '<project-folder>/node_modules/.cache/quasar-app-extension-ssg' or '<project-folder>/.ssg-compilation' if cache is set to false.

The Vite compilation output folder from where the extension can prerender pages.

cache

Type: Object or false

Default:

{
  ignore: [
    join(conf.ssg.distDir, '/**'), // dist/ssg
    join(conf.ssg.compilationDir, '/**'), // node_modules/.cache/quasar-app-extension-ssg
    join(conf.build.distDir, '/**'),
    'dist/**',
    'src-ssr/**',
    'src-cordova/**',
    'src-electron/**',
    'src-bex/**',
    'src/ssg.d.ts',
    'node_modules/**',
    '.**/*',
    '.*',
    'README.md'
  ],
  globbyOptions: {
    gitignore: true
  }
}

This option caches the compilation output folder and skips recompilation when no tracked file has changed.

fallback

Type: String

Default: '404.html'

The name of the SPA/PWA fallback file intended to be served when an index.html file does not exist for a given route.

Notes:

crawler

Type: Boolean

Default: true

Crawls html links as each page is generated to find dynamic and static routes to add to the page generation queue.

exclude

Type: String[] | Regexp[]

An array of routes or regular expressions matching them to prevent corresponding pages from being generated.

Example with an Array of String:

ssg: {
  exclude: ["/my-secret-page"];
}

With an Array of Regexp:

ssg: {
  exclude: [
    /^\/admin/, // path starts with /admin
  ];
}

shouldPreload({ file, type, extension, isLazilyHydrated })

Type: Function

A function to control what files should have <link rel="preload"> resource hints generated.

By default, no assets will be preloaded.

Example to preload assets:

ssg: {
  shouldPreload: ({ file, type, extension, isLazilyHydrated }) => {
    // type is inferred based on the file extension.
    // https://fetch.spec.whatwg.org/#concept-request-destination
    if (type === "script" || type === "style") {
      return true;
    }

    if (type === "font" && ext === "woff2") {
      // only preload woff2 fonts
      return file;
    }

    if (type === "image") {
      // only preload important images
      return file === "hero.jpg";
    }

    // do not preload anything else
    return false;
  };
}

shouldPrefetch({ file, type, extension, isLazilyHydrated })

Type: Function

A function to control what files should have <link rel="prefetch"> resource hints generated.

By default no assets will be prefetched. However this is possible to customize what to prefetch in order to better control bandwidth usage. This option expects the same function signature as shouldPreload.

inlineCriticalCss

Type: Boolean or Object

Default: true

Uses Beastcss to inline critical CSS and async load the rest for each generated page.

The default beastcss options can be customized by passing them to inlineCriticalCss.

Example:

ssg: {
  inlineCriticalCss: {
    internal: false,
    merge: false,
  };
}

Notes:

The value is forced to false when using the dev command.

robotoFontDisplay

:new: Added in v4.5.0

Type: String

Default: 'Optional'

Set the font-display css descriptor of Roboto font imported from @quasar/extras package.

This blog post from the Chrome Developers website can help to choose the best value.

Notes: The Roboto font imported from @quasar/extras package is replaced by its woff2 version (instead of woff) which reduces its weight by half.

With the help of the unicode-range css descriptor the browser will download latin and/or latin-ext variants depending on the characters used in the page.

autoImportSvgIcons

:new: Added in v4.5.0

Type: Boolean

Default: true

Auto import svg icons from @quasar/extras package.

Notes: For better performance when compiling the application, only icons from the configured Quasar Icon Set are auto imported.

onPageGenerated({ html, route, path })

Type: Function

Hook executed after pre-rendering a page just before writing it to the filesystem. This hook can be used to update html string and/or the generated page output path.

This function must return an Object containing html and path properties.

Can use async/await or directly return a Promise.

Example:

const { join, sep } = require('path');

// skipped code...

ssg: {
  onPageGenerated(page) {
    // do not write generated pages in subfolders

    // replace dist/ssg/some-route/index.html
    // by dist/ssg/some-route.html

    const normalizedRoute = page.route.replace(/\/$/, '');

    const fileName = page.route.length > 1 ? join(sep, normalizedRoute + '.html') : join(sep, 'index.html')

    return {
      html: page.html,
      path: page.path.replace(join(page.route, 'index.html'), fileName),
    };
  },
}

afterGenerate(files, distDir)

Type: Function

Hook executed after all pages has been generated.

Can use async/await or directly return a Promise.

Note: The files parameter is an Array of all generated page paths + filenames (including the fallback file).

</details>

Tips

Lazy/partial Hydration

It is possible to lazy hydrate components using the vue3-lazy-hydration package.

In production, when generating pages, the extension does not inject script/preload tags for split chunks corresponding to lazily hydrated components. In this way, these components are loaded client-side, on-demand, when hydration occurs.

Identify running mode

Since the version v4.0.0 the value of process.env.MODE is ssg when the app was built with the command quasar ssg generate or quasar ssg dev.

Below v4.0.0, process.env.STATIC can be used instead.

It could be useful if multiple builds are mixed with different modes to differentiate runtime procedures.

Svg Icons

Svg icons are highly recommended for SSG. The extension tries to reduce the disavantage of using svg by auto importing them.

Example:

// quasar.config.js
module.exports = configure(function (/* ctx */) {
  return {
    // skipped codes...
    framework: {
      iconSet: "svg-material-icons",
    },
  };
});
<!--- default MainLayout.vue --->
<template>
  <q-layout view="lHh Lpr lFf">
    <q-header elevated>
      <q-toolbar>
        <!--- matMenu icon is auto imported -->
        <q-btn
          flat
          dense
          round
          :icon="matMenu"
          aria-label="Menu"
          @click="toggleLeftDrawer"
        />
        <!--- skipped code -->
      </q-toolbar>
    </q-header>
  </q-layout>
</template>

<script>
  // skipped code...

  const linksList = [
    {
      title: "Docs",
      caption: "quasar.dev",
      icon: matSchool, // auto imported icon
      link: "https://quasar.dev",
    },
    // skipped code...
  ];

  export default defineComponent({
    // skipped code....
  });
</script>

Infos

About Boot File

This Extension uses a boot file ssg-corrections.js at client-side to apply corrections to the <body> tag classes.

This is necessary because the running platform is unknown at build time.

About PWA

SSG + PWA can be enabled by setting ssr.pwa to true inside quasar.config.js file.

<details> <summary>Vite</summary>

Quasar uses workbox-build package to generate a complete service worker and a list of assets to precache which is injected into the service worker file.

This means that all generated pages cannot be precached when Vite is compiling because they do not yet exist at this time. To fix this, when running the generate command, the extension moves the execution of workbox-build methods after all pages have been generated.

</details> <details> <summary>Webpack</summary>

Quasar uses workbox-webpack-plugin package to generate a complete service worker and a list of assets to precache which is injected into the service worker file.

This means that all generated pages cannot be precached when webpack is compiling because they do not yet exist at this time. To fix this, when running the generate command, the extension instead uses workbox-build package after all pages have been generated.

Therefore, workbox-build options must be passed in the key pwa.workboxOptions of quasar.config.js file instead of the workbox-webpack-plugin options. All other PWA options of the pwa key in the quasar.config.js file are valid and used according to the Quasar documentation.

</details>

About Cache Feature

The cache mechanism to avoid recompiling the app when it is not necessary is strongly inspired by Nuxt. See the Nuxt blog post about this feature.

Contributing

The quasar-app-extension-ssg repository is a monorepo using pnpm workspaces. The package manager used to install and link dependencies must be pnpm.

The monorepo contains the extension package inside the packages folder. It also contains private packages in the playground folder for testing the extension with quasar CLI with vite or webpack.

To develop locally, fork the repository then:

  1. Clone it in your local machine.

  2. Run pnpm i in the root folder.

  3. Change the code in the packages/quasar-app-extension-ssg folder.

  4. Run pnpm -w run command:vite or pnpm -w run command:webpack to test the extension commands against private packages in the playground folder.

:warning: If you intend to directly run Quasar commands in any playground folders, it's advisable to first run in background the pnpm -w run watch command in the root folder.

Testing against external packages

You may wish to test your locally modified copy against an external quasar project that is using the extension. To do this you must specify pnpm.overrides and list the package as a dependency in the root package.json:

{
  "dependencies": {
    "quasar-app-extension-ssg": "^4.7.0"
  },
  "pnpm": {
    "overrides": {
      "quasar-app-extension-ssg": "link:../path/to/forked-repo/packages/quasar-app-extension-ssg"
    }
  }
}

:warning: If not using pnpm as a package manager it is mandatory to use yalc to avoid possible issues with unresolved dependencies.