Nuxt.js app from UI kit to deployment

Hello, Habr!



I wrote this detailed, step-by-step tutorial so that anyone can create their own application using the Nuxt.js framework from scratch.



In this article, we'll discuss the basics, the basics of creating a Nuxt.js application:



  • project creation and configuration,
  • assets and static: styles, fonts, images, posts,
  • creating components,
  • creating pages and layouts,
  • application deployment (deployment).


See what happened!



A little about Nuxt.js



Nuxt is a high-level Vue framework. Nuxt.js allows you to develop isomorphic web applications out of the box by abstracting the distribution details of the server and client code. Thanks to this approach, we save time and can focus on development.



The main advantages of Nuxt:



  • SPA, SSR and prerender are already configured; all that is required of us is to indicate. In this application, we use a prerender for the product mode, that is, we generate all the pages of the site in advance, and then deploy them to the hosting for distributing statics.
  • Great SEO for all search engines is the result of using SSR or pre-renderer.
  • . js chunks, css styles API ( webpack 4, Nuxt).
  • Google Lighthouse / Page Speed. 100/100 .
  • CSS Modules, Babel, Postscc create-nuxt-app.
  • .
  • 50 Vue.js.


We can talk about the benefits of Nuxt for a very long time. This is the framework that I really love for its ease of use and the ability to create flexible and easily scalable applications. So, I propose to start and see all the benefits in practice.

More information about Nuxt.js on the official site . Detailed guides are also available here .



Design



A well-thought-out, ready-made design, or even better a UI kit, will significantly speed up and simplify the development of any application. If there is no free UI designer nearby, that's okay. As part of our instructions, we can handle it ourselves!



Especially for this article, I prepared a blog design in a modern, minimalist style with simple functionality, enough to demonstrate the capabilities of Nuxt.



For development, I used the Figma online service. The design and UI kit are available here . You can copy this template and use it in your project.



Project creation



To create a project, we will use the Nuxt create-nuxt-app utility from the Nuxt developers , which allows us to configure the application template through the cli.



We initialize the project by specifying its name:



npx create-nuxt-app nuxt-blog
      
      





Further, in several stages, the utility will offer us to choose a set of preferred libraries and packages, after which it will independently download, configure 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 in Vue with Typescript, you can use two APIs: Options API or Class API .



They do not differ functionally from each other, but they have different syntax. Personally, the Options API syntax is closer to me, so 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 webpack-dev-server with HMR installed and configured as a local server , which makes development fast and comfortable.



Since we are creating a demo version of the application, I will not write tests for it. But I highly recommend not neglecting application testing in commercial development.



If you have not touched on this topic before, then I advise you to pay attention to Jest - a very simple, but at the same time powerful tool that supports working with Nuxt together with vue-test-utils .



Project structure



Nuxt creates by default a directory and file structure suitable for a quick start to development. For our project, such a structure suits perfectly, so we will not change it. You can read more about the purpose of different directories on the Nuxt website .



-- Assets

-- Static

-- Pages

-- Middleware

-- Components

-- Layouts

-- Plugins

-- Store

-- nuxt.config.js

-- ...other files














Application creation



Before writing the code, let's do the following:



1. Remove the starter components and pages generated by Nuxt.

2. Install pug and scss for our convenience and to save time during development. Let's execute the command:



npm i --save-dev pug pug-plain-loader node-sass sass-loader fibers
      
      





After that, the use of the lang attribute for the template and style tags will become available:



<template lang="pug"></template>

<style lang="scss"></style>
      
      





3. Add support for the :: v-deep deep selector to the stylelint configuration, which will allow styles to be applied 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, go to the next stage.



Posts



Posts will be stored in the content / posts directory, which we will create in the project root as a set of markdown files.



Let's create 5 small files so that we can start working with them right away. For simplicity, we will use the names 1.md, 2.md, etc.



In the content directory, create the Posts.d.ts file, in which 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 that the meaning of all fields should be clear from the names.



Move 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, let's do it now.



Now let's add the images in .svg format to the created directory with the names that we specified above.



I will take 5 images from unDraw . This excellent resource is constantly updated and contains many free svg images.



Now that everything is ready, the content directory should look like this: And the images subdirectory should appear in the assets directory with the following content:



content/

-- posts.ts

-- Posts.d.ts

-- posts/

---- 1.md

---- 2.md

---- 3.md

---- 4.md

---- 5.md












assets/

-- images/

---- 1.svg

---- 2.svg

---- 3.svg

---- 4.svg

---- 5.svg

...








Dynamically retrieving files



