Creating a blog with Nuxt Content

by Gilbert Tanner on Nov 02, 2020 · 10 min read

Creating a blog with Nuxt Content

Nuxt Content is a git files based headless CMS that allows you to create a blog or documentation site from Markdown, JSON, YAML, XML, and CSV files. It includes:

  • Full-text search
  • Static site generation support with nuxt generate
  • A Powerful QueryBuilder API (MongoDB like)
  • Syntax highlighting to code blocks in markdown files using PrismJS.
  • Table of contents generation

and much more. In this post, we'll go through building a blog with Nuxt Content and deploying it on Netlify or Github Pages.

The post will focus on getting the functionality of the blog to work and won't cover styling the blog, but you can find the complete code, including the style, which is based on the Kick-Off repository on my Github.

Getting started

To get started, let's create a Nuxt project:

npx create-nuxt-app my-nuxt-content-blog
or
yarn create nuxt-app my-nuxt-content-blog

Select:

Project name: <your-project-name>
Programming language: JavaScript
Package manager: yarn or npm (npm is used in this article)
UI framework: None
Nuxt.js modules: PWA, Content
Linting tools: Prettier, Lint staged files
Testing framework: None
Rendering mode: Universal (SSR / SSG)
Deployment target: Server (Node.js hosting)
Development tools: None
Continuous integration: None
Version control system: Git

This creates a folder structure like:

tree -L 1 my-nuxt-content-blog

my-nuxt-content-blog
├── assets
├── components
├── content
├── layouts
├── middleware
├── node_modules
├── nuxt.config.js
├── package.json
├── package-lock.json
├── pages
├── plugins
├── README.md
├── static
└── store

10 directories, 4 files

The initial nuxt.config.js looks as follows:

export default {
  // Global page headers (https://go.nuxtjs.dev/config-head)
  head: {
    title: '<your-project-name>',
    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' }
    ]
  },

  // Global CSS (https://go.nuxtjs.dev/config-css)
  css: [
  ],

  // Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins)
  plugins: [
  ],

  // Auto import components (https://go.nuxtjs.dev/config-components)
  components: true,

  // Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules)
  buildModules: [
  ],

  // Modules (https://go.nuxtjs.dev/config-modules)
  modules: [
    // https://go.nuxtjs.dev/pwa
    '@nuxtjs/pwa',
    // https://go.nuxtjs.dev/content
    '@nuxt/content',
  ],

  // Content module configuration (https://go.nuxtjs.dev/config-content)
  content: {},

  // Build Configuration (https://go.nuxtjs.dev/config-build)
  build: {
  }
}

Next cd into the project and install Nuxt Content:

cd my-nuxt-content-blog
npm install @nuxt/content

To now start the dev server run:

npm run dev

nuxt_default_app

Add the first post

Next, let's create an example blog post. To do this, create a content folder, which will be the home of all your blog posts, and then at a folder called my-first-blog-post. The name of the folder will be the slug (URL) that is shown on the webpage.

mkdir content
mkdir content/my-first-blog-post

The content module can parse Markdown, CSV, YAML, JSON, JSON5, or XML. I recommend writing your articles in markdown as it has an easy to learn typing system that can be really powerful.

Now let's create our first blog post.

touch content/my-first-blog-post/index.md

For the title, description, and thumbnail, we'll use Front Matter. Then we'll also add some code, a math equation, and another image to the post.

---
title: My First blog Post
description: This is my first blog post
date: 2020-10-10
image: index.jpg
tags:
  - test
---

## Some headline

Code block: 
```javascript
console.log("Hello World")
```

Lift($L$) can be determined by Lift Coefficient ($C_L$) like the following equation.

$$
L = \frac{1}{2} \rho v^2 S C_L
$$

<v-img src="index.jpg" alt="Index"></v-img>

Building the pages

We're going to build 2 pages:

  • Homepage - /pages/index.vue
  • Post page - /pages/blog/_slug.vue

The starter code on Github also has a Header, Footer, and Search/Articles page, but I won't show how to create those in this article.

Blog Page

To view a single post, we need a dynamic route. To define a dynamic route in Nuxt, we need to create a .vue file with an underscore as a prefix. By creating a file called _slug.vue, we can use params.slug inside the file.

my-nuxt-content-blog/
  pages/
    blog/
      _slug.vue

We can use the asyncData method in our page component to fetch our article content before the page has been rendered.

<script>
  export default {
    async asyncData({ $content, params, error }) {
      // fetch our article here
    }
  }
</script>

Inside the asyncData method, we can fetch the article using the $content instance that is globally injected. To get the article where the directory name matches the slug, we can use the where method. If no article could be found, we'll return a 404 error. Furthermore, we'll fetch two other articles that we'll link to at the end of the article.

