Nuxt.js static content using Vuex store

Stateful Static Content - Leveraging Vuex for Nuxt.js Static Website Development

July 09, 2021 Dykraf

Use Vuex state management in Nuxt.js Static Content Website and write your static website with markdown files.

Web Story VersionWeb Story

Introduction

Nuxt.js content has a default $content() method for processing data content that available out of the box. In the official documentation instructions, fetch could be used in the pages on asyncData sections. Vuex is a state management system pattern and library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion. Below is the explanation about Nuxt.js static content using Vuex state management.

Disclaimer

The intention for using Vuex in the Nuxt.js content is just simply a choice. In this writings, I am going to use Vuex to store all the contents, fetch actions, and states. If you think that using Vuex is overhead or other concerns and does not necessarily have to, it is simply a choice.

For the purpose of this writing on Nuxt.js Content using Vuex, you can visit this link for the demo preview and this link for the source code.

Fun Fact: This blog also uses Nuxt.js Static Content and Vuex behind the curtain.

Content Front Matter

Here is the page sample from Nuxtjs content with front matter from the ~/content/**/*.md files:

---
title: Duis aute irure dolor in reprehenderit in voluptate
description: Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.
img: https://images.unsplash.com/photo-1580752300992-559f8e0734e0?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80
alt: nice image
author:
  name: Dykraf
  bio: Web Developer and Tech Writer
  img: static/img/logo.jpg
tags:
  - web development
  - nuxtjs
createdAt: 20-May-2021 00:05
updatedAt: 01-June-2021 20:30
status: publish
---

# Et harum quidem rerum facilis est et expedita distinctio

At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga.

# Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

# Quis autem vel eum iure reprehenderit qui in ea voluptate

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Content Pages

This is the blogs page from ~/pages/index.vue file :

export default {
  async asyncData({ $content }) {
    const articles = await $content('blogs')
      .only([
        'title',
        'description',
        'img',
        'slug',
        'author',
        'createdAt',
        'tags'
      ])
      .sortBy('createdAt', 'desc')
      .fetch()
    return {
      articles
    }
  }
}

And this is the blog detail page from ~/pages/blog/_slug.vue file :

export default {
  async asyncData({ $content, params }) {
    const articles = await $content('blogs', params.slug)
      .only([
        'title',
        'description',
        'img',
        'slug',
        'author',
        'createdAt',
        'tags'
      ])
      .sortBy('createdAt', 'desc')
      .fetch()
    return {
      articles
    }
  }
}

Activating Vuex store

I am going to start activating Vuex features and write in the ~/store/index.js directory inside the Nuxt.js project.

/* All states default */
const state = () => ({})
/* All states mutations */
const mutations = {}
/* All states getters */
const getters = {}
/* All states actions */
const actions = {}
/* Export all stores */
export default {
  state,
  mutations,
  getters,
  actions
}

Utility

Some utility will be needed for the store and others to stitch together. This will be in ~/utils/index.js file :

/* eslint-disable no-useless-escape */
/* eslint-disable no-prototype-builtins */

// JSON Parse
export function parsesJSON(data = '') {
  return JSON.parse(JSON.stringify(data))
}

// Format dates
export function formatDate(date) {
  const options = { year: 'numeric', month: 'long', day: 'numeric' }
  return new Date(date).toLocaleDateString('en', options)
}

// Check if object is empty
export function isEmptyObject(obj) {
  for (const prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      return false
    }
  }

  return JSON.stringify(obj) === JSON.stringify({})
}

Status type config

And then some constant variables to hold some status types. This will be in ~/config/statusTypes file :

export default {
  INIT: 'init',
  LOADING: 'loading',
  SUCCESS: 'success',
  ERROR: 'error'
}

Updated Store

The store will have some actions, mutations, and other logics at ~/store/index.js updated file :

import STATUS_TYPES from '~/config/statusTypes'
import { parsesJSON } from '~/utils'

const { INIT, SUCCESS } = STATUS_TYPES

/* All states default */
const state = () => ({
  blogs: {
    status: INIT,
    error: null,
    data: []
  },
  tags: {
    status: INIT,
    error: null,
    data: []
  },
  blog: {
    status: INIT,
    error: null,
    data: {}
  },
  blogTags: {
    status: INIT,
    error: null,
    data: []
  },
  prevNext: {
    status: INIT,
    error: null,
    data: {}
  },
  tag: {
    status: INIT,
    error: null,
    data: {}
  },
  author: {
    status: INIT,
    error: null,
    data: {}
  },
  readMore: {
    status: INIT,
    error: null,
    data: []
  }
})