Since we will receive images and files with the text of posts dynamically, we need 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 it 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 that remains for us is to enable this mixin in the nuxt.config.js file:



{
  plugins: [  
    '~plugins/mixins/getDynamicFile.ts',  
  ],
}
      
      





Fonts



After the stage of creating posts, connect the fonts. The easiest way to do this is the wonderful Webfontloader library , which allows you to get any font from Google Fonts . However, in commercial development, custom fonts are often used, so let's look at just that case here.



Rubik was chosen as the font for our application, which is distributed under the Open Font License . You can download it all from the same Google Fonts .



Please note that in the downloaded archive the fonts will be in the otf format, but since we work with the web, the woff and woff2 formats will be the best choice for us. They are smaller than any other formats, but are perfectly supported in all modern browsers. To convert otf to the formats we need, you can use one of the many free online services.



So, we have the fonts in the formats we need, it's time to include them in the project. To do this, create a fonts subdirectory in the static directory and add our fonts there. Let's create a fonts.css file in the same directory, which will be responsible for connecting 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");  
}  
  
...
      
      





The full contents of the file can be seen in the repository .



There are 2 things worth noticing:



1. We specify font-display: swap;, defining how the font connected via font-face will be displayed depending on whether it is loaded and ready to use.

In this case, we do not set the blocking period and set an infinite substitution period. That is, the font loading occurs in the background and does not block the page loading, and the font will be displayed when ready.



2. In src, we specify the boot order by priority. First, we check if the desired font is installed on the user's device by checking the possible variants of the font name. If we don't find it, then we check if the browser supports the more modern woff2 format, and if not, then go to the next woff format. There is a possibility that the user is using an outdated browser (for example, IE <9), in which case, in the future, we will indicate the fonts built into the browser as a fallback.



After creating the file with the font loading rules, you need to connect 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 before, we are using the preload and prefetch properties, thereby setting high priority in the browser for downloading these files and not blocking the page rendering.



Let's immediately add to the static favicon directory of our application, which can be generated using any free online service.



The static directory now looks like this: Move on to the next step.



static/

-- fonts/

---- fonts.css

---- Rubik-Bold.woff2

---- Rubik-Bold.woff

---- Rubik-Medium.woff2

---- Rubik-Medium.woff

---- Rubik-Regular.woff2

---- Rubik-Regular.woff

-- favicon.ico












Reusable styles



In our project, all the styles used are described by 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, in which we will store all reused styles in the project. In turn, the styles directory will contain the variables.scss file with all our scss variables.



The contents of the file can be seen in the repository .



Now we need to connect these variables to the project so that they are available in any of our components. Nuxt uses the @ nuxtjs / style-resources module 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'],  
  },
}
      
      





Fine! 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 appearance of the layouts is changed by the designer.



Let's create a global subdirectory in the assets / styles directory with the following files:



1. typography.scss - the file will contain all helper classes for text, including links.

Note that these helper classes change styles depending on the resolution of the user's device: smartphone or PC.



2. transitions.scss - the file will contain global animation styles, both for transitions between pages, and for animations inside components, if we need it in the future.



3. other.scss - the file will contain global styles, which have not yet been selected into a separate 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 mark the boundaries of the logical boxes, and the .content class will be used to restrict the width of the content and center it on the page.



We'll see examples of how these classes are used later on when we get started implementing components and pages.



4. index.scss - a general file that will be used as a single export point for all global styles.



The full contents of the files can be seen on Github .



At this stage, we will include these global styles so that they become available throughout the application. For this task Nuxt provides us with a css section in the nuxt.config.js file:



{
  css: ['~assets/styles/global'],
}
      
      





It is worth saying that in the future, when assigning css-classes, the following logic will be used:



1. If a tag has both helper classes and local classes, then local classes will be directly added to the tag, for example, p.some-local-class , and the helper classes are specified in the class property, for example, class = "body3 medium".



2. If a tag has only helper classes or only local classes, they will be directly added to the tag.



I use this technique for my convenience, to visually immediately distinguish between global and local classes.



Before development, let's install and enable reset.css so that our layout looks the same in all browsers. To do this, install the required package:



npm i reset-css
      
      





And we will include it in the nuxt.config.js file in the already familiar css section, which will now look like this:



{
  css: [
    '~assets/styles/global',
    'reset-css/reset.css',
  ],
}
      
      





Happened? If so, we are ready to move on to the next step!



Layouts



In Nuxt, Layouts are wrappers over pages that allow you to reuse common components between them and implement the necessary common logic. Since our application is extremely simple, it will be enough for us to use the default layout - default.vue.