async asyncData({ $content, params, error }) {
  try {
    const [article] = await $content({ deep: true })
      .where({ dir: `/${params.slug}` })
      .fetch()
    const moreStories = await $content({ deep: true })
      .only(['title', 'image', 'path'])
      .where({ title: { $ne: article.title } })
      .sortBy('createdAt', 'desc')
      .limit(3)
      .fetch()
    return { article, moreStories }
  } catch (err) {
    error({
      statusCode: 404,
      message: 'Page could not be found',
    })
  }
},

The article can now be displayed using the component.

<template>
  <div class="post">
    <h1>{{ article.title }}</h1>
    <nuxt-content :document="article" />
  </div>
</template>

Adding a Table of Content

The article variable includes a toc attribute, which contains the text and id of all headlines. To create a table of content, we can loop over this array and create an unordered list.

components/TableOfContent.vue:

<template>
  <section class="tableofcontent">
    <h3>Table of Content</h3>
    <div id="toc">
      <ul>
        <li
          v-for="link of toc"
          :key="link.id"
          :class="{ toc2: link.depth === 2, toc3: link.depth === 3 }"
        >
          <NuxtLink :to="`#${link.id}`">{{ link.text }}</NuxtLink>
        </li>
      </ul>
    </div>
  </section>
</template>

<script>
export default {
  props: ['toc'],
}
</script>

pages/blog/_slug.vue:

<template>
  <div class="post">
    <h1>{{ article.title }}</h1>
    <TableOfContent :toc="article.toc" />
    <nuxt-content :document="article" />
  </div>
</template>

<script>
import Prism from '~/plugins/prism'
import TableOfContent from '~/components/TableOfContent.vue'
...
</script>

Homepage

For the homepage, we'll create a file called index.vue inside the pages directory.

my-nuxt-content-blog/
  pages/
    index.vue

On the homepage, we'll display all articles inside the content directory. It's a good idea to use the only() method to only get the parameters needed, which will reduce loading times.

async asyncData({ $content, params }) {
  try {
    const articles = await $content({ deep: true })
      .only(['title', 'description', 'image', 'path'])
      .sortBy('createdAt', 'desc')
      .fetch()
    return { articles }
  } catch (err) {
    error({
      statusCode: 404,
      message: 'Page could not be found',
    })
  }
},

With all the articles loaded they are ready for displaying. For an example, check out the Github repository.

Setting up Prism.js

Prism.js comes along with Nuxt Content, and you can easily change themes as described in the documentation.

If you want to use more advanced highlighting features like line numbers, copy to clipboard, or highlighting keywords, you'll have to disable the server-side rendering and create your own plugin as shown by Matthew Blewitt.

Disable server-side code highlighting:

nuxt.config.js:

content: {
  markdown: {
    prism: {
      theme: false,
    }
  }
},

Prism plugin (plugins/prism.js):

import Prism from "prismjs";

// Include a theme:
import "prismjs/themes/prism-tomorrow.css";

// Include the toolbar plugin: (optional)
import "prismjs/plugins/toolbar/prism-toolbar";
import "prismjs/plugins/toolbar/prism-toolbar.css";

// Include the line numbers plugin: (optional)
import "prismjs/plugins/line-numbers/prism-line-numbers";
import "prismjs/plugins/line-numbers/prism-line-numbers.css";

// Include the line highlight plugin: (optional)
import "prismjs/plugins/line-highlight/prism-line-highlight";
import "prismjs/plugins/line-highlight/prism-line-highlight.css";

// Include some other plugins: (optional)
import "prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard";
import "prismjs/plugins/highlight-keywords/prism-highlight-keywords";
import "prismjs/plugins/show-language/prism-show-language";

// Include additional languages
import "prismjs/components/prism-bash.js";

// Set vue SFC to markdown
Prism.languages.vue = Prism.languages.markup;

export default Prism;

Now import the plugin inside pages/blog/_slug.vue and call Prism.highlightAll() in the mounted method.

<script>
import Prism from '~/plugins/prism'
import TableOfContent from '~/components/TableOfContent.vue'
export default {
  ...  
  mounted() {
    Prism.highlightAll()
  },
}
</script>

code_highlighting

Getting images to work

Nuxt Content doesn't support images in markdown yet, but most blogs will want to use images. A recommended workaround is to create a global Vue component that loads the image.

components/global/VImg.vue:

<template>
  <img :src="imgSrc()" :alt="alt" />
</template>

<script>
export default {
  props: {
    src: {
      type: String,
      required: true
    },
    alt: {
      type: String,
      required: true
    }
  },
  methods: {
    imgSrc() {
      try {
        const { article } = this.$parent;
        return require(`~/content${article.dir}/images/${this.src}`);
      } catch (error) {
        return null
      }
    }
  }
}
</script>

Now instead of using ![Alt text](image.jpg) or <img src="image.jpg" alt="Alt text"> use <v-img src="image.jpg" alt="Alt text">.

Implementing Latex (optional)

If you're interested in writing articles that include math equations, you can also add Latex support using remark-math.

First, install remark-math and rehype-mathjax.

