DEV Community

Matthew Blewitt
Matthew Blewitt

Posted on

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

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".

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:

---
title: Praesent sed neque efficitur
description: Aliquam ultrices ex eget leo tincidunt
date: 2020-10-10
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

-

<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
...

-

<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
...

-

<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:

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.

<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:

<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 ¯\_(ツ)_/¯.

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

<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 https://matthewblewitt.com

Happy coding!

Top comments (10)

Collapse
 
muhaddimu profile image
Muhaddis • Edited

Great post with a great explanation.

I am wondering why

<v-img src="Featured.png" alt="NPM Scripts"></v-img>

is not working inside the Markdown file. However, it's working in Vue component. I registered the component globally as well. Any ideas ?

Collapse
 
matthewblewitt profile image
Matthew Blewitt

Are you getting console errors? In that snippet of code I can see you haven't closed the closing tag and the src file name is capitalized.

Collapse
 
muhaddimu profile image
Muhaddis • Edited

I am not getting any errors in the console. Even though, I have tried to console.log(error)in the catch block.

For some reasons, the same code is working in the Vue slug.vue Component but not inside the Markdown files. Tried console logging the parameters before the return, it's showing the correct path but the image is not displaying.

Tried this approach, it's working fine:

 imgSrc() {
      try {
        return require(`~/content${this.src}`)
      } catch (error) {
        return null
      }
    }

See: github.com/MuhaddiMu/Portfolio/blo...

Now I have to pass the complete path relevant path /blog/slug-here/images/Featured.png

Fixed the typo in the comment. Is capital file name really matters?

Collapse
 
javierpomachagua profile image
Javier Pomachagua

Nice post!
I have a question, as a developer I can write my own post in markdown file but what if my client no dev want to write his post. ¿Should I build a UI for writting his post? ¿Can I use API post to nuxt content?

Collapse
 
taylerramsay profile image
Ramsay Design

Did you find a way to access local images from your frontmatter? I have been trying to add an image url in my frontmatter pointing to my assets folder so I can use it in my meta data to create a twitter card with the blog posts image in it when sharing the on social media. From what I'v read the content module doesn't use webpack when compiling the markdown files so the image url is not correct.

Collapse
 
9mza profile image
9MZa

This post make me fun with nuxt again. Thanks Matthew Blewitt. :D

Collapse
 
tsanjaya profile image
Tony S.

Nice post :)

Collapse
 
elirehema profile image
Elirehema Paul

Nice post. I used this as a stepping poit to create my demo blog elirehema.github.io/nuxt_content/.

Collapse
 
wacoss profile image
wacoss

Hi, Can Nuxt read content remote from an API?

Collapse
 
jswhisperer profile image
Greg, The JavaScript Whisperer

Great post!
Was wondering if you had an idea how to create a sitemap with npmjs.com/package/@nuxtjs/sitemap since all the routes are dynamic and outside /pages ?