Also, Nuxt uses a separate layout 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 assembly it will be replaced with a specific page that the user requested.



2.db-footer is our own Footer component (we'll write it a bit later) that will be automatically added to every page of our application.



error.vue



By default, for any error returned from the server in the http status, Nuxt redirects to layout / error.vue and passes an object containing a description of the error received via an input parameter called error.



Let's see what the script section looks like, 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 happens 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 messages for all errors for which we want to display a unique message and a default message for all others.



3. In the computed property errorText we check if the received error is in the dictionary. If there is an error, then we return a message for it. If there is no error, 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 created earlier in the assets / styles / global / other.scss file. They allow content to be displayed centered on the page.



This uses the section-header component, which has not yet been created, but in the future it will be a universal component for displaying headers. We will implement it when we talk about components.



The layouts directory looks like this: Let's start creating the components.



layouts/

-- default.vue

-- error.vue












Components



Components are the building blocks of our application. Let's start with the components we have already seen above.



In order not to inflate the article, I will not describe the styles of the components. They can be found in the repository of this application.



SectionHeader The

headings in our application are made in the same style, so it is logical to use one component to display them and change the displayed data through the input parameters.



Let's take a 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 how 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 contain 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 very tiny, so I will give all its code at once (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 are requesting the home.svg icon from the assets / icons directory. First, you need to create this directory and add the desired icon there.



DbFooter



The DbFooter component is very simple. It contains copyright and a link to create a letter.

The requirements are clear, let's start the implementation with 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, we have just one computed property that returns the current year, concatenated with a given string. Now let's take a 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 you click on the Contact us link, we will open the native mail client and immediately set the message subject. This solution is suitable for a demo of our application, but in real life, a more appropriate solution would be to implement a feedback form to send messages directly from the site.



PostCard



The post card is simple and straightforward.



<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 input parameter, post, which will contain all the necessary information about the post.



We also implement the computed property pageUrl for use in the template, which will return us a link to the desired page with the post.



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 so that the user has the opportunity to open the post in a new window using the mouse.



This is the first time the global getDynamicFile mixin we created earlier in this article is used.



PostList



The main component on the main page will consist of a post counter at the top and a list of the posts themselves.



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 with posts, we add them to the data object so that the template will have access to this data in the future.



The template itself 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>
      
      





For everything to work correctly, do not forget to add the list.svg icon to the assets / icons directory.



PostFull



PostFull is the main component on a separate post page that will be responsible for displaying the post text.



For this component, we need the @ nuxtjs / markdownit module , which will be responsible for converting md to html.



Let's install it:



npm i @nuxtjs/markdownit
      
      





Then add @ nuxtjs / markdownit to the modules section of the nuxt.config.js file:



{
  modules:  [
    '@nuxtjs/markdownit',
  ],
}
      
      





Fine! Let's start implementing the component. As always, 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 input parameter, post, which will contain all the necessary information about the post.



Moving on to 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 are using the standard v-html attribute to render the file, since @ nuxtjs / markdownit will do the rest of the work for us. Incredibly simple!



We can use the :: v-deep selector to access the style customization of our rendered .md file. Take a look on Github to see how this component is implemented.



In this component, I set only 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 start creating the pages.



As you probably already understood from the design, our application will consist of a main page with a list of all articles and a dynamic page that displays the selected post.



The structure of the pages directory: All components are self-contained, and their states are determined through input parameters, so our pages will look like a list of components specified in the required order. The main page will look like this:



pages/

-- index.vue

-- post/

---- _id.vue














<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 the correct padding, we used the global .page class we created earlier in assets / styles / global / other.scss.



A single 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 for us 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 implemented 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 general array of all posts.



It seems that they figured 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 .



Deploy to Hostman



Hurrah! Our application is almost ready. It's time to start deploying it.



For this task, I will use the Hostman cloud platform , which allows you to automate the deployment process.



Our application will be a static site, since a free plan is available for static sites in Hostman.



To publish, we must 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 installed ssl certificate from Let's Encrypt.



From now on, every new push to the selected branch (master by default) will be used to deploy a new version of the application. Incredibly simple and convenient!



Conclusion



So what we have:





We tried to show in practice how to work with the Nuxt.js framework. We managed to create a simple application from start to finish, from creating a UI kit to organizing a deployment.



If you've followed all the steps in this article, congratulations on building your first Nuxt.js application. Was it difficult? How do you work with this framework? If you have any questions or requests, do not hesitate to write in the comments.



All Articles