Nuxt Content

Migrating from Netlify CMS...

Table of Contents

Nuxt recently updated their Content Module alongside the full static build and I've started using this combo in my projects. In particular, my personal website (the one your reading this on) had been using a git-based headless CMS, Netlify CMS which works and integrates well with Netlify hosting. Netlify CMS is great and provides a intuitive user interface that makes it easy to create and manage content but I prefer to use a first-party module as opposed to a third-party one most of the time and especially with an excellent framework like Nuxt. Netlify CMS does have the advantage of working with several frameworks, is open-source, and extensible.

What is Nuxt Content?

Nuxt Content is a git-based, headless CMS. Git-based makes it easy to integrate content creation and management into your workflow. Using a static build, we are able to render the content files and serve them as static assets on a CDN. This results is an optimized and performant site that serves a small payload and is quick to render.

You can install the Nuxt Content module by selecting the option during the installation process using npx create-nuxt-app [name] or manually after installation with npm install @nuxtjs/content.

Nuxt Content can be used to write content in the /content directory, fetch it using the global $contentinstance and display it inside a component with the <nuxt-content> tag.

Converting old content to a new format (JSON to Markdown)

Before conversion my content was in JSON format from using the Netlify CMS editor. I could have left the content in JSON format since Nuxt Content supports a variety of formats but I wanted to use features that are available in Markdown only.

Nuxt Content supports Markdown, CSV, XML, YML/YAML, and JSON/JSON5.

Markdown features in Nuxt Content

YAML Front Matter

In Markdown files, you can use YAML front matter to easily add meta information to content. This is helpful for SEO concerns as well as including a thumbnail or author information for the content.

Using Vue Components

Vue components can be registered in the components/global directory to be used in Markdown files and rendered in <nuxt-content>. This feature is great for being able to reuse components for common pieces of content.

Automatic Table of Contents

A table of contents is automatically generated by using the h2 and h3 tags in the Markdown file and compiling them into an array of objects including the header id. We can iterate through those links in the template section of the components to display a clickable link for each section of the document.

Anchor Name Issue

One issue I ran into was with the anchors I was using for some elements. Heading tags (h1 - h6) are automatically adding to the toc (table of contents) array. However, other element tags that you may want to link to would need an associated anchor tag with an id.

---
  title: Anchor links for other elements
  description: "How do I use anchor links with elements other than headings in Markdown
---

# Table of contents
<a href="#anchor">Anchor</a>
<a href="#non-header-anchor">Anchor</a>

## Anchor
<a id="non-header-anchor">Non header anchor</a>

Nuxt Content currently has a bug that prevents the Markdown parser from parsing anchor tags that are missing a href attribute. A pull request has been accepted to fix it in a future release.

A workaround for this issue is to add an href attribute with an empty value. This will prevent the error from occurring and file will be parse as normal.

hello.md

---
  title: Anchor links for other elements
  description: "How do I use anchor links with elements other than headings in Markdown
---

# Table of contents
<a href="#anchor">Displaying Content</a>
<a href="#non-header-anchor">Anchor</a>

## Anchor
<a href="" id="non-header-anchor">Non header anchor</a>

Even though the value is an empty string, it will still pass the check in the parser.

Fetching content and sorting

Nuxt Content makes it easy to fetch your content with both filter and sort methods. In their example Content blog, they use filter to pick out only the title and slug for the previous and next links then sort the results by the createdAt attribute value which is automatically inserted into the YAML front matter when a new Markdown file is created in the /content directory.

On my blog index page, I used a similar mindset to sort my posts in ascending date order which put the most recent posts at the top of the list.

pages/blog/index.vue

async asyncData({ $content, params }) {
  const posts = await $content('blog', params.slug)
    .sortBy('date', 'desc')
    .fetch()
  return {
    posts
  }
}

This could be refactored to use the filter method. While I'm using most of the content properties, I could use the without(keys) method to exclude the few I'm not using.

On the individual page for the blog post, we use the params object once again to fetch the appropriate post.

async asyncData({ $content, params }) {
  const post = await $content('blog', params.blog).fetch()
  return {
    post
  }
},

params.blog is used because my pages is named _blog. Use the dynamic page name minus the underscore to get the correct parameter.

This lets you display the content of the file inside the <nuxt-content> tag using the :document attribute to pass the data.

<nuxt-content :document="post" />

Creating the table of contents from the toc array

The Content module automatically creates a toc array for the content file that consists of all the h2 and h3 tags present. You can use these 2 tags as headings in your Markdown file and then use the toc to display a handy table of contents on the page.

We want to display the table of contents above the <nuxt-content> tag.

<template>
  <h2>
    Table of Contents
  </h2>
  <ul>
    <li v-for="link of post.toc" :key="link.id">
      <NuxtLink :to="`#${link.id}`">{{ link.text }}</NuxtLink>
    </li>
  </ul>
</template>

This will loop through the toc array and display a text link for each heading.

Conclusion

That's it for now. I still need to deep-dive into the Content module to learn more about what it can do. It is actively being developed right now so I'm sure there are plenty of new features (and bug fixes) to come. It already looks really strong and I'm excited for what is to come!