Server-Side Rendering with Nuxt

Nuxt is inspired by a popular React project named Next.js, built by Zeit. Both projects have the aim of creating applications that allow for a better development experience using the latest ideologies, tools, and techniques. Nuxt recently entered version 1.x and onward, meaning that it should be considered stable to use for production websites.

We'll be taking a look at Nuxt in more detail throughout this chapter, and if you find it useful, it may become the default way that you create Vue applications.

In this chapter, we'ill cover the following topics:

  • Investigating Nuxt and understanding the benefits of using it
  • Creating an application with Nuxt
  • Using Nuxt middleware
  • Using layouts to define content
  • Understanding routing within Nuxt
  • Building a Vue project with Server-Side Rendering
  • Building a Vue project as a static site

Nuxt

Nuxt introduces the concept of Universal Vue Applications, as it allows us to take advantage ofServer-Side Rendering(SSR) with ease. At the same time, Nuxt also gives us the ability to generate static sites, which means that the content is rendered as HTML, CSS, and JS files without going backward and forward from the server.

That's not all—Nuxt handles route generation and doesn't detract from any core features of Vue. Let's create a Nuxt project.

Creating a Nuxt project

We can use Vue CLI to create a new Nuxt project using the starter template. This provides us with a barebones Nuxt project and saves us from having to configure everything manually. We'll be creating a "recipe list" application named "Hearty Home Cooking" that uses a REST API to get categoryandrecipe names. Run the following command in your Terminal to create a new Nuxt project:

# Create a new Nuxt project
$ vue init nuxt-community/starter-template vue-nuxt

# Change directory
$ cd vue-nuxt

# Install dependencies
$ npm install

# Run the project in the browser
$ npm run dev

The preceding steps are quite similar to what we've come to expect when creating a new Vue project, instead, we can simply use the Nuxt repository and starter template to generate a project.

If we take a look at ourpackage.json, you'll see that we don't have a list of production dependencies; instead, we just have one,nuxt:

"dependencies": {


  "nuxt": "^1.0.0"


}

This is important, as this means we don't have to manage the version of Vue or worry about other compatible packages since we only need to update the version ofnuxt.

Directory structure

If we open our project up inside the editor, we'll note that we have substantially more folders than our previous Vue applications. I've compiled a table that outlines what they mean:

