Learn how to create a Nuxt.js application. This is a complete, step-by-step tutorial.
In this post we provide instructions on how to create an application with Nuxt.js and share links to additional sources reviewing some of the topics in more depth. This post begins a series of our articles. Here we will discuss basics of building an app:
- project creation and configuration,
- assets and statics: styles, fonts, images, posts,
- components,
- pages and layouts,
- deployment.
Next time we are going to cover such topics as:
- dark mode,
- multilingual applications,
- PWA and SEO, including
Sitemap
auto-generation androbots.txt
, - setting up Analytics (Google and Yandex) and bug tracker (
Sentry
), - application optimization for passing tests
Lighthouse
/PageSpeed
.
What topics are you interested in? Write comments below, please.
Introduction
Over the recent years, many different frameworks for building static websites by using pre-rendering have emerged. Today we are going to talk about one of them — Nuxt.js. This open source framework is built on top of another well known frontend framework, Vue.js, but we think it has a number of distinct advantages.
To back up our point of view we decided to build a real application from scratch using Nuxt.js. Look what we got! With our step-by-step instruction, you can repeat this experiment yourself. Please note that to fully understand the article, you will need basic knowledge of working with Vue.js. Good luck!
Briefly about Nuxt.js
Nuxt
is a high-level framework based on Vue
. With Nuxt.js you can develop out-of-the-box isomorphic web applications by abstracting away the details of the server and client code distribution. With this approach, we save time and can focus on the development.
The key advantages of Nuxt
:
-
SPA, SSR
and pre-rendering are already configured; the only thing we need to do is to choose.
In this application we use a pre-rendering for the product-mode. It means that we generate all the pages of a web site in advance and then deploy to hosting for transmitting static data.
- Great
SEO
for all search engines as the result of usingSSR
or pre-renderer. - Quick interaction with the site compared to static websites. It is achieved by loading only the necessary
js chunks
,css styles
andAPI
requests (most of the process is automated bywebpack 4
working under the hood ofNuxt
). - Excellent
Google Lighthouse
/Page Speed
performance. With the right configuration, you can get 100/100 even on a weak server. -
CSS Modules
,Babel
,Postscc
and other nice tools are pre-configured using create-nuxt-app. - Default project structure allows for convenient work in medium-sized and large teams.
- Over 50 ready-to-use modules and the ability to use any packages from the extensive Vue.js ecosystem.
I could go on and on about Nuxt
advantages. This is the framework that I really love for its ease of use and the ability to create flexible and easily scalable applications. So, let’s start and see all the advantages in practice.
Find more information about Nuxt.js on the official website. Detailed guides are also available here.
Design
Well-arranged, ready-made design or, even better, a UI-kit, will make any application development much faster and easier. If you don’t have an available UI-designer at hand, that’s fine. Within the framework of our instruction, we will manage ourselves!
Specifically for this article, I prepared a modern, minimalist blog design with simple functionality, enough to demonstrate Nuxt
performance capabilities.
For development, I used an online tool, Figma
. The design and UI kit are available via this link. You can copy this template and use it in your project.
Creating a project
To create a project, we will use the create-nuxt-app utility from Nuxt
developers, which allows configuring the application template with cli
.
Initialize a project and state its name:
npx create-nuxt-app nuxt-blog
Further, the utility will offer to choose a set of preferred libraries and packages over a few steps, after that it will independently download, adjust and configure them for the project.
You can see a complete list of the selected options on Github.
For this project the configuration with
Typescript
will be used. When developing inVue
withTypescript
, you can use twoAPIs
: Options API or Class API. They have the same functionality, but different syntax. Personally, I preferOptions API
syntax, that’s why it will be used in our project.
After creating the project, we can run our application using the command: npm run dev
. It will now be available on localhost: 3000
.
Nuxt
uses a webpack-dev-server with installed and configured HMR as a local server, which makes development fast and convenient.
Since we are creating a demo version of an application, I will not write tests for it. But I highly recommend not to neglect application testing in commercial development.
If you are new to this topic, I advise you to look at Jest — a very simple but powerful tool that supports working with Nuxt in combination with vue-test-utils.
The project structure
Nuxt creates a default directory and file structure suitable for a quick development start.
-- Assets
-- Static
-- Pages
-- Middleware
-- Components
-- Layouts
-- Plugins
-- Store
-- nuxt.config.js
-- ...other files
This structure is perfectly suitable for our project, so we will not change it.
You can read more about the purpose of different directories on the Nuxt website.
Building an application
Before writing the code, let’s do the following:
1) Delete the starter components and pages created by Nuxt
.
2) Install pug
and scss
to make development more convenient
and faster. Run the command:
npm i --save-dev pug pug-plain-loader node-sass sass-loader fibers
After that, the lang
attribute will become available for the template
and style
tags.
<template lang="pug"></template>
<style lang="scss"></style>
3) Add support for deep selector ::v-deep
to the stylelint
configuration, which will allow you to apply styles to child components, ignoring scoped
. You can read more about this selector here.
{
rules: {
'at-rule-no-unknown': null,
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: ['v-deep'],
},
],
},
}
All preparations are over, let’s go to the next step.
Posts
Posts will be stored in the content/posts
, directory, that we will create at the root of the project as a set of markdown
files.
Let's create 5 small files, so that we can start working with them right away. To make it simple, name them 1.md
, 2.md
, etc.
In the content
directory, create a Posts.d.ts
file, where we define the types for the object containing all the necessary information about the post:
export type Post = {
id: number
title: string
desc: string
file: string
img: string
}
I think the meanings of the fields should be clear from their names.
Moving on. In the same directory, create another file called posts.ts
with the following content:
import { Post } from './Post'
export default [
{
id: 1,
title: 'Post 1',
desc:
'A short description of the post to keep the user interested.' +
' Description can be of different lengths, blocks are aligned' +
' to the height of the block with the longest description',
file: 'content/posts/1.md',
img: 'assets/images/1.svg',
},
...
{
id: 5,
title: 'Post 5',
desc:
'A short description of the post to keep the user interested.' +
' Description can be of different lengths, blocks are aligned' +
' to the height of the block with the longest description',
file: 'content/posts/5.md',
img: 'assets/images/5.svg',
},
] as Post[]
In the img
property, we refer to images in the assets/images
directory, but we haven't created this directory yet, so let's do it now.
Now, let's add images in .svg
format to the created directory with the names that we specified above.
I will take 5 images from unDraw. This great resource is constantly updated and contains many free svg
images.
Now that everything is ready, the content
directory should look like this:
content/
-- posts.ts
-- Posts.d.ts
-- posts/
---- 1.md
---- 2.md
---- 3.md
---- 4.md
---- 5.md
And in the assets
directory, the images
subdirectory should have appeared with the following content:
assets/
-- images/
---- 1.svg
---- 2.svg
---- 3.svg
---- 4.svg
---- 5.svg
...
Dynamic file generation
Since we will get images and files with post texts dynamically, it’s necessary to implement a global mixin, which we can use further in all components.
To do this, create a mixins
subdirectory in the plugins
directory, and in the subdirectory create a getDynamicFile.ts
file with the following content:
import Vue from 'vue'
export const methods = {
getDynamicFile(name: string) {
return require(`@/${name}`)
},
}
Vue.mixin({
methods,
})
All we need to do now is enable this mixin in the nuxt.config.js
file:
{
plugins: [
'~plugins/mixins/getDynamicFile.ts',
],
}
Fonts
After creating posts let’s enable fonts. The easiest way is to use the wonderful Webfontloader library, which allows you to get any font from Google Fonts. However, in commercial development proprietary fonts are used more often, so let's look at such a case here.
As the font for our application I chose Rubik
, which is distributed under the Open Font License. It can also be downloaded from Google Fonts.
Please note that in the downloaded archive the fonts will be in the otf
format, but since we are working with the web
, the woff
and woff2
formats will be our best choice.They have smaller size than other formats, but they are fully supported in all modern browsers. To convert otf
to the necessary formats, you can use one of the many free online services.
So, we have the fonts in the needed formats, now it's time to add them to the project. To do this, create a fonts
subdirectory in the static
directory and add the fonts there. Create a fonts.css
file in the same directory; it will be responsible for adding our fonts in the application with the following content:
@font-face {
font-family: "Rubik-Regular";
font-weight: normal;
font-style: normal;
font-display: swap;
src:
local("Rubik"),
local("Rubik-Regular"),
local("Rubik Regular"),
url("/fonts/Rubik-Regular.woff2") format("woff2"),
url("/fonts/Rubik-Regular.woff") format("woff");
}
...
You can see the full contents of the file in the repository.
You should pay attention to two things:
1) We specify font-display: swap;
, defining how the font added via font-face
will be displayed depending on whether it has loaded and is ready to use.
In this case, we do not set a block period and set an infinite swap period. Which means that the font is loaded as a background process without blocking the page loading, and the font will be displayed when ready.
2) In src
, we set the load order by priority.
First, we check if the needed font is installed on the user's device by checking possible variations of the font name. If you don't find it, check if the browser supports the more modern woff2
format, and if not, then use the woff
format. There is a chance that the user has an outdated browser (for example, IE
<9), in this case, we will further specify the fonts built into the browser as a fallback
.
After creating the file with font loading rules, you need to add it in the application — in the nuxt.config.js
file in the head
section:
{
head: {
link: [
{
as: 'style',
rel: 'stylesheet preload prefetch',
href: '/fonts/fonts.css',
},
],
},
}
Note that here, as earlier, we are using the preload
and prefetch
properties, thereby setting high priority in the browser for loading these files without blocking the page rendering.
Let's add favicon
to the static
directory of our application right away, which can be generated using any free online service.
Now the static
directory looks like this:
static/
-- fonts/
---- fonts.css
---- Rubik-Bold.woff2
---- Rubik-Bold.woff
---- Rubik-Medium.woff2
---- Rubik-Medium.woff
---- Rubik-Regular.woff2
---- Rubik-Regular.woff
-- favicon.ico
Moving on to the next step.
Reused styles
In our project, all the used styles are described with a single set of rules, which makes development much easier, so let's transfer these styles from Figma
to the project files.
In the assets
directory, create a styles
subdirectory, where we will store all styles reused in the project. And the styles
directory will contain the variables.scss
file with all our scss
variables.
You can see the contents of the file in the repository.
Now we need to connect these variables to the project so that they are available in any of our components. In Nuxt the @nuxtjs/style-resources module is used for this purpose.
Let’s install this module:
npm i @nuxtjs/style-resources
And add the following lines to nuxt.config.js
:
{
modules: [
'@nuxtjs/style-resources',
],
styleResources: {
scss: ['./assets/styles/variables.scss'],
},
}
Well done! Variables from this file will be available in any component.
The next step is to create a few helper classes and global styles that will be used throughout the application. This approach will allow you to centrally manage common styles and quickly adapt the application if the layout design is changed.
Create a global
subdirectory in the assets/styles
directory with the following files:
1) typography.scss
file will contain all the helper classes to control text, including links.
Note that these helper classes change styles depending on the user's device resolution: smartphone or PC.
2) transitions.scss
file will contain global animation styles, both for transitions between pages, and for animations inside components, if needed in the future.
3) other.scss
file will contain global styles, which can not yet be separated into a specific group.
The page
class will be used as a common container for all components on the page and will form the correct padding on the page.
The .section
class will be used to denote the logical unit boundaries, and the .content
class will be used to restrict the content width and its centering on the page. We will see how these classes are used further when we start implementing components and pages.
4) index.scss
is a common file that will be used as a single export point for all global styles.
You can see the full contents of the file on Github.
At this step, we connect these global styles to make them available throughout the application. For this task Nuxt
has a css
section in the nuxt.config.js
file:
{
css: ['~assets/styles/global'],
}
I should say that in the future,
css
classes will be assigned according to the following logic:1) If a tag has both helper classes and local classes, then the local classes will be added directly to the tag, for example,
p.some-local-class
, and the helper classes will be specified in theclass
property, for example,class = "body3 medium "
.2) If a tag has either helper classes or local classes, they will be added directly to the tag.
I use this approach for my convenience, it helps to visually distinguish between global and local classes right away.
Before the development, let's install and enable reset.css
so that our layout looks the same in all browsers. To do this, we install the required package:
npm i reset-css
And enable it in the nuxt.config.js
file in the already familiar css
section, that will now look like this:
{
css: [
'~assets/styles/global',
'reset-css/reset.css',
],
}
Got it? If you did, we are ready to move on to the next step.
Layouts
In Nuxt
, Layouts are wrapper files for our app that allow you to reuse common components between them and implement the necessary common logic. Since our application is pretty simple, it will be enough for us to use the default layout
- default.vue
.
Also, in Nuxt
a separate layout
is used for an error page like 404
, which is actually a simple page.
Layouts
in the repository.
default.vue
Our default.vue
will have no logic and will look like this:
<template lang="pug">
div
nuxt
db-footer
</template>
Here we use 2 components:
1) nuxt
during the building process it will be replaced with a specific page requested by the user.
2) db-footer
is our own Footer component (we will write it a little later), that will be automatically added to every page of our application.
error.vue
By default, when any error returns from the server in the http
status, Nuxt
redirects to layout/error.vue
and passes an object containing the description of the received error via a prop named error
.
Let's look at the script
section, which will help to unify the work with the received errors:
<script lang="ts">
import Vue from 'vue'
type Error = {
statusCode: number
message: string
}
type ErrorText = {
title: string
subtitle: string
}
type ErrorTexts = {
[key: number]: ErrorText
default: ErrorText
}
export default Vue.extend({
name: 'ErrorPage',
props: {
error: {
type: Object as () => Error,
required: true,
},
},
data: () => ({
texts: {
404: {
title: '404. Page not found',
subtitle: 'Something went wrong, no such address exists',
},
default: {
title: 'Unknown error',
subtitle: 'Something went wrong, but we`ll try to figure out what`s wrong',
},
} as ErrorTexts,
}),
computed: {
errorText(): ErrorText {
const { statusCode } = this.error
return this.texts[statusCode] || this.texts.default
},
},
})
</script>
What is going on here:
1) First, we define the types that will be used in this file.
2) In the data
object, we create a dictionary that will contain all unique error messages for some specific, considerable errors we choose and a default message for all other errors.
3) In the computed errorText
property we check if the received error is in the dictionary. If the error is there, then we return its message. If it’s not, we return the default message.
In this case our template will look like this:
<template lang="pug">
section.section
.content
.ep__container
section-header(
:title="errorText.title"
:subtitle="errorText.subtitle"
)
nuxt-link.ep__link(
class="primary"
to="/"
) Home page
</template>
Note that here we are using the .section
and .content
global utility classes that we have created earlier in the assets/styles/global/other.scss
file. They allow to center the content on the page.
Here the section-header
component is used; it has not yet been created, but later it will be a universal component for displaying headers. We will implement it when we start discussing components.
The layouts
directory look like this:
layouts/
-- default.vue
-- error.vue
Let’s move on to creating components.
Components
Components are the building blocks of our application. Let’s start with the components that we have already mentioned above.
I will not describe the components’ styles not to make this article too long. You can find them in the application repository.
SectionHeader
The headers in our application have the same style, so it makes total sense to use one component to display them and change the displayed data through the props.
Let’s look at the script
section of this component.
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'SectionHeader',
props: {
title: {
type: String,
required: true,
},
subtitle: {
type: String,
default: '',
},
},
})
</script>
Now let’s see what the template will look like:
<template lang="pug">
section.section
.content
h1.sh__title(
class="h1"
) {{ title }}
p.sh__subtitle(
v-if="subtitle"
class="body2 regular"
) {{ subtitle }}
</template>
As we can see, this component is a simple wrapper for the displayed data and does not have any logic.
LinkToHome
The simplest component in our application is the link above the title that leads to the home page from the selected post page.
This component is really tiny, so I will write all its code here (without styles):
<template lang="pug">
section.section
.content
nuxt-link.lth__link(
to="/"
class="primary"
)
img.lth__link-icon(
src="~/assets/icons/home.svg"
alt="icon-home"
)
| Home
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'LinkToHome',
})
</script>
Note that we request the home.svg
icon from the assets/icons
directory. You need to create this directory first and add there the necessary icon.
DbFooter
DbFooter component is very simple. It contains copyright
and a link to generate a letter.
The requirements are clear, so let’s start the implementation from the script
section.
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'DbFooter',
computed: {
copyright(): string {
const year = new Date().getUTCFullYear()
return `© ${year} · All rights reserved`
},
},
})
</script>
In DbFooter there is only one computed property that returns the current year, concatenated with a given string Now let’s look at the template:
<template lang="pug">
section.section
.content
.footer
a.secondary(
href="mailto:example@mail.com?subject=Nuxt blog"
) Contact us
p.footer__copyright(
class="body3 regular"
) {{ copyright }}
</template>
When clicking on the Contact us
link, we will open the native mail client and immediately set the message subject. This solution is suitable for our application demo, but in real life, a more appropriate solution would be to implement a feedback form to send messages directly from the site.
PostCard
Postcard is a pretty simple component without any complexities.
<script lang="ts">
import Vue from 'vue'
import { Post } from '~/content/Post'
export default Vue.extend({
name: 'PostCard',
props: {
post: {
type: Object as () => Post,
required: true,
},
},
computed: {
pageUrl(): string {
return `/post/${this.post.id}`
},
},
})
</script>
In the script
section, we define one post
prop, which will contain all the necessary information about the post.
We also implement the pageUrl
computed property for use in the template, which will return us a link to the desired post page.
The template will look like this:
<template lang="pug">
nuxt-link.pc(:to="pageUrl")
img.pc__img(
:src="getDynamicFile(post.img)"
:alt="`post-image-${post.id}`"
)
p.pc__title(class="body1 medium") {{ post.title }}
p.pc__subtitle(class="body3 regular") {{ post.desc }}
</template>
Note that the root element of the template is nuxt-link
. This is done to enable the user to open the post in a new window using the mouse.
This is the first time that the getDynamicFile
global mixin we created earlier in this article is used.
PostList
The main component on the home page consists of a post counter at the top and a list of posts.
The script
section for this component:
<script lang="ts">
import Vue from 'vue'
import posts from '~/content/posts'
export default Vue.extend({
name: 'PostList',
data: () => ({
posts,
}),
})
</script>
Note that after importing the array of posts, we add them to the data
object so that the template has access to this data in the future.
The template looks like this:
<template lang="pug">
section.section
.content
p.pl__count(class="body2 regular")
img.pl__count-icon(
src="~/assets/icons/list.svg"
alt="icon-list"
)
| Total {{ posts.length }} posts
.pl__items
post-card(
v-for="post in posts"
:key="post.id"
:post="post"
)
</template>
Don't forget to add the list.svg
icon to the assets/icons
directory for everything to work as expected.
PostFull
PostFull
is the main component on a separate post page that displays the post content.
For this component, we need the @nuxtjs/markdownit module, which is responsible for converting md to html.
Let's install it:
npm i @nuxtjs/markdownit
Then let’s add @nuxtjs/markdownit
to the modules
section of the nuxt.config.js
file:
{
modules: [
'@nuxtjs/markdownit',
],
}
Excellent! Let's start implementing the component. As usual, from the script
section:
<script lang="ts">
import Vue from 'vue'
import { Post } from '~/content/Post'
export default Vue.extend({
name: 'PostFull',
props: {
post: {
type: Object as () => Post,
required: true,
},
},
})
</script>
In the script
section, we define one prop post
, which will contain all the necessary information about the post.
Let’s look at the template:
<template lang="pug">
section.section
.content
img.pf__image(
:src="getDynamicFile(post.img)"
:alt="`post-image-${post.id}`"
)
.pf__md(v-html="getDynamicFile(post.file).default")
</template>
As you can see, we dynamically get and render both an image and a .md
file using our getDynamicFile
mixin.
I think you noticed that we use the v-html directive to render the file, since @nuxtjs/markdownit
do the rest. That’s extremely easy!
We can use the ::v-deep
selector to customize styles of rendered .md
file. Take a look on Github to see how this component is made.
In this component, I only set indents for paragraphs to show the principle of customization, but in a real application, you will need to create a complete set of styles for all used and necessary html elements.
Pages
When all the components are ready, we can create the pages.
As you probably already understood from the design, our application consists of a main page with a list of all posts and a dynamic web page that displays the selected post.
Pages
directory structure:
pages/
-- index.vue
-- post/
---- _id.vue
All components are self-contained, and their states are determined through props, so our pages will look like a list of components specified in the right order.
The main page will look like this:
<template lang="pug">
.page
section-header(
title="Nuxt blog"
subtitle="The best blog you can find on the global internet"
)
post-list
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'HomePage',
})
</script>
To set proper indentation, we used the global .page
class we created earlier in assets/styles/global/other.scss
.
A separate post page will look a little more complex. Let's take a look at the script
section first:
<script lang="ts">
import Vue from 'vue'
import { Post } from '~/content/Post'
import posts from '~/content/posts'
export default Vue.extend({
validate({ params }) {
return /^\d+$/.test(params.id)
},
computed: {
currentId(): number {
return Number(this.$route.params.id)
},
currentPost(): Post | undefined {
return posts.find(({ id }) => id === this.currentId)
},
},
})
</script>
We see the validate
method. This method is absent in Vue
, Nuxt
provides it to validate the parameters received from the router. Validate will be called every time you navigate to a new route. In this case, we just check that the id
passed to us is a number. If the validation fails, the user will be returned to the error.vue
error page.
There are 2 computed properties presented here.
Let's take a closer look at what they do:
1) currentId
- this property returns us the current post id
(which was obtained from the router parameters), having previously converted it to number
.
2) currentPost
returns an object with information about the selected post from the array of all posts.
Well, we seem to figure it out. Let's take a look at the template:
<template lang="pug">
.page
link-to-home
section-header(
:title="currentPost.title"
)
post-full(
:post="currentPost"
)
</template>
The style section for this page, as well as for the main page, is missing.
The code for the pages on Github.
Deployment to Hostman
Hooray! Our application is almost ready. It's time to start deploying it.
To do this task I will use the Hostman cloud platform, which allows to automate the deployment process.
Besides, Hostman provides a free plan for static sites. That is exactly what we need.
To publish we should click the Create
button in the platform interface, select a free plan and connect our Github repository, specifying the necessary options for deployment.
Immediately after that, publishing will automatically start and a free domain will be created in the *.hostman.site zone
with the ssl
certificate from Let's Encrypt.
From now with every new push to the selected branch (master
by default) a new version of the application will deploy. Simple and convenient!
Conclusion
So, what we have now:
We tried to demonstrate in practice how to work with Nuxt.js. We have managed to build a simple application from start to finish, from making a UI kit to a deployment process.
If you have followed all the steps from this post, congratulations on creating your first Nuxt.js application! Was it difficult? What do you think about this framework? If you have any questions or suggestions, feel free to write comments below.
Sources:
Building
- Official site Nuxt.js
- @nuxtjs/style-resources module
- Options API or Class API
- webpack-dev-server
- HMR
- Jest
Fonts and Images
- Open Font License
- Google Fonts
- Webfontloader library
- images from unDraw
Deployment
Top comments (0)