The author selected Open Sourcing Mental Illness to receive a donation as part of the Write for DOnations program.
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.
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:
- 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:
OutputNeed 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:
Outputcreate-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:
- cd favorite-airports
- 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:
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
):
- 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:
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:
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.
pages
DirectoryUnlike 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:
<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:
<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:
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
:
- mkdir pages/airport
In the airport
directory, create a file named _code.vue
in your text editor and add the following:
<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:
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:
...
<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:
<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.
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:
- 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:
<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:
<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:
<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:
<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:
<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:
...
<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:
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.
context
with Page-Specific PropertiesSince 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:
<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
:
<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:
<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:
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).
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!