Simple Guide to Nuxt.js (Vue.js SSR)

February 14, 2020

Why use Nuxt

I wanted to do Vue.js Server-Side Rendering (SSR). After reading into https://ssr.vuejs.org/ and exploring https://github.com/vuejs/vue-hackernews-2.0/, I found that there are a lot of codes required to piece things together, and decide to explore Nuxt.js. I try to keep to vanilla Vue.js without touching Vuex or Nuxt.js for quite a while (as more frameworks means more learning and more problems to solve), but I guess there is more pros for using Vuex in the case of SSR.

  • Nuxt.js support something called Universal mode, which generate both SSR and client side code.
  • It also support static site generation, like VuePress or Hugo.
  • SSR just work out of the box, though you need to be aware of some SSR caveats while coding.
  • Using Nuxt.js means I won’t be using Vue Cli, but luckily their tooling and hot-reload is pretty good as well.

SSR Caveats

  • For AJAX, use library like axios as it support server and client side fetch. Fetch API work on client-side (browser only).
  • On server-side, reactivity is disabled. It renders the component default state only.
  • Lifecyle Hook: beforeCreate and created is used (beforeMount and mount is executed on client only); Avoid global side effects code (such as setInterval in beforeCreate - created, do them at mounted and destroyed instead)
  • Avoid access to browser API like window and document
  • Some 3rd party library might not be written with universal (server and client-side) usage in mind
  • Probably want to use Webpack for both client and server code, due to vue-loader, file-loader, css-loader, javascript features (Babel), etc.

Setup Nuxt.js Project

npx create-nuxt-app PROJECT
cd PROJECT

NOTE: Previous vue init nuxt-community/starter-template is deprecated.

create-nuxt-app v2.14.0
✨  Generating Nuxt.js project in journey-nuxt
? Project name PROJECT
? Project description My superior Nuxt.js project
? Author name desmond
? Choose the package manager Npm
? Choose UI framework Bootstrap Vue
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios, Progressive Web App (PWA) Support
? Choose linting tools None
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools jsconfig.json (Recommended for VS Code)

NOTE: I setup with Axios, BootstrapVue and Universal (SSR) rendering mode.

Setup Page / Route

Edit pages/index.vue, which is the main home page.

<template>
  <div class="mb-3">
      <h1>{{ title }}</h1>
      <div>{{ description }}</div>
    <button type="button" class="btn btn-primary" @click="showToast">Show Toast</button>
  </div>

  <pagination :total-page="totalPage" :current-page="page" />
</template>

<script>
import Pagination from '~/components/Pagination.vue'

export default {
  head() {
    return {
      title: 'Home',
      meta: [
        { hid: 'description', name: 'description', content: 'This is home page' }
      ]
    }
  },
  async asyncData ({ $axios }) {
    const { data } = await $axios.get('/data/home.json')
    /*
    return {
      title: data.title,
      description: data.desscription
    }
     */
    return data
  },
  data() {
    return {
      page: 1,
      totalPage: 3
    }
  },
  methods: {
    showToast() {
      this.$toast.show('Hello from Toast')
    }
  },
  components: {
    Pagination
  }
}
</script>

asyncData is called before the component is rendered, which return data is merged into components’s data.

head is for page title and other meta tags.

About Page

Edit pages/about.vue

<template>
  <div>
    <h1>About</h1>
  </div>
</template>

<script>
export default {
  head() {
    return {
      title: 'About'
    }
  }
}
</script>

Pages

Page component which receive param id.

Edit pages/page/_id.vue

<template>
  <div>
    <div class="mb-3">
      <h1>Page {{ page }}</h1>
      <div v-for="value in items" :key="value">
        {{ value }}
      </div>
    </div>
    <pagination :total-page="totalPage" :current-page="page" />
  </div>
</template>

<script>
import Pagination from '~/components/Pagination.vue'

export default {
  head() {
    return {
      title: `Page ${this.page}`
    }
  },
  validate ({ params }) {
    // Must be a number
    return /^\d+$/.test(params.id)
  },
  async asyncData ({ $axios, params }) {
    const { data } = await $axios.get(`/data/page-${params.id}.json`)
    return data
  },
  data() {
    return {
      totalPage: 3,
    }
  },
  computed: {
    page() {
      return parseInt(this.$route.params.id)
    }
  },
  components: {
    Pagination
  }
}
</script>

Header / Meta tag

Edit nuxr.config.js.

export default {
  // ..
  head: {
    // title formatting
    titleTemplate: '%s - My App Name',
    meta: [
      { charset: 'utf-8' },
      // for bootstrap
      { name: 'viewport', content: 'width=device-width, initial-scale=1, shrink-to-fit=no' },
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  // ..
}

Static Json File

Edit static/data/home.json

{
  "title": "Home",
  "description": "This is Home"
}

Edit static/data/page-2.json

{
  "title": "List of Fruit",
  "items": ["Apple", "Orage", "Pear"]
}

Edit static/data/page-3.json

{
  "title": "List of Friends",
  "items": ["Mei Ru", "Jack", "John"]
}

Components

Edit components/Pagination.vue

Import this Pagination component.

Replace all <a> with <router-link> and :href with :to.

Layout

This is the default layout used by all pages component.

Edit layouts/default.vue

<template>
  <div>
    <notifications />
    <!-- <notifications group="default" /> -->

    <b-navbar toggleable="lg" type="dark" variant="info">
      <div class="container">
        <b-navbar-brand to="/">APP_NAME</b-navbar-brand>

        <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
        <b-collapse id="nav-collapse" is-nav>
          <b-navbar-nav>
            <b-nav-item to="/about">About</b-nav-item>
          </b-navbar-nav>
        </b-collapse>
      </div>
    </b-navbar>

    <div class="container" style="padding-top: 16px;">
      <nuxt/>
    </div>
  </div>
</template>

<style>
html {
  font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
    Roboto, 'Helvetica Neue', Arial, sans-serif;
  font-size: 16px;
  word-spacing: 1px;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  box-sizing: border-box;
}

*,
*:before,
*:after {
  box-sizing: border-box;
  margin: 0;
}
</style>

WARN: The generated pages/index.vue have a container style which mess up the container CSS of Bootstrap.

Install Vue Plugins

We install a plugin for notification.

npm install --save @nuxtjs/toast

NOTE: Not all vue components are compatible with Nuxt.js/SSR due to the confitions of SSR Caveats mentioned above. Refer to a list of Nuxt.js Modules.

NOTE: Refer to Nuxt.js Install Vue.js Plugins for non-Nuxt modules.

Edit nuxt.config.js.

export default {
  // ..
  modules: [
    // ..
    '@nuxtjs/toast'
  ],
  toast: {
    position: 'top-center',
    duration: 3000
  },
  // ...
}

NOTE: The usage to toast plugin is demonstrated in pages/index.vue as shown above.

Local Development

npm run dev

Access: http://localhost:3000/

Deployment

Refer Deploy Nuxt.js (SSR) to Firebase Cloud Functions

References:

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.