Paulund
2022-06-12 #nuxtjs #vuejs

Create A Blog With NuxtJS

In this tutorial we're going to create a blog built with NuxtJS.

Create NuxtJS Application

The easiest way to create a NuxtJS application is by using the create-nuxt-app package. This can be installed by using the command.

yarn create nuxt-app <project-name>

This will run through a few questions to get you setup such as UI Framework, TypeScript, linter, testing framework and even the continuous integration platform you'd like to use.

Once installed you can run your nuxt site by using the following.

cd <project-name>
yarn dev

The application is now running on http://localhost:3000.

To make sure that you can view a page, go to the pages directory and add a new file for posts.vue and add a <template></template>, this will allow you to navigate to http://localhost:3000/posts to see the new posts page.

Install Content Package

Now we have NuxtJS installed and running locally we need to install a package that will allow us to create content for the blog.

NuxtJS has an official package nuxt/content that will allow you to fetch content inside a markdown file via an API and act as a Git based headless CMS.

Install NuxtJS Content

To install you can use the following command.

yarn add @nuxt/content

To add modules into NuxtJS you need to add a module into the nuxt.config.js file.

{
  modules: [
    '@nuxt/content'
  ],
  content: {
    // Options
  }
}

Now we can create a markdown file in the content folder to fetch and display. As we're creating a blog we might want to have different content types therefore create a articles folder to place your markdown files inside.

You can define a data into your blog post by adding a YAML block to the type of the file.

---
title: 
slug: 
category: 
extension: md
createdAt:
---

You can then fill out the blog post content using the standard markdown syntax.

Default Layout

You can extend the main layout in NuxtJS by creating your own default layout, create a file in layouts/default.vue and it will be used for all your pages that you don't specify a layout.

The simplest layout you can use is by just including in the <nuxt /> tag, this is what nuxt will use to inject in the page component.

<template>
  <Nuxt />
</template>

For this blog we want to include a header and footer, we'll use the below file as the main template.

<template>
  <div>
    <header-bar></header-bar>

    <div class="w-full md:container mx-auto px-6">
      <Nuxt />
    </div>

    <footer-bar></footer-bar>
  </div>
</template>

Error Layout

The Error layout is the page that will be displayed when a 404/500 error code was returned. This should be placed inside the layouts folder layouts/error.vue but this is not a layout and should be used as a page, therefore do not include the <Nuxt /> tag inside the page.

<template>
  <div class="my-8">
    <h1>Page Not Found</h1>
  </div>
</template>

<script>
export default {
  layout: 'error',
  props: {
    error: {
      type: String,
      default: '404',
    },
  },
}
</script>

Homepage

The first page we're going to create is the homepage of the blog, for this page we're going to show a list of the 5 latest posts on the blog, create a new file in the pages directory of index.vue pages/index.vue

Using the content package we can use the API to fetch the latest posts.

const articles = await $content('articles')
      .sortBy('createdAt', 'desc')
      .limit(5)
      .fetch()

The $content('articles') part tells the API to search inside the articles folders in the content, then we're going to sort it by the latest date .sortBy('createdAt', 'desc'), we only want to return the latest 5 posts .limit(5) then fetch the content .fetch().

To make sure that this is called on the server side we're going to use the asyncData method on the component. The return of this method will allow us to use this variable locally within the component like below.

<template>
  <div class="container">
    <div class="border-b border-gray-500 pb-2 my-4" v-for="post in articles" :key="post.slug">
      <h2 class="text-4xl mb-8 font-bold">
        <NuxtLink :to="{ name: 'slug', params: { slug: post.slug.toLowerCase() } }">
          {{ post.title }}
        </NuxtLink>
      </h2>
      <nuxt-content :document="{ body: post.excerpt }" />
    </div>
  </div>
</template>

<script>
export default {
  async asyncData({ $content, params }) {
    const articles = await $content('articles')
      .sortBy('createdAt', 'desc')
      .limit(5)
      .fetch()

    return { articles }
  },
}
</script>

Homepage SEO

Nuxt allows you to add global meta tags and SEO configs which is what we'll use on the homepage, the alternative is to add meta information inside the page component itself.

To use global settings we can add SEO data inside the nuxt.config.js file.

export default {
  // Global page headers (https://go.nuxtjs.dev/config-head)
  head: {
    title: 'Paulund',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      { rel: 'alternate', type: 'application/rss+xml', href: '/feed/rss.xml' },
    ],
  },
}

Homepage Test

Add the NuxtJS test utils to your project.

yarn add --dev jest @nuxt/test-utils

Then add the package to your jest config jest.config.js

module.exports = {
  preset: '@nuxt/test-utils'
}

Create a test file in the pages directory pages/__tests__/index.test.js.

The test will attempt to get the content of the / url and check the response for specific content.

import { get, setupTest } from '@nuxt/test-utils'

describe('Homepage', () => {
  setupTest({
    server: true,
    setupTimeout: 120000,
  })

  it('renders the index page', async () => {
    const { body } = await get('/')

    expect(body).toContain('<div class="container">')
  })
})

