Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 041 / 100

Prerendering and static sites with Nuxt in Vue.js & Nuxt

Fast pages from a CDN.

Expona Admin7 min read

Introduction

When your marketing team asks why the homepage takes three seconds to load, or your documentation site keeps timing out under traffic spikes, you need a different approach than server rendering every request. Prerendering and static sites with Nuxt in Vue.js & Nuxt give you the speed of a CDN-distributed static asset combined with the developer experience of a modern framework. In this guide, we'll walk through how to generate fully static sites, selectively prerender routes, and automate link crawling so your team ships fast pages from a CDN without sacrificing flexibility.

Understanding Static Generation with nuxi generate

The foundation of static site generation in Nuxt is the nuxi generate command. Unlike nuxi build, which prepares your app for server rendering, generate builds your entire application as static HTML files at build time. Each route becomes a .html file that your CDN serves directly.

Let's say you have a blog with ten published posts. Instead of rendering them on demand, you generate them once during your deployment pipeline:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    prerender: {
      crawlLinks: true,
      routes: ['/sitemap.xml', '/rss.xml']
    }
  }
})

When you run npm run generate, Nuxt crawls your app starting from the index page, follows every discovered link, and outputs static HTML for each route. The result is a .output/public directory ready to deploy to Netlify, Vercel, or any static host. No Node.js server needed.

This approach works beautifully for marketing sites, documentation portals, and product catalogs where content changes are less frequent than page views. A single HTML file served from a CDN edge location loads in milliseconds, even under heavy traffic.

Selective Route Prerendering with routeRules

Not every page needs to be static. If you have user-specific content, real-time data, or authenticated sections, you can mix static and server-rendered routes using routeRules. This hybrid approach gives you the best of both worlds.

Imagine a SaaS dashboard with public marketing pages and private user workspaces. You want the landing page, pricing page, and blog prerendered as static, but user dashboards should render on demand:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/': { prerender: true },
      '/pricing': { prerender: true },
      '/blog/**': { prerender: true },
      '/docs/**': { prerender: true },
      '/api/**': { cache: { maxAge: 60 } },
      '/dashboard/**': { ssr: false }
    }
  }
})

With this config, Nuxt treats /, /pricing, all blog and docs routes as prerendered static files. API routes get cached for sixty seconds. Dashboard routes render client-side only, so user data stays private and dynamic. During nuxi generate, only the routes marked prerender: true are output as static HTML.

