Build a static generated blog with Nuxt v2.13.0 and @nuxt/content

This blog will be built with markdown and have syntax highlighting

26/06/2020

Nuxt v2.13.0 just dropped and in this version they've made it even easier to generate a fully static website. In this blog post: Going full static, they detail the updates and importantly the new command to generate the site as static html: nuxt export.

You can combine this new functionality with @nuxt/content, a new module that acts as a Git-based Headless CMS. These are some of the features you get out of the box:

  • Syntax highlighting to code blocks in markdown files using PrismJS.
  • Markdown, CSV, YAML, JSON(5)
  • Vue components in Markdown

Getting started

Let's get started. Run the create nuxt project command and select universal mode:

npx create-nuxt-app nuxt-blog-starter

Once complete cd in the newly created folder:

npm install @nuxt/content

Setting up config

In the nuxt.config file we to add the @nuxt/content module and set the site to target: "static".

nuxt.config.js
export default {
  ...
  target: "static",
  mode: "universal",
  ...
  modules: ["@nuxt/content"],
  content: {
    markdown: {
      prism: {
        theme: false,
      },
    },
  }
  ...
};

Add the first post

Create a content folder in the root directory with the following structure and add your markdown file. The folder name will determine the url of the post.

nuxt-blog-starter/
  components/
  pages/
  content/
    posts/
      my-first-blog-post/
        index.md
        img/
          image.jpg

Add the Markdown file with the YAML header format:

content/posts/my-first-blog-post/index.md
---
title: Praesent sed neque efficitur
description: Aliquam ultrices ex eget leo tincidunt
date: 2020-06-26
image: index.jpg
tags:
  - test
  - another
---

Ut ut justo arcu. Praesent sed neque efficitur,
venenatis diam mollis, lobortis erat. Praesent eget
imperdiet odio, tincidunt eleifend mauris. Sed luctus lacinia auctor.

Building the views

We're going to build 3 views:

  • Single post - /pages/posts/_slug.vue
  • Post lists - /pages/index.vue
  • Tags lists - /pages/tags/_slug.vue

Create the single vue

To view a single post we need to create a dynamic route page:

nuxt-blog-starter/
  pages/
    posts/
      _.slug.vue
...
pages/posts/_slug.vue
<template>
  <div class="post">
    <h1>{{ post.title }}</h1>
    <p class="lead">{{ post.description }}</p>
    <nuxt-content :document="post" />
  </div>
</template>
<script>
export default {
  async asyncData({ params, error, $content }) {
    try {
      const postPath = `/posts/${params.slug}`;
      const [post] = await $content("posts", { deep: true })
        .where({ dir: postPath })
        .fetch();
      return { post };
    } catch (err) {
      error({
        statusCode: 404,
        message: "Page could not be found",
      });
    }
  },
  head() {
    return {
      title: this.post.title,
      meta: [
        {
          hid: "description",
          name: "description",
          content: this.post.description,
        },
      ],
      link: [
        {
          rel: "canonical",
          href: "https://nuxt-blog.com/" + this.post.dir,
        },
      ],
    };
  },
};
</script>

Now if you run npm run dev and go to http://localhost:3000/posts/my-first-blog-post you will see the post you created.

Creating the post list page

Let's create the index of the blog and list all the posts.

nuxt-blog-starter/
  pages/
    index.vue
...
/pages/index.vue
<template>
  <div class="posts">
    <h1>Posts</h1>
    <div v-for="post in posts" :key="post.dir">
      <h3 class="heading">{{ post.title }}</h3>
      <p>{{ post.description }}</p>
      <p class="tags">
        <span v-for="tag in post.tags" :key="tag" class="tag">
          <nuxt-link :to="`/tags/${tag}`">{{ tag }}</nuxt-link>
          &nbsp;
        </span>
      </p>
      <nuxt-link :to="post.dir">Read more</nuxt-link>
    </div>
  </div>
</template>
<script>
export default {
  async asyncData({ params, error, $content }) {
    try {
      const posts = await $content("posts", { deep: true }).fetch();
      return { posts };
    } catch (err) {
      error({
        statusCode: 404,
        message: "Page could not be found",
      });
    }
  },
  head() {
    return {
      title: "Nuxt blog",
      meta: [
        {
          hid: "description",
          name: "description",
          content: "Cool nuxt blog",
        },
      ],
      link: [
        {
          rel: "canonical",
          href: "https://nuxt-blog.com/",
        },
      ],
    };
  },
};
</script>

Creating the tags view

Finally let's create the tags lists page.

nuxt-blog-starter/
  pages/
    tags/
      _.slug.vue
...
/pages/tags/_slug.vue
<template>
  <div class="posts">
    <h1>Tags: {{ $route.params.slug }}</h1>
    <div v-for="post in posts" :key="post.dir">
      <h3 class="heading">{{ post.title }}</h3>
      <p>{{ post.description }}</p>
      <p class="tags">
        <span v-for="tag in post.tags" :key="tag" class="tag">
          <nuxt-link :to="`/tags/${tag}`">{{ tag }}</nuxt-link>
          &nbsp;
        </span>
      </p>
      <nuxt-link :to="post.dir">Read more</nuxt-link>
    </div>
  </div>