Folder Description
Assets Used to store project assets, such as uncompiled images, js, and CSS. Uses Webpack loaders to load as modules.
Components Used to store application components. These are not converted to routes.
Layouts Used to create application layouts, such as default, error, or other custom layouts.
Middleware Used to define custom application middleware. This allows us to run the custom functionality on different events, such as navigating between pages.
Pages Used to create components (the.vuefile) that serve as application routes.
Plugins Used to register application-wide plugins (that is, withVue.use).
Static Used to store static files; each item inside this folder is mapped to/*instead of/static/*.
Store Used with the Vuex store. Both the standard and module implementations of Vuex can be used with Nuxt.

Although this may seem more complex, keep in mind that this helps us separate our concerns, and the structure allows Nuxt to handle things such as autoroute generation.

Nuxt configuration

Let's add some custom links to our project so that we can take advantage of CSS libraries, fonts, and more. Let's add Bulma to our project.

Bulma is a CSS framework that allows us to build applications with Flexbox and lets us take advantage of many premade components. We can add it (and other external CDN files) to our project by navigating tonuxt.config.jsand adding a new object to ourlinkobject within theheadobject, like so:

head: {
  // Omitted
  link: [
    { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
    {
      rel: 'stylesheet',
      href:
    'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.1/css/bulma.min.css',
    },
  ],
}

If we then use the developer tools to check the head inside of our HTML document, you'll note that Bulma has been added to our project. If we head over to our developer tools we can see that it does indeed use Bulma within the project:

Navigation

Each time we create a new.vuefile inside the pages directory, we're given a new route for our application. This means that any time we want to create a new route, we just create a new folder with the route name and the rest is handled by Nuxt. Given that we have defaultindex.vuein ourpagesfolder, the routes initially look like this:

routes: [
  {
    name: 'index',
    path: '/',
    component: 'pages/index.vue'
  }
]

If we then add acategoriesfolderwith anindex.vueinside, Nuxt would generate the following routes:

routes: [
  {
    name: 'index',
    path: '/',
    component: 'pages/index.vue'
  },
  {
    name: 'categories',
    path: '/categories',
    component: 'pages/categories/index.vue'
  }
]

If we want to take advantage of dynamic route parameters, such as anid, we can make a component named_id.vueinside thecategoriesfolder. This automatically creates a route with theidparameter, allowing us to take action based on a user's selection:

routes: [
  {
    name: 'index',
    path: '/',
    component: 'pages/index.vue'
  },
  {
    name: 'categories',
    path: '/categories',
    component: 'pages/categories/index.vue'
  },
  {
    name: 'categories-id',
    path: '/categories/id',
    component: 'pages/categories/_id.vue'
  }
]

Navigating between routes

How do we navigate between routes with Nuxt? Well, we do so using thenuxt-linkcomponent, of course!

This is similar to therouter-linkcomponent that's used when navigating between links with a standard Vue.js application (and as of writing, it is identical to it), but this is wrapped with thenuxt-linkcomponent to take advantage of features, such as prefetching, in the future.

Layouts

We can create custom layouts inside our Nuxt project. This allows us to change the way our pages are arranged and also allows us to add commonalities, such as static navigation bars and footers.Let's use Bulma to create a new navigation bar that allows us to navigate between multiple components within our site.

Inside thecomponentsfolder, make a new file calledNavigationBar.vueand give it the following markup:

<template>
  <nav class="navbar is-primary" role="navigation" aria-label="main 
  navigation">
    <div class="navbar-brand">
      <nuxt-link class="navbar-item" to="/">Hearty Home Cooking</nuxt-
      link>
    </div>
  </nav>
</template>

<script>
export default {}
</script>

We then need to add this to our default layout insidelayouts/default.vue. I've also enclosed thenuxttag (that is, our mainrouter-view) with appropriate Bulma classes to center our content:

<template>
  <div>
    <navigation-bar></navigation-bar>
    <section class="section">
      <nuxt class="container"/>
    </section>
  </div>
</template>

<script>
import NavigationBar from '../components/NavigationBar'

export default {
  components: {
    NavigationBar
  }
}
</script>

If we then head to the browser, we have an application that looks like this, reflecting our code:

The Mock REST API

Before we create the components to display our data, let's mock out a REST API with JSON Server. To do this, we'll need a file nameddb.jsoninside the root of our project, as follows:

{


  "recipes": [


    { "id": 1, "title": "Blueberry and Chocolate Cake", "categoryId": 1, "image": "https://static.pexels.com/photos/291528/pexels-photo-291528.jpeg" },


    { "id": 2, "title": "Chocolate Cheesecake", "categoryId": 1, "image": "https://images.pexels.com/photos/47013/pexels-photo-47013.jpeg"},


    { "id": 3, "title": "New York and Berry Cheesecake", "categoryId": 1, "image": "https://images.pexels.com/photos/14107/pexels-photo-14107.jpeg"},


    { "id": 4, "title": "Salad with Light Dressing", "categoryId": 2, "image": "https://static.pexels.com/photos/257816/pexels-photo-257816.jpeg"},


    { "id": 5, "title": "Salmon Slices", "categoryId": 2, "image": "https://static.pexels.com/photos/629093/pexels-photo-629093.jpeg" },


    { "id": 6, "title": "Mushroom, Tomato and Sweetcorn Pizza", "categoryId": 3, "image": "https://static.pexels.com/photos/7658/food-pizza-box-chalkboard.jpg" },


    { "id": 7, "title": "Fresh Burger", "categoryId": 4, "image": "https://images.pexels.com/photos/460599/pexels-photo-460599.jpeg" }


  ],


  "categories": [


    { "id": 1, "name": "Dessert", "description": "Delcious desserts that range from creamy New York style cheesecakes to scrumptious blueberry and chocolate cakes."},


    { "id": 2, "name": "Healthy Eating", "description": "Healthy options don't have to be boring with our fresh salmon slices and sweet, crispy salad."},


    { "id": 3, "name": "Pizza", "description": "Pizza is always a popular choice, chef up the perfect meat feast with our recipes!"},


    { "id": 4, "name": "Burgers", "description": "Be the king of the party with our flagship BBQ Burger recipe, or make something lighter with our veggie burgers!"}


  ]


}

Next, ensure that you have JSON Server installed on your machine by running the following command in the Terminal:

$ npm install json-server -g

We can then run the server on the3001port(or any portotherthan3000because this is what Nuxt runs on) by typing the following command in the Terminal:

$ json-server --watch db.json --port 3001

This will watch for any changes to our database and update the API accordingly. We'll then be able to make requests tolocalhost:3000/recipes/:idandlocalhost:3000/categories/:id. In Nuxt, we can do this withaxiosandasyncData; let's take a look at that next.

asyncData

We can use theasyncDatamethod to resolve data for our component before the component is loaded, essentially requesting data on the server side and then merging the results with the data object inside our component instance when loaded. This makes it a great place to add asynchronous actions, such as getting data from a REST API.

We'll use theaxioslibrary to create HTTP requests, so we'll need to ensure that we've installed it. Run the following from your Terminal:

$ npm install axios

Then, insidepages/index.vue, we will get a list of categories to show the user when our application starts. Let's do that insideasyncData:

import axios from 'axios'

export default {
  asyncData ({ req, params }) {
    return axios.get(`http://localhost:3001/categories`)
      .then((res) => {
        return {
          categories: res.data
        }
      })
  },
}

Categories

AsasyncDatais merged with our Vue instance's data object, we can then access the data inside of our view. Let's create acategorycomponent that displays a category for each category inside our API:

<template>
  <div class="card">
    <header class="card-header">
      <p class="card-header-title">
        {{category.name}}
      </p>
    </header>
    <div class="card-content">
      <div class="content">
        {{category.description}}
      </div>
    </div>
    <footer class="card-footer">
      <nuxt-link :to="categoryLink" class="card-footer-
      item">View</nuxt-link>
    </footer>
  </div>
</template>

<script>

export default {
  props: ['category'],
  computed: {
    categoryLink () {
      return `/categories/${this.category.id}`
    }
  }
}
</script>

<style scoped>
div {
  margin: 10px;
}
</style>

In the preceding code, we used Bulma to take the category information and placed it on a card. We also used acomputedproperty to generate the prop for thenuxt-linkcomponent. This allows us to navigate the user to a list of items based on categoryid. We can then add this to our rootpages/index.vuefile:

<template>
  <div>
    <app-category v-for="category in categories" :key="category.id" 
    :category="category"></app-category>
  </div>
</template>

<script>
import Category from '../components/Category'
import axios from 'axios'

export default {
  asyncData ({ req, params }) {
    return axios.get(`http://localhost:3001/categories`)
      .then((res) => {
        return {
          categories: res.data
        }
      })
  },
  components: {
    'app-category': Category
  }
}
</script>

As a result, this is what our front page now looks like:

Category detail

In order to navigate the user to thecategorydetail page, we'll need to create a_id.vuefile inside thecategoriesfolder. This will give us access to the ID parameter inside this page. This process is similar to before, except that now we've also added avalidatefunction that checks whether theidparameter exists:

<script>
import axios from 'axios'

export default {
  validate ({ params }) {
    return !isNaN(+params.id)
  },
  asyncData ({ req, params }) {
    return axios.get(`http://localhost:3001/recipes? 
    categoryId=${params.id}`)
      .then((res) => {
        return {
          recipes: res.data
        }
      })
  },
}
</script>

Thevalidatefunction ensures that the parameter exists for this route, and if it doesn't exist, it will navigate the user to an error (404) page. Later on in this chapter, we'll get the hang of how to create our own error pages.

We now have arecipesarray inside ourdataobject that contains recipes based on thecategoryIdthat the user selected. Let's create aRecipe.vuecomponent inside the components folder that displays recipe information:

<template>
  <div class="recipe">
    <div class="card">
      <div class="card-image">
        <figure class="image is-4by3">
          <img :src="recipe.image">
        </figure>
      </div>
      <div class="card-content has-text-centered">
        <div class="content">
          {{recipe.title}}
        </div>
      </div>
    </div>
  </div>
</template>

<script>

export default {
  props: ['recipe']
}
</script>

<style>
.recipe {
  padding: 10px; 
  margin: 5px;
}
</style>

Once again, we're using Bulma for styling and are able to pass a recipe into this component as a prop. Let's iterate over all recipes inside our_id.vuecomponent:

<template>
  <div>
    <app-recipe v-for="recipe in recipes" :key="recipe.id" 
    :recipe="recipe"></app-recipe>
  </div>
</template>

<script>
import Recipe from '../../components/Recipe'
import axios from 'axios'

export default {
  validate ({ params }) {
    return !isNaN(+params.id)
  },
  asyncData ({ req, params }) {
    return axios.get(`http://localhost:3001/recipes?
    categoryId=${params.id}`)
      .then((res) => {
        return {
          recipes: res.data
        }
      })
  },
  components: {
    'app-recipe': Recipe
  }
}
</script>

Whenever we select a category, we now get the following page, which shows the selected recipes:

Error page

What if the user navigates to a route that doesn't exist or there's an error in our application? Well, we certainly could take advantage of Nuxt's default error page, or we could create our own.

We can do that by creatingerror.vueinside thelayoutsfolder. Let's go ahead and do that and display an error message if the status code is404; if not, we'll display a generic error message:

<template>
  <div>
    <div class="has-text-centered" v-if="error.statusCode === 404">
      <img src="https://images.pexels.com/photos/127028/pexels-photo-
      127028.jpeg" alt="">
        <h1 class="title">Page not found: 404</h1>
        <h2 class="subtitle">
          <nuxt-link to="/">Back to the home page</nuxt-link>
        </h2>
    </div>
    <div v-else class="has-text-centered">
      <h1 class="title">An error occured.</h1>
      <h2 class="subtitle">
        <nuxt-link to="/">Back to the home page</nuxt-link>
      </h2>
    </div>
  </div>
</template>

<script>

export default {
  props: ['error'],
}
</script>

If we then navigate tolocahost:3000/e, you'll be navigated to our error page. Let's take a look at the error page:

Plugins

We'll need the ability to add recipes to our application; as adding new recipes will require a form and some input(s) in order to appropriately validate the form, we'll useVuelidate. If you remember from previous chapters, we can addVuelidateand other plugins withVue.use. The process is similar when using Nuxt, but requires an extra step. Let's installVuelidateby running the following command in the Terminal:

$ npm install vuelidate

Inside our plugins folder, make a new file namedVuelidate.js. Inside this file, we can importVueandVuelidateand add the plugin:

import Vue from 'vue'


import Vuelidate from 'vuelidate'




Vue.use(Vuelidate)

We can then updatenuxt.config.jsto add the plugins array, which points toward ourVuelidatefile:

plugins: ['~/plugins/Vuelidate']

Inside thebuildobject, we'll also add'vuelidate'to the vendor bundle so that it's added to our application:

build: {


 vendor: ['vuelidate'],


 // Omitted


}

Adding recipes

Let's make a new route underpages/Recipes/new.vue; this will then generate a route tolocalhost:3000/recipes/new. Our implementation will be simple; for example, having recipe steps asstringmay not be the best idea for production, but it allows us to achieve our goal(s) in development.

We can then add the appropriate data object and validation(s) withVuelidate:

import { required, minLength } from 'vuelidate/lib/validators'

export default {
  data () {
    return {
      title: '',
      image: '',
      steps: '',
      categoryId: 1
    }
  },
  validations: {
    title: {
      required,
      minLength: minLength(4)
    },
    image: {
      required
    },
    steps: {
      required,
      minLength: minLength(30)
    }
  },
}

Next up, we can add the appropriate template, which includes everything from validation messages, to contextual classes, and enabling/disabling thesubmitbutton if the form is valid/invalid:

<template>
  <form @submit.prevent="submitRecipe">
    <div class="field">
      <label class="label">Recipe Title</label>
      <input class="input" :class="{ 'is-danger': $v.title.$error}" v-
      model.trim="title" @input="$v.title.$touch()" type="text">
      <p class="help is-danger" v-if="!$v.title.required && 
      $v.title.$dirty">Title is required</p>
      <p class="help is-danger" v-if="!$v.title.minLength && 
      $v.title.$dirty">Title must be at least 4 characters.</p>
    </div>

    <div class="field">
      <label class="label">Recipe Image URL</label>
      <input class="input" :class="{ 'is-danger': $v.image.$error}" v-
      model.trim="image" @input="$v.image.$touch()" type="text">
      <p class="help is-danger" v-if="!$v.image.required && 
      $v.image.$dirty">Image URL is required</p>
    </div>

    <div class="field">
      <label class="label">Steps</label>
      <textarea class="textarea" rows="5" :class="{ 'is-danger': 
      $v.steps.$error}" v-model="steps" @input="$v.steps.$touch()" 
      type="text">
      </textarea>
      <p class="help is-danger" v-if="!$v.steps.required && 
      $v.steps.$dirty">Recipe steps are required.</p>
      <p class="help is-danger" v-if="!$v.steps.minLength && 
      $v.steps.$dirty">Steps must be at least 30 characters.</p>
    </div>

    <div class="field">
      <label class="label">Category</label>
      <div class="control">
        <div class="select">
          <select v-model="categoryId" @input="$v.categoryId.$touch()">
            <option value="1">Dessert</option>
            <option value="2">Healthy Eating</option>
          </select>
        </div>
      </div>
    </div>

    <button :disabled="$v.$invalid" class="button is-
    primary">Add</button>
  </form>
</template>

To submit the recipe, we'll need to make a POST request to our API:

import axios from 'axios'

export default {
  // Omitted
  methods: {
    submitRecipe () {
      const recipe = { title: this.title, image: this.image, steps: 
      this.steps, categoryId: Number(this.categoryId) }
      axios.post('http://localhost:3001/recipes', recipe)
    }
  },
}

Instead of navigating to thehttp://localhost:3000/recipes/newURL manually, let's add an item to our navigation bar:

<template>
  <nav class="navbar is-primary" role="navigation" aria-label="main navigation">
    <div class="navbar-brand">
      <nuxt-link class="navbar-item" to="/">Hearty Home Cooking</nuxt-
      link>
    </div>
    <div class="navbar-end">
      <nuxt-link class="navbar-item" to="/recipes/new">+ Add New 
      Recipe</nuxt-
     link>
    </div>
  </nav>
</template>

Here's what our page now looks like:

Although we haven't used the recipe steps in our application, I've included it in this example as a feature you may want to include yourself.

Transitions

When navigating between pages, Nuxt makes adding transitions super simple. Let's add atransitionto each navigation action by adding custom CSS. Add a file namedtransition.cssinto theassetsfolder, and we'll hook into the various different page states:

.page-enter-active, .page-leave-active {
  transition: all 0.25s;
}

.page-enter, .page-leave-active {
  opacity: 0;
  transform: scale(2);
}

After adding the file, we'll need to tell Nuxt that we want to use it as a.cssfile. Add the following code to yournuxt.config.js:

 css: ['~/assets/transition.css']

Now, we can navigate between any page and we'll have a page transition each time.

Building for production

Nuxt offers us a variety of ways to build our project for production, such as server-rendered (Universal), static, orSingle Page Application(SPA) mode. All of these offer different pros and cons, depending on the use case.

By default, our project is in server-rendered (Universal) mode and can be built for production by running the following command in the Terminal:

$ npm run build

We then get adistfolderinside the.nuxtfolder within our project; this contains the built end result(s) of our application, which can be deployed to a hosting provider:

Static

In order to build our project in static mode, we can run the following command in the Terminal:

$ npm run generate

This will build a static site, which can then be deployed to a static hosting provider such as Firebase. This is how the Terminal should appear:

SPA mode

To build our project in the SPA mode, we will need to add the following key value tonuxt.config.js:

mode: 'spa'

We can then build our project once again, but this time it'll be built using SPA mode:

$ npm run build

Our command Terminal should now look like the following:

Summary

In this chapter, we discussed how to use Nuxt to create server-rendered Vue applications. We also discussed just_how easy_it is to create new routes and how to add custom CSS libraries inside our project. Furthermore, we covered how we can add transitions to our pages to make it interesting when switching between routes. We also covered how we can build different versions of our project, depending on whether we want a universal, static, or SPA application.

In the final chapter, we'll be discussing common anti-patterns within Vue.js and how to avoid them. This is paramount to writing consistent software that can survive the test of time.

results matching ""

    No results matching ""