In the setup of the test we need to define the server option to define this as server-size rendering, as we're using the content package for Nuxt we're going to extend the timeout to 120000 ms to make sure the application can fetch the content.

Create A Single Post Page

Once you have the homepage the next page you can create is the single post page, this page will be linked from the homepage using the <NuxtLink> tag.

First create a file inside pages folder with a filename of _slug.vue this will allow you to have a dynamic page that will take the slug in the URL which we can use to search for the right blog post.

Using the content API we will query for articles and use the params.slug to search for the correct blog post.

This uses the slug parameter in the markdown folder where we define the content.

---
title: 
slug: 
category: 
extension: md
createdAt:
---

If we don't find a blog post an exception will be thrown therefore we can catch this exception and throw a 404 error using the error function helper.

To make sure that we update meta title tags we want to update the head with the title of the post.

<script>
export default {
  async asyncData({ $content, params, error }) {
    const post = await $content('articles', params.slug)
      .fetch()
      .catch((e) => {
        error({ statusCode: 404, message: 'Page not found' })
      })

    const postDate = new Date(post?.createdAt)

    return { post, postDate }
  },

  head() {
    return {
      title: this.post.title,
    }
  }
}
</script>

Inside the template tag of the vue component we want to display the full content of the article.

<template>
  <div>
    <div class="flex items-center pt-10 pb-4 mb-4">
      <section class="w-full">
        <h1 class="mt-4 mb-0 text-5xl">{{ post.title }}</h1>
      </section>
    </div>

    <div class="mb-8">
      <nuxt-content :document="post" />
    </div>
  </div>
</template>

To show the title of the post we can use the variable post.title to display the entire content of the article the nuxt content package has a content tag we can use in <nuxt-content> we will then pass in the post object.

<nuxt-content :document="post" />

Create A Category Page

As you see from the meta information we have in the content articles we've define a category for the blog post.

---
title: 
slug: 
category: 
extension: md
createdAt:
---

We're now going to create a page that will show all of the content within this category.

Inside your pages folder create a new folder of category, then inside this folder create a new _slug.vue file. This will activate a slug to us of /category/<category-slug>.

Within this file you'll need to search for all the posts that are defined to be within this category.

See the below code that will use the content API to search for all articles where the category matches that in the slug of the page.

<script>
import titlefy from '~/mixins/titlefy'

export default {
  mixins: [titlefy],

  async asyncData({ $content, params }) {
    const articles = await $content('articles')
      .only(['title', 'slug', 'excerpt', 'category'])
      .where({ category: params.slug })
      .sortBy('createdAt', 'desc')
      .fetch()

    return { articles }
  },

  head() {
    return {
      title: this.titlefy(this.slug),
    }
  },
}
</script>

Once we have the articles we can then look through these and display the excerpt of these articles.

<template>
  <div>
    <div class="bg-gray-200 p-8 mb-8">
      <h1 class="text-4xl">{{ titlefy(slug) }}</h1>
    </div>

     <div class="border-b border-gray-500 pb-2 my-4" v-for="post in articles" :key="post.slug">
      <h2 class="text-4xl mb-8 font-bold">
        <NuxtLink :to="{ name: 'slug', params: { slug: post.slug.toLowerCase() } }">
          {{ post.title }}
        </NuxtLink>
      </h2>
      <nuxt-content :document="{ body: post.excerpt }" />
    </div>
  </div>
</template>

Create a Search Component

Using the nuxt content package you have the feature of searching for the content inside your articles. We're going to create a new component that gives us a search box for the user and then will show the blog post that match the search term.

First create a file in the components folder of SearchPosts.vue.

Within this component we're going to have data points for the search term and the results of the search. We'll need a method that will run as the user is entering letters into the search box and search for the articles.

As we could have a lot of data to search through we're going to make sure search is only actioned when the search term is over 3 characters.

<script>
export default {
  data() {
    return {
      searchTerm: '',
      searchResults: [],
    }
  },

  methods: {
    async searchPosts() {
      if (this.searchTerm.length < 3) {
        this.searchResults = []
        return
      }

      // Search fields are defined in the configuration fullTextSearchFields, default ['title', 'description', 'slug', 'text']
      this.searchResults = await this.$content('articles')
        .search(this.searchTerm)
        .limit(5)
        .fetch()
    },
  },
}
</script>

Then we can build the HTML for the search box that will display the content of the search results when articles are found from the search term.

<template>
  <div
    id="article-search"
    class="flex items-center text-center py-12 mb-4 bg-gray-100"
  >
    <div class="w-full mx-auto px-6 flex-col">
      <h1 class="mb-6">Search The Articles</h1>
      <div class="mx-1">
        <section>
          <div>
            <input
              v-model="searchTerm"
              type="search"
              placeholder="Search for a article"
              autocomplete="off"
              @keydown="searchPosts"
            />
          </div>
          <div class="relative">
            <section v-if="searchResults.length">
              <NuxtLink
                v-for="post in searchResults"
                :key="post.slug"
                :to="{
                  name: 'slug',
                  params: { slug: post.slug.toLowerCase() },
                }"
                class="w-full text-left bg-white block p-4 hover:bg-gray-100"
              >
                {{ post.title }}
              </NuxtLink>
            </section>
          </div>
        </section>
      </div>
    </div>
  </div>