You can also use swr (stale-while-revalidate) to serve static content while revalidating in the background:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/news/**': { swr: 3600 },  // Serve cached, revalidate every hour
      '/blog/**': { prerender: true, swr: false }
    }
  }
})

This setup is ideal for sites where freshness matters but traffic spikes are unpredictable. The CDN serves cached HTML while your server refreshes it in the background.

One of the biggest challenges with static generation is ensuring you've included every page. If you forget to add a route to your prerender list, it won't be built. Nuxt solves this with automatic link crawling.

When you enable crawlLinks: true, Nuxt starts from your index page and follows every internal link it finds in your HTML. This discovery process is recursive, so it will find pages linked from pages linked from your home page:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    prerender: {
      crawlLinks: true,
      routes: [
        '/',
        '/sitemap.xml',
        '/rss.xml',
        '/404'  // Explicitly include error pages
      ]
    }
  }
})

Let's trace through a real scenario. Your app has a page listing all blog posts:

typescript
// pages/blog/index.vue
<template>
  <div>
    <h1>Blog</h1>
    <ul>
      <li v-for="post in posts" :key="post.slug">
        <NuxtLink :to="`/blog/${post.slug}`">
          {{ post.title }}
        </NuxtLink>
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
const posts = await useFetch('/api/posts')
</script>

During the crawl phase, Nuxt will:

  1. Start at / (your index page)
  2. Parse the HTML and find a link to /blog
  3. Visit /blog, fetch the posts, render the page
  4. Find links to /blog/first-post, /blog/second-post, etc.
  5. Visit each blog post and render it as static HTML

The crawler respects your app's routing structure, so nested routes, dynamic segments, and query parameters all work. However, links created dynamically after hydration or via JavaScript won't be discovered. For those cases, add them to the routes array explicitly.

Deployment and CDN Strategy

Once you've generated your static files, deployment becomes straightforward. Your .output/public directory contains everything needed:

  • HTML files for every prerendered route
  • CSS, JavaScript, and image assets
  • A server entry point for any dynamic routes or API handlers

On Vercel or Netlify, you can push to your repository and let the platform handle the build and deployment automatically. The static files get cached and distributed across their global CDN. Users in Tokyo load your site from an edge server in Tokyo, not from your origin.

For higher-traffic sites or specific requirements, you might deploy to Cloudflare Pages or AWS CloudFront. The generated output is agnostic to the host:

typescript
// Typical deployment flow
// 1. Run: npm run generate
// 2. Output: .output/public (all static files)
// 3. Deploy: Upload to CDN or commit to hosting provider
// 4. Result: Static HTML served from edge locations worldwide

One important consideration: if you use routeRules with non-prerendered routes, your deployment needs a Node.js server to handle those routes. Platforms like Vercel and Netlify handle this automatically, but serverless environments might require additional configuration.

Handling Dynamic and User-Specific Content

Static generation works great for public, content-driven sites. But what if you have user-specific data that shouldn't be prerendered? The answer is hybrid rendering and client-side data fetching.

For example, a user's notification count or dashboard data should never be baked into static HTML. Instead, fetch it client-side:

typescript
// pages/dashboard.vue
<template>
  <div>
    <h1>Dashboard</h1>
    <p v-if="pending">Loading...</p>
    <p v-else-if="data">Notifications: {{ data.count }}</p>
  </div>
</template>

<script setup lang="ts">
const { data, pending } = await useFetch('/api/notifications')
</script>

In your nuxt.config.ts, tell Nuxt not to prerender this route:

typescript
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/dashboard/**': { ssr: false }  // Client-side only
    }
  }
})

The dashboard HTML is generated with a loading state, then your client-side code fetches the user's real data after hydration. This keeps static files public while preserving privacy.

Common Mistakes

Forgetting to disable crawlLinks when you have auth-required routes: If your app requires authentication but crawlLinks is enabled, Nuxt will get redirected to the login page and won't discover protected routes. Either disable crawling or explicitly list protected routes.

Prerendering routes with dynamic segments without providing values: A route like /products/[id] can't be prerendered without knowing which IDs to build. You must provide them explicitly or use dynamic crawling with links.

Mixing server-only logic with prerendered pages: Code that relies on server context (headers, cookies, sessions) will fail in static HTML. Move authentication checks to middleware or client-side guards.

Not setting correct cache headers for static assets: Prerendered HTML files should use short cache TTLs (or no cache) so updates deploy quickly. JavaScript and image bundles can use long cache times since they have content hashes.

Ignoring the build size: Generating hundreds of routes creates many HTML files. Monitor your .output/public size and optimize before deployment, especially if your hosting has bandwidth limits.

Key Takeaways

  • nuxi generate converts your entire Nuxt app into static HTML, perfect for hosting on a CDN and serving with minimal latency.
  • Use routeRules to selectively prerender certain routes while keeping others server-rendered or client-side, giving you flexibility for mixed content patterns.
  • Enable crawlLinks: true to automatically discover and build all pages reachable from your site's internal links, reducing manual configuration.
  • Prerendering is ideal for marketing sites, documentation, blogs, and product catalogs—any content where updates happen during deployment, not at runtime.
  • Combine static pages with client-side data fetching for user-specific or real-time content, keeping your HTML static while loading dynamic data on demand.
  • Deploy to a CDN like Vercel, Netlify, or Cloudflare to serve static HTML from edge locations worldwide, achieving sub-second load times.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles