// Tutorial //

How To Use Server-Side Rendering with Nuxt.js

Published on February 16, 2022
Default avatar
By Dave Berning
Developer and Author
How To Use Server-Side Rendering with Nuxt.js

The author selected Open Sourcing Mental Illness to receive a donation as part of the Write for DOnations program.

Introduction

Single Page Applications (SPA) are applications that render as a single index.html page on the client side. Traditionally, SPAs contain very little HTML on load; instead, a framework like Vue.js allows you to inject content into an HTML wrapper via JavaScript when certain conditions are met. Though this makes for a dynamic and customizable application, it also causes issues for Search Engine Optimization (SEO) because web crawlers can only analyze files coming from the server, and all of the important HTML on dynamically generated routes is never analyzed.

Nuxt.js is a framework for Vue.js applications that can solve this problem with server-side rendering, a strategy that renders the application on the server then sends it to the client. With Nuxt, all of your routes are generated from .vue files in the pages directory. Inside of these pages you can fetch data from a REST API and inject it into the template of your component. All of the data and HTML inside of the template is rendered by the Nuxt server, which sends a generated .html file to the client. Since that .html is provided by the server, web crawlers can analyze all of the important HTML in any route.

In this tutorial, you will set up a sample web application with information about airports. The app will serve an About page and a dynamically generated page for each airport in the data set. You will then use Nuxt.js to generate page routes, create layout components, and set page-specific metadata.

Prerequisites

Step 1 — Installing Nuxt in the Example Application

In this step, you are going to create a Nuxt project via a command in your computer’s terminal. This npx command will run an external script that will generate your new Vue.js project according to the information you provide in prompts. This is similar to the vue create command when generating a new Vue project with the Vue CLI. You will then set up your example application, which you will use to test out Nuxt features later in the tutorial.

In your terminal, run the following npx command. This command will generate a project in the directory you are currently working in:

  1. npx create-nuxt-app favorite-airports

If this is your first time running create-nuxt-app, a prompt will display with a list of required packages to install first. Type y and hit ENTER to continue:

Output
Need to install the following packages: create-nuxt-app Ok to proceed? (y) y

Once the required packages are installed, you will encounter the first prompt:

Output
create-nuxt-app v4.0.0 ✨ Generating Nuxt.js project in favorite-airports ? Project name: (favorite-airports)

Type in your preferred name for your app, or press ENTER to select the default. Continue to work through the prompts individually, selecting the highlighted options in the following block:

Output
✨ Generating Nuxt.js project in favorite-airports ? Project name: favorite-airports ? Programming language: JavaScript ? Package manager: Npm ? UI framework: None ? Nuxt.js modules: Axios - Promise based HTTP client ? Linting tools: None ? Testing framework: None ? Rendering mode: Universal (SSR / SSG) ? Deployment target: Server (Node.js hosting) ? Development tools: None ? What is your Github username? ? Version control system: Git

When that is complete, change directory into favorite-airports and start the local development server:

  1. cd favorite-airports
  2. npm run dev

This will start up a local server that you can visit at the address provided. In your browser, go to http://localhost:3000/. You will see the following:

The Nuxt.js default splash screen

Now that your project is runnning, you can get the example app set up by adding an array of objects that you will use as data in this tutorial.

Open your terminal and run the following command in the project root directory (favorite-airports):

  1. mkdir data

This will create the data directory. In your text editor of choice, create and open a file named airports.js in the data directory and add in the following:

favorite-airports/data/airports.js
export default [
  {
    name: 'Cincinnati/Northern Kentucky International Airport',
    abbreviation: 'CVG',
    city: 'Hebron',
    state: 'KY'
  },
  {
    name: 'Seattle-Tacoma International Airport',
    abbreviation: 'SEA',
    city: 'Seattle',
    state: 'WA'
  },
  {
    name: 'Minneapolis-Saint Paul International Airport',
    abbreviation: 'MSP',
    city: 'Bloomington',
    state: 'MN'
  },
  {
    name: 'Louis Armstrong New Orleans International Airport',
    abbreviation: 'MSY',
    city: 'New Orleans',
    state: 'LA'
  },
  {
    name: 'Chicago O\'hare International Airport',
    abbreviation: 'ORD',
    city: 'Chicago',
    state: 'IL'
  },
  {
    name: 'Miami International Airport',
    abbreviation: 'MIA',
    city: 'Miami',
    state: 'FL'
  }
]

This is an array of objects consisting of a few airports in the United States. You will use this data to dynamically generate pages for your app.

Save and close this file.

Next, you will import the CSS framework Tailwind to add styling to your application. To do this, open up the Nuxt configuration file nuxt.config.js in your editor and add the following highlighted code:

favorite-airports/nuxt.config.js
export default {
  // Global page headers: https://go.nuxtjs.dev/config-head
  head: {
    title: 'favorite-airports',
    htmlAttrs: {
      lang: 'en'
    },
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
      { name: 'format-detection', content: 'telephone=no' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      { rel: 'stylesheet', type: 'text/css', href: 'https://cdn.jsdelivr.net/npm/tailwindcss@2.1.2/dist/tailwind.min.css' }
    ]
  },
...
}

Save and close the configuration file.

You have now installed Nuxt, generated your project, and set it up to try out Nuxt features. In the next step, you are going to create different types of routes automatically by placing .vue files within the pages directory.

Step 2 — Generating Routes via the pages Directory

Unlike in a traditional Vue.js app, routes in Nuxt are not generated via a router file. Instead, they are generated from the pages directory. Each .vue file in this directory will generate a route. In this step, you will try out this functionality by making an About page and dynamically generating pages for each airport.

Take a look at your pages directory. There is already a page in there: index.vue. This is equivalent to having a static index.html page. This index.vue page contains a single <Tutorial /> component that holds the HTML that rendered in your browser when you ran the local development server. This appears as follows:

favorite-airports/pages/index.vue
<template>
  <Tutorial />
</template>

<script>
export default {
  name: 'IndexPage'
}
</script>

Next, you will expand on this by creating your own static Nuxt route. In your terminal, create and open a file called about.vue in the pages directory. The name of the file before the extension will be the URL path, so the URL in this case would be your_site/about. Inside of this about.vue file, add the following:

favorite-airports/pages/about.vue
<template>
  <div class="container mx-auto my-5">
    <h1 class="text-2xl leading-7 font-semibold">This is the About Page</h1>
    <p>This page contains the about information of this application. This is a static route created by Nuxt via the `about.vue` file in the pages directory</p>
  </div>
</template>

<script>
export default {
  name: 'AboutPage'
}
</script>

On this page, you created an <h1> header and a <p> element within a <div>. You then added styling to each element with Tailwind classes, setting the font and margins. In the <script> element, you added a name property to identify this page in your Vue app.

Save this file and navigate to http://localhost:3000/about. You will see the HTML you wrote rendered in the browser:

The About page created by the user with Nuxt, with a "This is the About Page" header and paragraph content.

Now that you’ve made a static route, you can move on to creating a dynamic route. This route will have parameters that you can leverage later in the .vue file. The URL structure will be /airport/:code, with :code replaced with the airport code in the data set.

In Vue Router, you might have an object like this in your routes array:

const routes = [
  { path: '/airport/:code', component: AirportDetail }
]

But as illustrated earlier, routes in Nuxt are generated via the pages directory. To create a dynamic route, the .vue file must start with an underscore (_). The name of the file following the underscore will be the parameter name. This is comparable to :code in /airports/:code.

In your terminal, create a new directory named airport under pages:

  1. mkdir pages/airport

In the airport directory, create a file named _code.vue in your text editor and add the following:

favorite-airports/pages/airport/_code.vue
<template>
  <div class="container mx-auto my-5">
    <h1 class="text-2xl leading-7 font-semibold">This is the Airport Detail Page</h1>
    <p>This page contains the specific information about an airport.</p>
  </div>
</template>

<script>
export default {
  name: 'AirportDetailPage'
}
</script>

In this file, you are adding a styled <h1> and <p> to the page again, but this time it contains placeholder information specific to an airport. Save and close the file.

Open your browser to http://localhost:3000/airport/cvg and you will find the following:

The Airport Detail page, with the h1 header and paragraph element written in the Vue code.

With this page created, you can now fetch data where the airport.abbreviation property matches the code parameter in your pages directory. In Nuxt, there are two additional hooks or lifecycle methods relevant here: fetch and asyncData. You can use the fetch method to make network calls before the page is rendered. The asyncData method is used when shaping reactive data before the page is rendered.

Any reactive data that is inside of asyncData will be merged into data in that page. As an example, take the following code:

export default {
  data() {
    return {
      lastName: 'Grohl'
    }
  },
  asyncData() {
    const firstName = 'Dave'
  }
}

This would be the same as writing the following:

export default {
  data() {
    return {
      firstName: 'Dave',
      lastName: 'Grohl'
    }
  },
}

The only difference is that asyncData is rendered on the server, while data is rendered on the client.

To try out asyncData, create a filter loop that returns an object from the airports.js data set:

favorite-airports/pages/airport/_code.vue
...
<script>
import airports from '~/data/airports.js'

export default {
  name: 'AirportDetailPage',
  asyncData ({ route }) {
    const airport = airports.filter(airport => airport.abbreviation === route.params.code.toUpperCase())[0]

    return {
      airport
    }
  }
}
</script>

In this code, you are importing the airports.js data into the page, then filtering out every object except where the abbreviation and route.params.code match. Since this is done in the asyncData hook, airport is now a data property you can use in your template.

To use the data, add the following highlighted line to the template section of the same file:

favorite-airports/pages/airport/_code.vue
<template>
  <div class="container mx-auto my-5">
    <h1 class="text-2xl leading-7 font-semibold">This is the Airport Detail Page</h1>
    <p>This page contains the specific information about the <strong>{{ airport.abbreviation }}</strong> airport.</p>
  </div>
</template>
...

Save this file to render the airport abbreviation dynamically. If you open your browser window to http://localhost:3000/airport/cvg, you will now find the airport code in the template. Try visiting an airport route with a different code such as /airport/sea to see that data updated.

Now that you have created static and dynamic routes in Nuxt, you can move on to displaying more data properties for these airports.js objects. In the next section, you will create layouts in Nuxt and assign them to a page. Once that is done, you will leverage what you’ve done in this step to add more useful data into the layout.

Step 3 — Creating Nuxt Layouts

In Nuxt, you can create layouts for your pages to make your application more modular. This is a similar process to creating layout components in a traditional Vue.js app, except in Nuxt, you assign a layout via the layout property on the page level. In this step, you will create a layout to reuse for each of your dynamically generated airport detail pages.

To create a layout, first make a layouts directory in your project:

  1. mkdir layouts

Create and open a file named AirportDetail.vue in this directory. In this layout, you will automatically add a sidebar with information about the airport. To do this, add the following code to the file:

favorite-airports/layouts/AirportDetail.vue
<template>
  <div>
    <Nuxt />
  </div>
</template>

<script>
import airports from '~/data/airports.js'

export default {
  computed: {
    airport() {
      return airports.filter(airport => airport.abbreviation === this.$route.params.code.toUpperCase())[0]
    }
  }
}
</script>

In this code, you are importing the airports.js file into the layout and adding a computed property to get the airport object via the code parameter in the router. The <Nuxt /> element in the template is where the page’s content gets injected. This acts similar to a Vue slot.

Next, you will leverage some Tailwind classes. Add the following to set the layout and styling:

favorite-airports/layouts/AirportDetail.vue
<template>
  <div class="container mx-auto grid grid-cols-9 mt-5 gap-5">
    <aside class="col-span-2">
      <!-- sidebar -->
    </aside>
    <main class="col-span-7">
      <!-- main content -->
      <Nuxt />
    </main>
  </div>
</template>
...

In this snippet, you are wrapping everything into a container that is also a 3-column wide grid. The aside element is the sidebar and the main element is your main content.

With this grid created, add the sidebar with a few Tailwind classes:

favorite-airports/layouts/AirportDetail.vue
<template>
  <div class="container mx-auto grid grid-cols-9 mt-5 gap-5">
    <aside class="col-span-2">
      <!-- sidebar -->
      <div class="shadow-md shadow-black mt-5 border p-5 rounded">
        
      </div>
    </aside>
    <main class="col-span-7">
      <!-- main content -->
      <Nuxt />
    </main>
  </div>
</template>
...

This will add a few styles, including a box-shadow, border, border-radius, padding, and margin-top. Inside of this sidebar, populate some information:

favorite-airports/layouts/AirportDetail.vue
<template>
  <div class="container mx-auto grid grid-cols-9 mt-5 gap-5">
    <aside class="col-span-2">
      <!-- sidebar -->
      <div class="shadow-md shadow-black mt-5 border p-5 rounded text-center">
        <p class="text-3xl font-bold">{{ airport.abbreviation }}</p>
        <p>An airport located in {{ airport.city }}, {{ airport.state }}</p>
      </div>
    </aside>
    <main class="col-span-7">
      <!-- main content -->
      <Nuxt />
    </main>
  </div>
</template>
...

This code adds some airport-specific details to the page and centers the text.

Before you move on, add an <h1> with the airport.name as its value. This will be above the <Nuxt /> element, since it will display on every page with this layout:

favorite-airports/layouts/AirportDetail.vue
<template>
  <div class="container mx-auto grid grid-cols-9 mt-5 gap-5">
    <aside>...</aside>
    <main class="col-span-7">
      <!-- main content -->
      <h1 class="text-3xl font-bold mt-5">{{ airport.name }}</h1>
      <Nuxt />
    </main>
  <div>
</template>
...

You now have a complete layout for your AirportDetail pages. Save and close the layout file.

In order to use this layout, you will next assign the layout: AirportDetail property to the page you want to use it on. Open up pages/airport/_code.vue in your text editor, then add the following highlighted lines:

favorite-airports/pages/airport/_code.vue
...
<script>
import airports from '~/data/airports.js'

export default {
  name: 'AirportDetailPage',
  asyncData ({ route }) {
    const airport = airports.filter(airport => airport.abbreviation === route.params.code.toUpperCase())[0]

    return {
      airport
    }
  },
  layout: 'AirportDetail'
}
</script>

With that complete, save this file and open your browser to http://localhost:3000/airport/cvg. You will now find the following:

The airport detail page with the layout implemented.

Note: The hot reloading does not always automatically implement this change. If the layout is not rendered in your browser, stop your development server with CTRL+C, then run npm run dev again to restart the server.

With that complete, the sidebar will automatically appear with information when you visit a route with a proper airport code.

In this step, you created a layout and assigned it to a page, enabling you to keep your app DRY. Next, you will use the context object to provide page-specific properties.

Step 4 — Using Server context with Page-Specific Properties

Since Nuxt applications are rendered on the server, there will be times when it may not be obvious how to access a page’s route, params, and even the Vuex store. Since it does not have access to the whole Vue instance via this on the server side, Nuxt provides a context object that you can use for these various properties in the fetch and asyncData hooks. In this step, you will use context to add a dynamic page description to your airport pages.

In an earlier section of this tutorial, you already used the context object in the _code.vue file:

favorite-airports/pages/airport/_code.vue
<script>
  export default {
    asyncData ({ route }) {
      const airport = airports.filter(airport => airport.abbreviation === route.params.code.toUpperCase())[0]

      return {
        airport
      }
    },
    layout: 'AirportDetail'
  }
</script>

Here you deconstructed the route property from the context object because that was the only property you needed. But you could also give it a standard name, such as context or ctx:

favorite-airports/pages/airport/_code.vue
<script>
  export default {
    asyncData (context) {
      const airport = airports.filter(airport => airport.abbreviation === context.route.params.code.toUpperCase())[0]

      return {
        airport
      }
    },
    layout: 'AirportDetail'
  }
</script>

In addition to the context object, the fetch method, and the asyncData method, pages also have additional properties that can only be accessed on the page level, most notably the head property. With this property, you can inject meta information into the <head> of the DOM (Document Object Model) when the page is rendered. Information in this property will be readable by the web server.

To set the description of your page, open the _code.vue file in your text editor:

favorite-airports/pages/airport/_code.vue
<script>
  export default {
    ...,
    head() {
      return {
        title: 'Airport Information | Aiport App',
        meta: [
          {
            hid: 'description',
            name: 'description',
            content: 'Detailed information about the specific airport.'
          }
        ]
      }
    }
  }
</script>

In this code, head is a function that returns an object. You added a title property to this object and a meta property with an array of objects defining the description property. This is eqivalent to writing the following in a static .html page:

<head>
  <title>Airport Information | Aiport App</title>
  <meta hid="description" name="description" content="Detailed information about the specific airport.">
</head>

Save your _code.vue file and open it in your browser. The rendered DOM will now have the information you added:

The rendered DOM with <head> element information

You can also make the title and description dynamic by using string interpolation to inject the current airport information in the <head> element:

<script>
  export default {
    head () {
    return {
      title: `${this.airport.name} Information | Aiport App`,
        meta: [
          {
            hid: 'description',
            name: 'description',
            content: `Detailed information about the ${this.airport.name} (${this.airport.abbreviation}) airport.`
          }
        ]
      }
    }
  }
</script>

Now when you save and refresh your browser window, you will find airport-specific information injected into the <head>, which will be useful for your application’s search engine optimization (SEO).

Conclusion

As demostated in this tutorial, Nuxt provides you the tools to dynamically render a page on the server side, configure your SEO with the <head> property, and modularize your code with the layout property. It also allows you to define which data is rendered on the server with asyncData and fetch, or on the client with data.

To learn more about Nuxt’s functionality, visit the official Nuxt documentation. For more tutorials on Vue, check out the How To Develop Websites with Vue.js series page.


Want to learn more? Join the DigitalOcean Community!

Join our DigitalOcean community of over a million developers for free! Get help and share knowledge in our Questions & Answers section, find tutorials and tools that will help you grow as a developer and scale your project or business, and subscribe to topics of interest.

Sign up
About the authors
Default avatar
Developer and Author

I’m a software engineer from Cincinnati. I work on TypeScript apps with Vue.js. Currently a Senior Front-End Engineer at Enodo, based in Chicago.


Default avatar
Senior Technical Editor

Editor at DigitalOcean, fiction writer and podcaster elsewhere, always searching for the next good nautical pun!


Still looking for an answer?

Was this helpful?
Leave a comment