</template>
<script>
export default {
  async asyncData({ params, error, $content }) {
    try {
      const posts = await $content("posts", { deep: true })
        .where({ tags: { $contains: params.slug } })
        .fetch();
      return { posts };
    } catch (err) {
      error({
        statusCode: 404,
        message: "Page could not be found",
      });
    }
  },
  head() {
    return {
      title: "Tags",
      meta: [
        {
          hid: "description",
          name: "description",
          content: "Cool nuxt blog tags",
        },
      ],
      link: [
        {
          rel: "canonical",
          href: "https://nuxt-blog.com/tags",
        },
      ],
    };
  },
};
</script>

Setting up Prism.js

Prism comes along with @nuxt/content but it renders server-side. If we want to use plugins such as line numbers we need to have it render in the client. To do this create a plugin called prism:

/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;

Import the prism plugin and call Prism.highlightAll(); in mounted.

pages/posts/_slug.vue
<template>
  <div class="post">
    <h1>{{ post.title }}</h1>
    <p class="lead">{{ post.description }}</p>
    <nuxt-content :document="post" />
  </div>
</template>
<script>
import Prism from "~/plugins/prism";
export default {
  async asyncData({ params, error, $content }) {
    try {
      const postPath = `/posts/${params.slug}`;
      const [post] = await $content("posts", { deep: true })
        .where({ dir: postPath })
        .fetch();
      return { post };
    } catch (err) {
      error({
        statusCode: 404,
        message: "Page could not be found",
      });
    }
  },
  mounted() {
    Prism.highlightAll();
  },
  head() {
    return {
      title: this.post.title,
      meta: [
        {
          hid: "description",
          name: "description",
          content: this.post.description,
        },
      ],
      link: [
        {
          rel: "canonical",
          href: "https://nuxt-blog.com/" + this.post.dir,
        },
      ],
    };
  },
};
</script>

Dealing with images

@nuxt/content doesn't support images in the markdown yet. The suggested way around this is to use a Vue component and require the images with webpack:

/components/VImg.vue
<template>
  <div class="img">
    <img :src="imgSrc()" :alt="alt" />
  </div>
</template>

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

N.B. @nuxt/content requires Vue components used in markdown to use <v-img src="index.jpg" alt="Index"></v-img> format.

Using images this way creates a warning error in the console but it doesn't affect the build ¯\_(ツ)_/¯.

content/posts/my-first-blog-post/index.md
---
title: Praesent sed neque efficitur
description: Aliquam ultrices ex eget leo tincidunt
date: 2020-10-10
image: index.jpg
tags:
  - test
  - another
---
<v-img src="index.jpg" alt="Index"></v-img>
Ut ut justo arcu. Praesent sed neque efficitur,
venenatis diam mollis, lobortis erat. Praesent eget
imperdiet odio, tincidunt eleifend mauris. Sed luctus lacinia auctor.

Now that we've built this component we can add a featured image to the single view

pages/posts/_slug.vue
<template>
  <div>
    <div class="post-header">
      <h1 class="h1 post-h1">{{ post.title }}</h1>
      <p v-if="post.description" class="excerpt">
        {{ post.description }}
      </p>
      <div class="post-details">
        <div class="tags">
          <span v-for="(tag, i) in post.tags" :key="i" class="tag">
            <nuxt-link :to="'/tags/' + tag">#{{ tag }}</nuxt-link>
          </span>
        </div>
        <div class="date">{{ post.date | date }}</div>
      </div>
      <v-img
        v-if="post.image"
        class="post-img"
        :src="post.image"
        :alt="post.title"
      ></v-img>
    </div>
    <nuxt-content :document="post" />
  </div>
</template>
<script>
import VImg from "~/components/VImg";

export default {
  components: {
    VImg
  },
  async asyncData({ params, error, $content }) {
    try {
      const postPath = `/posts/${params.slug}`;
      const [post] = await $content("posts", { deep: true })
        .where({ dir: postPath })
        .fetch();
      return { post };
    } catch (err) {
      error({
        statusCode: 404,
        message: "Page could not be found"
      });
    }
  },
  mounted() {
    Prism.highlightAll();
  },
  head() {
    return {
      title: this.post.title,
      meta: [
        {
          hid: "description",
          name: "description",
          content: this.post.description
        }
      ],
      link: [
        {
          rel: "canonical",
          href: "https://matthewblewitt.com/posts/" + this.post.slug
        }
      ]
    };
  }
};
</script>

Generating the static files

We've created our view and now we can generate our static files in dist/ folder.

nuxt build && nuxt export

We can also run those static files directly by running:

nuxt serve

Conclusion

The blog is now ready to be uploaded to your static hosting of choice on Netflify or an S3 bucket. I've uploaded a basic version of this onto Github https://github.com/matthewblewitt/nuxt-static-generated-blog.

Originally posted on

Happy coding!