Nuxt 3 Hybrid Rendering: Pre-Render Dynamic Routes (SSG)
nitro:config
hook with the prerender:routes
hook.Introduction
Nuxt 3 offers various rendering modes, such as client-side, universal or hybrid-rendering. For SEO and performance optimization, you might want to pre-render routes during the build process, known as Static Site Generation (SSG). In Nuxt 3, a SSG-like application can be generated by utilizing the universal rendering mode to generate HTML pages while preserving the benefits of a client-side rendered application. This is even possible for dynamic routes (sometimes called dynamic pages). Based on a single page layout, they can be used to create a set of pages populated with content fetched from an API.
If you want to render some routes from your pages
directory client-side (e.g., pages/index.vue
) but pre-render dynamic routes (e.g., pages/articles/[slug].vue
) during the build process (SSG), this can be achieved with the hybrid-rendering mode but requires additional Nuxt configuration.
In This Guide
This guide demonstrates two ways to configure the Nuxt 3 hybrid-rendering mode to pre-render dynamic routes, similar to a static site generator (SSG), while rendering other routes client-side. Specifically, this guide focuses on the Nuxt 3 configuration for pre-rendering dynamic routes. For illustration purposes, it is assumed, that the content for the dynamic routes is fetched from a Strapi 4 headless CMS. Nevertheless, the methods can be applied to similar scenarios.
Setup
- Nuxt 3.8.2
- Vite 4.5.0
- nitro 2.8.0
- node 20.14.0
- Strapi 4.15.5
- Nuxt Strapi Module 1.10.0
Prerequisites
- Basic understanding of how to use the Nuxt 3 configuration file
nuxt.config.ts
. - Basic understanding of Nuxt 3’s file-based routing.
- An API endpoint being set up. In this case an API endpoint of a Strapi 4 headless CMS, and the Nuxt 3 Strapi module is used.
Static Site Generation in Nuxt 3
In Nuxt 3, static site generation (SSG) can be achieved with the universal rendering mode. This might be little different to SSG in the context of a static site generator such as Jekyll, but it produces HTML files containing the relevant content important to SEO and application performance, because the HTML is immediately available to the client (the browser). These files can be uploaded to any web server, such as Apache HTTP Server, without running a separate process for the application server-side. The easiest way to generate a static Nuxt 3 application is by executing the npm run generate
command. This pre-renders dynamic routes and generates the corresponding HTML files which can then be uploaded to a static hosting service, but it does so for all routes of the application.
To combine pre-rendered and client-side rendered routes, the hybrid-rendering mode offers additional features. However, in contrast to npm run generate
, with npm run build
, Nitro, the underlying server engine, must be configured to recognize the dynamic routes. Then, with npm run build
Nuxt produces HTML files only for the routes defined in nuxt.config.ts
.
The configuration depends on the use case. In some cases using the routeRules
option in nuxt.config.ts
is sufficient (refer to the documentation). However, this might not be enough for dynamic routes when content is fetched from an API. For example, if you have a dynamic route pages/articles/[slug].vue
for different articles of an online shop. In this case, the nitro
option from the Nuxt configuration or the prerender:routes
lifecycle hook might be more suitable.
Method 1: Pre-Rendering Dynamic Routes Using the Nitro Lifecycle Hook
This method consists of two steps and hooks into the lifecycle of the Nitro server engine and extends the list of routes to be pre-rendered during the build process (documentation). First, it is necessary to fetch the available paths of the dynamic routes from an API (here, a Strapi 4 headless CMS). Second, the paths (the routes) are then added in the prerender:routes
lifecycle hook to instruct Nitro to pre-render them during the build process.
First, add the following async
function at the beginning of nuxt.config.ts
:
const getArticlesRoutes = async () => {
const url = "https://www.example.com";
const response = await fetch(url + "/api/articles?fields[0]=slug");
const articles = await response.json();
return articles.data.map((article) => `/articles/${article.attributes.slug}`);
};
export default defineNuxtConfig({ … });
This function fetches the slugs of all articles from the Strapi 4 API (line 3), extracts the slug for each article, and returns an array of paths (line 5). The schema can be converted to similar scenarios and is not limited to the Strapi 4 CMS as long as the function returns an array with the necessary paths in the form ["/articles/product-1", "/articles/product-2"]
.
In the second step, hook into the build process, before initializing Nitro with the prerender:routes
hook. Therefore, add lines 10 to 16 to the nuxt.config.ts
:
const getArticlesRoutes = async () => {
const url = "https://www.example.com";
const response = await fetch(url + "/api/articles?fields[0]=slug");
const articles = await response.json();
return articles.data.map((article) => `/articles/${article.attributes.slug}`);
};
export default defineNuxtConfig({
…
hooks: {
async "prerender:routes"(ctx) {
const routes = await getArticlesRoutes();
routes.forEach(route => ctx.routes.add(add));
}
},
…
});
Explanation:
async "prerender:routes"(ctx)
address the lifecycle hook, to apply the subsequent configuration. Theasync
keyword is necessary because the asynchronousgetArticleRoutes()
function will be called.await getArticlesRoutes()
calls the asynchronous function to fetch the paths as explained above and assigns it to theroutes
constant.- routes.forEach(route => ctx.routes.add(add)); adds the fetched routes to the set of routes to be pre-rendered.
nitro:config
hook to add the addional routes. For the sake of completeness, this approach is shown below. It is based on a discussion on GitHub) and leads to the same result.Initial nuxt.config.ts
with the nitro:config
:...
export default defineNuxtConfig({
…
hooks: {
async "nitro:config"(nitroConfig) {
if (nitroConfig.dev && !process.argv?.includes("generate")) { return }
const routes = await getArticlesRoutes();
nitroConfig.prerender?.routes?.push(...routes);
}
},
…
});
async "nitro:config"(nitroConfig)
address the lifecycle hook, to apply the subsequent configuration. Theasync
keyword is necessary because the asynchronousgetArticleRoutes()
function will be called.if (nitroConfig.dev && !process.argv?.includes("generate")) { return }
ensures to limit the configuration tonpm run build
, sincenpm run generate
already pre-renders all routes. The Nitro configuration object that is given to the function has an attribute that contains the information whether the server is in development mode (nitroConfig.dev
). To check if thenpm run generate
command was executed, theargv
interface of the Node.jsprocess
module can be used to check if it includes a substringgenerate
(process.argv?.includes("generate")
). Only if the process is in thebuild
mode, the function is continued. Otherwisereturn
exits the lifecycle hook.await getArticlesRoutes()
calls the asynchronous function to fetch the paths as explained above and assigns it to theroutes
constant.nitroConfig.prerender?.routes?.push(...routes)
adds the fetched routes to the array of routes to be pre-rendered in the Nitro configuration.
Method 2: Enabling Nitro's Dynamic Route Crawling for Pre-Rendering
Instead of specifying which dynamic routes to pre-render, this method configures Nitro to crawl the routes itself. This is done by setting the nitro option
in nuxt.config.ts
. The following configuration enables the pre-rendering of dynamic routes:
export default defineNuxtConfig({
…
nitro: {
prerender: {
crawlLinks: true,
routes: ["/"],
ignore: ["/api", "/feedback"]
}
},
…
}
Explanation:
crawlLinks: true
instructs Nitro to crawl all links it can find.routes: ["/"]
defines the starting point for Nitro to begin crawling. In this example, the home page is the starting point. Nitro will crawl and pre-render all linked pages, including dynamic routes.ignore: ["/api", "/feedback"]
tells Nitro to ignore these pages for pre-rendering. Alternatively, you can specify them with therouteRules
option, e.g.,routeRules: { "/api": { prerender: false }, … }
For more information, please refer to the Nitro documentation
Running the build process with npm run build
now generates HTML documents for dynamic routes that Nitro can crawl, except those defined in ignore
.
Summary
Nuxt 3's hybrid-rendering mode can be leveraged to pre-render dynamic routes while maintaining client-side rendering for other routes. This can be achieved using either the prerender:routes
lifecycle hook or the nitro.prerender
option available in the Nuxt 3 configuration file. The lifecycle hook specifies which routes to pre-render, while the nitro.prerender
option instructs Nitro to crawl available routes based on the document structure.