// TODO - Expand into loading, success and errors mutations
/* All states mutations */
const mutations = {
  GET_BLOGS(state, payload) {
    state.blogs.data = payload
    state.blogs.status = SUCCESS
  },
  GET_TAGS(state, payload) {
    state.tags.data = payload
    state.tags.status = SUCCESS
  },
  GET_BLOG(state, payload) {
    state.blog.data = payload
    state.blog.status = SUCCESS
  },
  GET_BLOG_TAGS(state, payload) {
    state.blogTags.data = payload
    state.blogTags.status = SUCCESS
  },
  GET_TAG(state, payload) {
    state.tag.data = payload
    state.tag.status = SUCCESS
  },
  GET_AUTHOR(state, payload) {
    state.author.data = payload
    state.author.status = SUCCESS
  },
  GET_READ_MORE(state, payload) {
    state.readMore.data = payload
    state.readMore.status = SUCCESS
  },
  GET_PREV_NEXT(state, payload) {
    state.prevNext.data = payload
    state.prevNext.status = SUCCESS
  }
}

// TODO - Expand into loading, success and errors getters
/* All states getters */
const getters = {
  getBlogs: (state) => parsesJSON(state.blogs),
  getTags: (state) => parsesJSON(state.tags),
  getBlog: (state) => parsesJSON(state.blog),
  getBlogTags: (state) => parsesJSON(state.blogTags),
  getPrevNext: (state) => parsesJSON(state.prevNext),
  getTag: (state) => parsesJSON(state.tag),
  getAuthor: (state) => parsesJSON(state.author),
  getReadMore: (state) => parsesJSON(state.readMore)
}

/* All states actions */
const actions = {
  async getBlogs({ commit }, params, callback) {
    const storeBlogs = await this.$content('blogs')
      .only([
        'title',
        'description',
        'img',
        'slug',
        'author',
        'createdAt',
        'updatedAt',
        'tags'
      ])
      .without(['body'])
      .where({ ...params, status: 'publish' })
      .sortBy('createdAt', 'desc')
      .limit(12)
      .fetch()

    commit('GET_BLOGS', storeBlogs)
  },
  async getTags({ commit }, params, callback) {
    const storeTags = await this.$content('tags')
      .only(['name', 'description', 'img', 'slug'])
      .sortBy('createdAt', 'asc')
      .fetch()

    commit('GET_TAGS', storeTags)
  },
  async getBlog({ commit }, params, callback) {
    const storeBlog = await this.$content('blogs', params.slug).fetch()

    commit('GET_BLOG', storeBlog)
  },
  async getBlogTags({ commit }, params, callback) {
    const tagLists = await this.$content('tags')
      .only(['name', 'slug'])
      .where({ name: { $containsAny: params.tags } })
      .without(['body'])
      .fetch()

    const storeBlogTags = Object.assign(
      {},
      ...tagLists.map((s) => ({ [s.name]: s }))
    )
    commit('GET_BLOG_TAGS', storeBlogTags)
  },
  async getPrevNext({ commit }, params, callback) {
    const [prev, next] = await this.$content('blogs')
      .where({ status: 'publish' })
      .only(['title', 'slug'])
      .without(['body'])
      .sortBy('createdAt', 'asc')
      .surround(params.slug)
      .fetch()

    commit('GET_PREV_NEXT', { prev, next })
  },
  async getTag({ commit }, params, callback) {
    const storeTag = await this.$content('tags', params.slug)
      .only(['name', 'description', 'img', 'slug'])
      .fetch()

    commit('GET_TAG', storeTag)
  },
  async getAuthor({ commit }, params, callback) {
    const storeAuthor = await this.$content('blogs', params.slug)
      .only(['name', 'description', 'img', 'slug'])
      .where({
        'author.name': {
          $regex: [params.author, 'i']
        },
        status: 'publish'
      })
      .without(['body'])
      .limit(1)
      .sortBy('createdAt', 'asc')
      .fetch()

    commit('GET_AUTHOR', storeAuthor)
  },
  async getReadMore({ commit }, params, callback) {
    const storeReadMore = await this.$content('blogs')
      .only(['title', 'slug'])
      .where({
        status: 'publish',
        slug: { $ne: params.slug },
        tags: { $containsAny: params.tags }
      })
      .without(['body'])
      .sortBy('createdAt', 'desc')
      .limit(3)
      .fetch()

    commit('GET_READ_MORE', storeReadMore)
  }
}

/* Export all stores */
export default {
  state,
  mutations,
  getters,
  actions
}

Using store in pages with dispatch

Dispatch store in the pages with these examples ~/pages/index.vue :

<template>
  <div id="wrapper" class="container">
    <div class="theme">
      <!-- Header -->
      <Header />
      <main class="main">
        <section>
          <Blogs :items="blogs" />
        </section>
        <Topics :items="tags" />
      </main>
      <!-- Footer -->
      <Footer />
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  async fetch({ params, store: { dispatch, getters } }) {
    await dispatch('getBlogs')
    await dispatch('getTags')
  },
  computed: {
    ...mapState({
      blogs: (state) => state.blogs.data,
      tags: (state) => state.tags.data
    })
  }
}
</script>

