Skip to content
⚠️ This article was written in 2018. Some content may be outdated.

Nuxt.js Server-Side Rendering Introduction: Why SSR

I recently used Nuxt.js for a project that needed SEO. Here's a summary of the basic SSR concepts and Nuxt's core usage.

Why SSR ​

A standard Vue SPA works like this:

Browser requests the page
→ Server returns empty HTML (just <div id="app"></div>)
→ Browser downloads the JS bundle
→ Vue renders on the client, fills in content
→ User sees the page

This causes two problems:

  1. Poor SEO: search engine crawlers get empty HTML and can't see the content
  2. Slow first paint: users have to wait for JS to download and execute before seeing anything

The SSR solution:

Browser requests the page
→ Server runs Vue, generates complete HTML
→ Returns HTML with content
→ User sees the page immediately (fast first paint)
→ JS loads, Vue takes over (Hydration)
→ Page becomes a normal SPA

What Is Nuxt.js ​

Nuxt.js is a Vue-based SSR framework that hides the complexity of SSR configuration:

  • Automatic routing (file-based)
  • Built-in Vuex integration
  • Automatic code splitting
  • Static site generation (SSG) support

Project Structure ​

nuxt-app/
├── pages/          ← page components, routes are auto-generated
│   ├── index.vue   → /
│   ├── about.vue   → /about
│   └── users/
│       └── _id.vue → /users/:id
├── layouts/        ← layout templates
│   └── default.vue
├── components/     ← regular components
├── store/          ← Vuex store
├── static/         ← static files
├── assets/         ← assets to be processed
└── nuxt.config.js  ← configuration file

Core Concept: asyncData ​

The key SSR hook for fetching data on the server side:

vue
{% raw %}
<template>
  <div>
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
  </div>
</template>

<script>
import axios from "axios";

export default {
  async asyncData({ params, error }) {
    try {
      const { data } = await axios.get(`/api/posts/${params.id}`);
      return { post: data };
    } catch (e) {
      error({ statusCode: 404, message: "Post not found" });
    }
  },
};
</script>
{% endraw %}

asyncData executes on the server and its returned data is merged with data(). The page HTML already contains this data, so search engines can index it.

Note: You cannot use this inside asyncData (the component hasn't been instantiated yet). Use the context argument to access route info, store, etc.

fetch hook ​

fetch also runs on the server, but is used to populate the Vuex store:

vue
<script>
export default {
  async fetch({ store, params }) {
    await store.dispatch("posts/fetchPost", params.id);
  },
};
</script>

Dynamic Routes ​

Use _ prefix in filenames to denote dynamic parameters:

pages/
├── users/
│   ├── index.vue      → /users
│   └── _id.vue        → /users/:id (dynamic)
vue
<!-- pages/users/_id.vue -->
<script>
export default {
  async asyncData({ params }) {
    const userId = params.id;
    // ...
  },
};
</script>

Common nuxt.config.js Options ​

javascript
module.exports = {
  // Page head configuration
  head: {
    title: "My Site",
    meta: [
      { charset: "utf-8" },
      { name: "viewport", content: "width=device-width, initial-scale=1" },
      { name: "description", content: "Site description" },
    ],
  },

  // Global CSS
  css: ["~/assets/main.scss"],

  // Plugins
  plugins: ["~/plugins/axios"],

  // Nuxt modules
  modules: ["@nuxtjs/axios"],

  // Build configuration
  build: {
    extend(config, ctx) {
      // Custom webpack configuration
    },
  },
};

Static Site Generation (generate) ​

If you don't need real SSR and just want static HTML (blog use case):

bash
npm run generate

Nuxt will request all pages at build time and generate static HTML files that can be deployed to a CDN.

javascript
// nuxt.config.js - dynamic routes need to be declared for static generation
module.exports = {
  generate: {
    routes: async () => {
      const { data } = await axios.get("/api/posts");
      return data.map((post) => `/posts/${post.id}`);
    },
  },
};

Pitfall: window is not defined ​

In an SSR environment (Node.js) there is no window object — using it directly will throw:

javascript
// ❌ This will throw on the server
mounted() {
  // mounted only runs on the client, this is safe ✅
  window.addEventListener('resize', this.handleResize)
}

// ❌ This will throw
asyncData() {
  const width = window.innerWidth  // window doesn't exist on the server!
}

For libraries that can only be used on the client (e.g. DOM-manipulating libraries):

javascript
// nuxt.config.js
plugins: [
  { src: "~/plugins/some-plugin", ssr: false }, // client-only
];

Summary ​

Nuxt.js wraps SSR complexity very well and the convention-based file routing makes it easy to get started. asyncData is the core — understanding that it runs on both server and client is key. It's best suited for content-heavy sites that need SEO, such as e-commerce home pages.

MIT Licensed