npm i remark-math@3.0.1 rehype-mathjax@3.0.0
or
yarn add remark-math@3.0.1 rehype-mathjax@3.0.0

Then register both plugins in your nuxt.config.js file.

content: {
    markdown: {
      prism: {
        theme: false,
      },
      remarkPlugins: ['remark-math'],
      rehypePlugins: ['rehype-mathjax'],
    },
  },

Adding Google Analytics (optional)

To add Google Analytics, you can either use the official Google Analytics module or create your own plugin as described in the Nuxt documentation.

/* eslint-disable */

export default ({ app }) => {
    /*
     ** Only run on client-side and only in production mode
     */
    if (process.env.NODE_ENV !== 'production')
      return /*
       ** Include Google Analytics Script
       */
    ;(function (i, s, o, g, r, a, m) {
      i['GoogleAnalyticsObject'] = r
      ;(i[r] =
        i[r] ||
        function () {
          ;(i[r].q = i[r].q || []).push(arguments)
        }),
        (i[r].l = 1 * new Date())
      ;(a = s.createElement(o)), (m = s.getElementsByTagName(o)[0])
      a.async = 1
      a.src = g
      m.parentNode.insertBefore(a, m)
    })(
      window,
      document,
      'script',
      'https://www.google-analytics.com/analytics.js',
      'ga'
    )
    /*
     ** Set the current page
     */
    ga('create', 'UA-XXXXXXXX-X', 'auto')
    /*
     ** Every time the route changes (fired on initialization too)
     */
    app.router.afterEach((to, from) => {
      /*
       ** We tell Google Analytics to add a `pageview`
       */
      ga('set', 'page', to.fullPath)
      ga('send', 'pageview')
    })
  }

Replace UA-XXXXXXXX-X with your Google Analytics tracking ID.

Then, add the plugin inside nuxt.config.js:

plugins: [
  ...
  { src: '~/plugins/ga.js', mode: 'client' },
],

Adding Sitemap

A sitemap tells search engines how your website is structured. This will allow search engines to see all pages of your website. Nuxt has its own sitemap module that can be easily integrated with Nuxt Content, as demonstrated by Gareth Redfern.

First, install @nuxtjs/sitemap and add it to the nuxt.config.js file.

npm i @nuxtjs/sitemap
or
yarn add @nuxtjs/sitemap

nuxt.config.js:

// Modules (https://go.nuxtjs.dev/config-modules)
modules: [
  // https://go.nuxtjs.dev/pwa
  '@nuxtjs/pwa',
  // https://go.nuxtjs.dev/content
  '@nuxt/content',
],

Next, you need to add the sitemap configuration object in your nuxt.config.js file. In it, you'll need to add your hostname (site URL) and a routes property, which lists all the dynamic routes. This is necessary since the module doesn't automatically pick up dynamic routes.

To keep things clean Gareth Redfern recommends creating a getRoutes.js file inside the utils folder that loads all the posts.

utils/getRoutes.js:

export default async () => {
  const { $content } = require('@nuxt/content')
  const files = await $content({ deep: true })
    .only(['path', 'updatedAt'])
    .fetch()
  return files.map((file) => {
    return {
      url: 'blog/' + file.path.replace('index', ''),
      lastmod: Date.parse(file.updatedAt),
    }
  })
}

nuxt.config.js:

...
// Sitemap
sitemap: {
  hostname: '<domain>',
  routes() {
    return getRoutes();
  },
},

Now you should be able to navigate to https://www.yoursite.com/sitemap.xml to see your sitemap.

Adding Social Media & SEO Meta Data

Meta tags provide information about the webpage in the HTML of the document. This information is called "metadata" and while it is not displayed on the page itself, it can be read by search engines and web crawlers. - Moz

Gareth Redfern has a great article showing you how to add social media and SEO metadata to your Nuxt blog.

Generating a static site

To deploy the blog website to a static site hosting platform like Netlify or Github Pages we can run the nuxt generate command, which will build our app adding all our webpack assets and creating .js bundles for us and then export our html, css, js and images as static assets.

Since Nuxt 2.14+, it also automatically crawls all your links and generates your routes based on those links. Therefore you do not need to do anything for your dynamic routes to be crawled.

We can then use nuxt start to check our website one last time before deployment.

Deploying on Netlify

To deploy your website on Netlify, navigate to netlify.com, click on the "Sign up" button and sign up with either Github, Gitlab, Bitbucket, or Email account.

Then go to "New site from Git" and choose the Git provider where your site's source code is hosted.

connect_to_git_provider

Pick the right repository:

pick_a_repository

Select the branch and add build settings:

build_options_and_deploy

After a few minutes, your website should be deployed. For more information on deploying Nuxt with Netlify, check out the Nuxt documentation and Netlify documentation.

Deploying on Github Pages

You can also deploy your website on Github pages. For this, I recommend checking out "Nuxt.js — gh-pages deployment" from Anna Kozyreva.

Credit / Other resources

Free Machine Learning Newsletter

Table of Content