</template>

Create A Contact Page

When you have a blog post it's important to have a contact page for your readers to be able to contact you. Nuxt doesn't have this ability built-in by default by there are packages you can use to install to send a email from a contact form.

For this contact form we're going to use a package called nuxt-mail

npm i nuxt-mail

This will allow you to add a module to the nuxt build config nuxt.config.js to define the SMTP to use to send the email.

modules: [
  '@nuxt/content',
  '@nuxtjs/axios',
  ['nuxt-mail', {
    message: {
      to: '[email protected]',
    },
    smtp: {
      host: "smtp.example.com",
      port: 587,
      auth: {
        user: '',
        pass: ''
      },
    },
  }],
],

Inside your pages folder create a new page of contact.vue on this page we'll need to have a few text boxes for name, email and message. Then we'll send this message on the click of the submit button.

<template>
    <div>
        <h1>Contact</h1> 

        <div v-if="emailSent" class="alert alert-success">
            <p>Email sent</p>
        </div>
        
        <form v-on:submit.prevent="submitForm" v-if="!emailSent">
            <div>
                <label>Name</label> 
                <p><input type="text" id="name" placeholder="Name" required="required" class="input" v-model="name"></p>
            </div> 
            
            <div>
                <label>Email</label> 
                <p><input type="text" id="email" placeholder="Email" required="required" class="input" v-model="email"></p>
            </div> 
            
            <div>
                <label>Message</label> 
                <p><textarea id="message" placeholder="Message" required="required" class="input" v-model="message"></textarea></p>
            </div> 
            
            <div>
                <p><button id="send" class="button is-primary">Send</button></p>
            </div>
        </form>
    </div>
</template>

<script>
export default {
    data() {
        return {
            name: '',
            email: '',
            message: '',
            emailSent: false
        }
    },
    methods: {
        submitForm () {
            this.$mail.send({
                from: this.email,
                subject: 'Contact Form',
                text: this.name + '<br>' + this.message,
            }).then(() => {
                this.emailSent = true
            })
        }
    }
}
</script>

Add Google Analytics

There is a NuxtJS package that allows you to easily add Google analytics to your website, to install this on your website use the command

npm install --save-dev @nuxtjs/google-analytics

Then add the package to your build modules inside the nuxt.config.js.

{
  buildModules: [
    '@nuxtjs/google-analytics'
  ],
}

Next you'll need to define the Google analytics ID to use inside the nuxt.config.js file.

googleAnalytics: {
  id: 'UA-XXX-X'
}

Add RSS Feed

A common way blogs are discovered and read are via RSS feeds we can add this feature to our blog by using another NuxtJS package we can install by using.

npm install @nuxtjs/feed

Next we need to setup the RSS feed inside the nuxt.config.js file to tell it how to display the posts on the RSS feed, you can use the following config setup.

feed() {
    const baseUrlArticles = 'https://website.com'
    const baseLinkFeedArticles = '/feed'
    const feedFormats = {
      rss: { type: 'rss2', file: 'rss.xml' },
      json: { type: 'json1', file: '' },
    }
    const { $content } = require('@nuxt/content')

    const createFeedArticles = async function (feed) {
      feed.options = {
        title: 'Blog RSS',
        description: 'RSS feed',
        link: baseUrlArticles,
      }
      const articles = await $content('articles')
        .sortBy('createdAt', 'desc')
        .limit(20)
        .fetch()

      articles.forEach((article) => {
        const url = `${baseUrlArticles}/${article.slug}`

        const postDate = new Date(article?.createdAt)
        feed.addItem({
          title: article.title,
          id: url,
          link: url,
          date: postDate,
          description: article.description,
          content: article.bodyPlainText,
        })
      })
    }

    return Object.values(feedFormats).map(({ file, type }) => ({
      path: `${baseLinkFeedArticles}/${file}`,
      cacheTime: 1000 * 60 * 15,
      type,
      create: createFeedArticles,
    }))
  },

Optimize CSS Builds With PurgeCSS

Purge CSS will look at your unused CSS classes and remove them from the final CSS file there is a package that will allow you to easily add this feature into your Nuxt application.

npm install --save-dev nuxt-purgecss

Then in your buildModules add the nuxt purgecss package inside the nuxt.config.js.

buildModules: [
  'nuxt-purgecss',
],

Then to make sure the CSS is extracted into it's own file add the extractCSS property to the build config.

build: {
  extractCSS: true,
},

This gives you the basics of creating a blog with NuxtJS, in future articles we'll go through the hosting options you have with a NuxtJS blog.