Dispatch in blog detail ~/pages/blog/_slug.vue :

<template>
  <main>
    <article>
      <div>
        <img :src="blog.img" :alt="blog.alt" />
      </div>
      <section>
        <div>
          <div v-if="!isEmptyObject(tags)">
            <h1>
              {{ blog.title }}
            </h1>
            <div>
              <span>
                {{ blog && $dateFns.format(blog.createdAt, 'MMMM dd, yyyy') }}
              </span>
              <span></span>
              <NuxtLink :to="`/blog/author/${blog.author && blog.author.name}`"
                ><span>{{ blog.author.name }}</span></NuxtLink
              >
            </div>
            <div v-for="(tag, id) in blog.tags" :key="id">
              <NuxtLink :to="`/blog/tag/${tags[tag] && tags[tag].slug}`">
                {{ tags[tag] && tags[tag].name }}
              </NuxtLink>
            </div>
            <p>
              {{ blog.description }}
            </p>
          </div>
        </div>
        <div>
          <div>
            <!-- table of contents -->
            <nav v-if="blog.toc.length">
              <h5>Table of contents:</h5>
              <ul>
                <li
                  v-for="link of blog.toc"
                  :key="link.id"
                  :class="{
                    'font-normal ml-1 mb-1': link.depth === 2
                  }"
                >
                  <nuxtLink
                    :to="`#${link.id}`"
                    :class="{
                      'py-2': link.depth === 2,
                      'ml-1 pb-2 text-gray-700': link.depth === 3
                    }"
                    >{{ link.text }}</nuxtLink
                  >
                </li>
              </ul>
            </nav>
          </div>
          <div>
            <!-- content from markdown -->
            <nuxt-content :document="blog" />
            <!-- content readalso component -->
            <ReadAlso :items="readMore" />
            <!-- content author component -->
            <Author :author="blog.author" />
            <!-- prevNext component -->
            <PrevNext :prev="prevNext.prev" :next="prevNext.next" />
          </div>
        </div>
      </section>
      <Footer />
    </article>
  </main>
</template>

<script>
import { mapState } from 'vuex'

export default {
  async fetch({ i18n, context, params, store, store: { dispatch, getters } }) {
    await dispatch('getBlog', params)
    await dispatch('getBlogTags', { tags: getters.getBlog.data.tags })
    await dispatch('getReadMore', {
      slug: params.slug,
      tags: Object.keys(store.state.blogTags.data)
    })
    await dispatch('getPrevNext', params)
  },
  computed: {
    ...mapState({
      blog: (state) => state.blog.data,
      tags: (state) => state.blogTags.data,
      readMore: (state) => state.readMore.data,
      prevNext: (state) => state.prevNext.data
    })
  }
}
</script>

Directory Structures

Here are the directory structures from the Nuxtjs content website with vuex:

.
├── README.md
├── assets
│   └── css
│       └── tailwind.css
├── components
│   ├── Logo.vue
│   ├── cards
│   │   ├── Blog.vue
│   │   └── Blogs.vue
│   ├── footer
│   │   └── Footer.vue
│   ├── header
│   │   └── Header.vue
│   └── navigation
│       ├── Bottom.vue
│       └── Top.vue
├── config
│   └── statusTypes.js
├── content
│   ├── blogs
│   │   ├── write-second-article-in-this-website.md
│   │   ├── write-third-article-in-this-website.md
│   │   └── write-forth-article-in-this-website.md
│   └── tags
│       ├── javascript.md
│       ├── nuxtjs.md
│       └── web_development.md
├── jest.config.js
├── jsconfig.json
├── layouts
│   └── default.vue
├── utils
│   └── index.js
├── middleware
├── nuxt.config.js
├── package.json
├── pages
│   ├── blog
│   │   ├── _slug.vue
│   │   ├── author
│   │   │   └── _author.vue
│   │   └── tag
│   │       └── _tag.vue
│   └── index.vue
├── plugins
│   └── README.md
├── renovate.json
├── store
│   └── index.js
├── stylelint.config.js
├── tailwind.config.js
└── yarn.lock

Closing

There are several ways to deploy your Nuxtjs static content website, you could use any hosting service provider or use free static website hosting like Vercel and Netlify.

I have been using these hosting service provider for quite some time for publish several of my web projects. As a web developer, these hosting providers have been extremely helpful in terms of hosting my static websites.

All of these are the sample and descriptions of how the possibilities on Nuxt.js Static Content using Vuex. Thank you for stopping by! The source code are in GitHub. Here is the demo link on NuxtJS static content using vuex.

Topics

Recent Blog List Content:

Archive