Build a static generated blog with Nuxt v2.13.0 and @nuxt/content
Tags: nuxt, vue
Published: 2020-06-25 14:36:46

---
title: Build a static generated blog with Nuxt v2.13.0 and @nuxt/content
published: true
description: This blog will be built with markdown and have syntax highlighting
tags:
  - nuxt
  - vue
---

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](https://nuxtjs.org/blog/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](https://content.nuxtjs.org/) 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:
```bash
npx create-nuxt-app nuxt-blog-starter
```

Once complete `cd` in the newly created folder:
```bash
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"`.

```js{3,6-13}[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:

```markdown[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
---

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

```js[/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.
```vue{9,25-27}[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](https://github.com/nuxt/content/issues/106). The suggested way around this is to use a Vue component and require the images with webpack:

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

```markdown{10}[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

```vue{16-21,27}[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.

```bash
nuxt build && nuxt export
```

We can also run those static files directly by running:

```bash
nuxt serve
```

## Conclusion
The blog is now ready to be uploaded to your static hosting of choice on [Netflify](https://nuxtjs.org/faq/netlify-deployment/) or an S3 bucket. I've uploaded a basic version of this onto [Github https://github.com/matthewblewitt/nuxt-static-generated-blog](https://github.com/matthewblewitt/nuxt-static-generated-blog). 

Originally posted on [https://matthewblewitt.com](https://matthewblewitt.com)

Happy coding!