Document-Driven Mode in Nuxt Content v2

Cecelia Martinez - Jan 20 '23 - - Dev Community

The new Document-Driven Mode in Nuxt Content v2 automatically creates pages and routes from the files within your content directory. You can use Markdown components (MDC syntax), layouts, and global variables to enhance your Markdown files with style, interactivity, navigation, and more.

What makes Document-Driven Mode different?

If you’ve used the v1 of the Content module, you’re already familiar with the file-based routing and query system. You also get the auto-imported component and rendering option benefits provided by Nuxt.

Document-Driven Mode provides a different developer experience for letting your content “drive” the structure and generation of your site. There are some key features and differences when using Document-Driven Mode.

Note: Document-Driven Mode is an optional setting in Content v2.

Direct binding between content directory and pages

This means that every file in your content directory automatically becomes a page. This means you no longer need a pages directory with .vue files for route creation. Creating an index.md file will bind to the default page for each directory.

content/
    index.md
    tutorials/
        index.md
    nuxt-content-module.md
    nuxt-ionic-module.md
    blog/
        index.md
    blog-post-1.md
    blog-post-2.md
Enter fullscreen mode Exit fullscreen mode

Layout binding in Markdown files

Because you are no longer creating .vue pages, leverage Layouts for ensuring specific types of content have a certain style or layout. For example, you may have tutorial and blog content types with different layouts. For example, create tutorial.vue and blog.vue files in your layouts directory, then reference the layout in the frontmatter of your Markdown file.

Example:

---
title: Making connections at conferences
description: How to push past the discomfort and make new connections when attending tech conferences.
date: 2022-11-23
img: 
tags: [community, career]
layout: blog
---
Enter fullscreen mode Exit fullscreen mode

Markdown Components (MDC) Syntax

This feature isn’t specific to Document-driven Mode, but is an important new feature of Content v2 when building directly from Markdown files. MDC Syntax lets you insert Vue components directly into your Markdown files. You can even pass props within the Markdown file.

MDC Syntax is useful when you want to use a Vue component in a single Markdown file. For example, this is an index.md file, which just contains the <Hero> component.

:hero
Enter fullscreen mode Exit fullscreen mode

Global variables

Nuxt Content v2 provides a useContent() composable with page, surround, navigation, and globals variables that make it easy to use data from the current route before the page renders. This is helpful for rendering the page or creating navigation for next and previous in a blog context. The following example is from the Nuxt Content docs:

<script setup lang="ts">
const { prev, next } = useContent()
</script>
<template>
  <div>
    <NuxtLink v-if="prev" :to="prev._path">{{ prev.title }}</NuxtLink>
    <NuxtLink v-if="next" :to="next._path">{{ next.title }}</NuxtLink>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

There are other aspects of Document-Driven Mode, such as default page slots for when document are empty or not found, and some configuration options. Check out the documentation for more details and examples.

Creating a Document-Driven Site

For this blog post, I’ll be using the updated version of my own site, ceceliacreates.com. You can find the source code in the repository here.

I first built this site with Nuxt Content v1. If you’re curious how the code compares between v1 and v2, you can check out the previous version source code here.

I decided to start from scratch instead of upgrading the existing site, but kept some of the styling and layout (particularly the landing page). This is a fairly simple site focused on sharing content like events and blog posts, so Document-Driven Mode seemed like a good fit.

Installation

To create a new Document-Driven Mode site from scratch, use the following command:

npx nuxi init doc-driven-app -t doc-driven
Enter fullscreen mode Exit fullscreen mode

If you already have a Nuxt Content v2 site created, you can enable Document-Driven Mode in your nuxt.config file.

export default defineNuxtConfig({
  modules: ['@nuxt/content'],
  content: {
    documentDriven: true
  }
})
Enter fullscreen mode Exit fullscreen mode

Directory Structure

A Document-Driven Mode site has three main directories to leverage: content, layouts, and components. There is also the app.vue root component.

Here is a simplified example of my directory structure:

nuxt.config.ts
app.vue
content/
    index.md
    blog/
        index.md
    blog-post-1.md
    blog-post-2.md
    events/
        index.md
    event-1.md
    event-2.md
layouts/
    blog.vue
components/
    content/
        BlogHeader.vue
    BlogFooter.vue
    ContentList.vue
    Event.vue
    Hero.vue
Enter fullscreen mode Exit fullscreen mode

I have an index.md as the root for each directory inside content. Without this, Nuxt Content will use the built-in <DocumentNotFound> component automatically.

I have a single blog.vue layout file. You can also create a default.vue layout, but my site didn’t need a fallback layout because my app.vue was sufficient.

❗ You’ll notice that there is a content directory INSIDE components. This is required to use your components inside content Markdown files, so make sure to include it.

Setting up app.vue

My app.vue file is simple, with just the required <NuxtPage /> component and a custom <Navigation /> component inside my template. I’ve also added some global styles and am leveraging the Nuxt useHead() component to set the meta tags and favicon for my site.

<template>
    <Navigation />
    <NuxtPage />
</template>

<style>
// ...
</style>

<script setup lang="ts">
useHead({
  title:
    "Cecelia speaks and writes about web and mobile dev, testing, OSS, communities, and mentoring.",
  meta: [
    {
      name: "description",
      content:
        "Cecelia Martinez is a Developer Advocate who speaks and writes about web and mobile development, CI/CD, and testing. She is passionate about open source devtools, building inclusive communities, and ensuring everyone feels like they belong in tech. She is a volunteer with Out in Tech and Women Who Code Front End and a GitHub Star.",
    },
  ],
  link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }],
});
</script>
Enter fullscreen mode Exit fullscreen mode

Adding Markdown Content

From here, I can start creating Markdown files within the content directory. However, I’d recommend starting by setting up types for the content. Because we’re using Markdown files, we need to extend the MarkdownParsedContent type built into Nuxt Content, as described in the documentation here.

Here are the types for my Blog and Event content.

import type { MarkdownParsedContent } from "@nuxt/content/dist/runtime/types";

export type Section = "blog" | "events";
export type EventCategory = "workshop" | "conference" | "webinar" | "stream";

export interface Blog extends MarkdownParsedContent {
  title: string;
  description: string;
  date: string;
  img?: string;
  tags: string[];
}

export interface Event extends MarkdownParsedContent {
  event: string;
  title: string;
  date: string;
  type: EventCategory;
  description: string;
  url: string;
  img?: string;
  embed?: string;
  video?: string;
  slides?: string;
  tags: string[];
}
Enter fullscreen mode Exit fullscreen mode

I also recommend creating a snippet for generating the Markdown front matter to correspond to the required fields for your content. You can learn how to do this with VS Code in the blog post here.

Here is an example of one of the Markdown files inside my events directory, which leverages front matter for the event information.

---
event: All Things Open 2022
title: No-Code OSS Contributions
date: 2022-10-30
type: Conference
description: There are many ways to contribute to an open source project without writing a single line of code. For contributors, identify was you can impact a project without diving into the codebase. For maintainers, learn how to make your project no-code friendly to encourage contributions of all kinds and broaden your community of contributors.
url: https://2022.allthingsopen.org/sessions/no-code-oss-contributions/
img:
embed: _KD10cx9H90
video:
slides: https://slides.com/ceceliamartinez/no-code-oss-contributions
tags: ["oss", "community", "diversity"]
---
Enter fullscreen mode Exit fullscreen mode

The blog post files are similar, except there is Markdown content following the front matter which contains the body of the post.

Using Markdown Components (MDC) Syntax

I’m using Markdown files for everything, including my main index page and the index pages for my content categories. This means I’ll need to use Markdown Components (MDC) Syntax to introduce styling, data fetching, or other functionality not included in Markdown.

Let’s start with the index page. I’ve created a <Hero> component with the site title, my avatar, some social icons, and an intro.

Below is a snippet of the component:

<template>
  <main>
    <h1>Cecelia Creates</h1>
    <div id="avatar">
      <NuxtImg src="/avatar.png" sizes="sm:80vw md:50vw lg:300px" id="avatar" />
    </div>
    <div id="icons">
      <a
        v-for="link in socialLinks"
        :key="link.name"
        :href="link.url"
        target="blank"
      >
        <font-awesome-icon :icon="['fab', link.icon]" />
      </a>
    </div>
    <div id="about">
     // some links and text about me
    </div>
  </main>
</template>

<script setup>
const socialLinks = [
  {
    name: "Twitter",
    url: "https://twitter.com/ceceliacreates",
    icon: "twitter-square",
  },
  {
    name: "Youtube",
    url: "https://www.youtube.com/@ceceliacreates",
    icon: "youtube-square",
  },
  {
    name: "GitHub",
    url: "https://github.com/ceceliacreates",
    icon: "github-square",
  },
];
</script>

<style scoped>
// ...
</style>
Enter fullscreen mode Exit fullscreen mode

Now, to use the component, I use the : MDC Syntax in my index.md file.

:hero
Enter fullscreen mode Exit fullscreen mode

That’s all there is. It’s not particularly exciting, but for more complex components, you can also use MDC Syntax to set props or attributes, and event nest components. Check out the documentation here for more options.

I also created an <Event> component that renders the details for a given event on its own page. Here is the template for this component:

<template>
  <main>
    <p v-if="!event">Event info missing</p>
    <div v-else>
      <h1>{{ event.title }}</h1>
      <h3>{{ event.event }}</h3>
      <p>{{ event.type + ", " + new Date(event.date).toDateString() }}</p>
      <a :href="event.url" v-show="event.url" target="blank"
        ><p>More Details</p></a
      >
      <div id="video-wrapper">
        <iframe
          id="video"
          v-show="event.embed"
          :src="`https://www.youtube.com/embed/${event.embed}`"
          frameborder="0"
          allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
          allowfullscreen
        ></iframe>
      </div>
      <a :href="event.video" v-show="event.video && !event.embed" target="blank"
        ><span>Video</span></a
      >
      <span v-show="event.video && !event.embed && event.slides"> || </span>
      <a :href="event.slides" v-show="event.slides" target="blank"
        ><span>Slides</span></a
      >
      <p>{{ event.description }}</p>
    </div>
    <p><NuxtLink to="/events"> ⬅️ Back to events </NuxtLink></p>
  </main>
</template>
Enter fullscreen mode Exit fullscreen mode

To include this component for my events, I pass the :event MDC Syntax in the body of my event Markdown files, like this:

---
event: All Things Open 2022
title: No-Code OSS Contributions
date: 2022-10-30
type: Conference
description: There are many ways to contribute to an open source project without writing a single line of code. For contributors, identify was you can impact a project without diving into the codebase. For maintainers, learn how to make your project no-code friendly to encourage contributions of all kinds and broaden your community of contributors.
url: https://2022.allthingsopen.org/sessions/no-code-oss-contributions/
img:
embed: _KD10cx9H90
video:
slides: https://slides.com/ceceliamartinez/no-code-oss-contributions
tags: ["oss", "community", "diversity"]
---
:event
Enter fullscreen mode Exit fullscreen mode

Because I am using this component across all events, using a layout would probably be more efficient, and I will likely create an event layout in the future. In the meantime, my VS Code front matter snippet automatically inserts the :event component syntax.

Fetching Content Data

For my category index.md files, I created a <ContentList> component that renders the list of either blog posts or events, with a link, title, date, image, and description.

<template>
  <main>
    <div v-for="item in data" :key="item._path" class="item">
      <h3>
        ➡️ <NuxtLink :to="item._path">{{ item.title }}</NuxtLink>
      </h3>
      <p id="date">{{ new Date(item.date).toDateString() }}</p>
      <NuxtImg
        v-show="item.img"
        :src="`${item.img}`"
        sizes="sm:80vw md:50vw lg:30vw"
      />
      <p id="description">{{ item.description }}</p>
    </div>
  </main>
</template>
Enter fullscreen mode Exit fullscreen mode

To fetch the items for my <ContentList> component, I use the Nuxt Content queryContent() method inside my <script setup>. The queryContent() method is new for Nuxt 3. It accepts a path to query and has chained methods for filtering, sorting, and specifying how many results to return. More details are available in the docs here.

Wrap your query in the useAsyncData() composable, which is auto-imported. The first parameter to useAsyncData() is just a name for the query. In the code below, I’m dynamically passing either events or blog via the section prop to specify the name and path for the query.

<script setup lang="ts">
import { Section } from "~~/types";

const props = defineProps<{
  section: Section;
}>();

const { data } = await useAsyncData(`content-list-${props.section}`, () =>
  queryContent(`/${props.section}`)
    .where({ _dir: { $ne: "" } })
    .sort({ date: -1 })
    .find()
);
</script>
Enter fullscreen mode Exit fullscreen mode

In the example above, I’m using .where({ _dir: { $ne: '' } }) to only return content files where the directory is not blank. This is to exclude the index.md files from the list.

Fetching with useContent()

Document-Driven Mode also provides a useContent() method for fetching data about the content used to generate the current page. This is helpful if you only need context for the page you’re on, or info on the content surrounding the current page.

For my <Event> component, I only need the current content to render the event details.

<script setup lang="ts">
import { Event } from "~~/types";

const { page } = useContent();

const event: Event = page;
</script>
Enter fullscreen mode Exit fullscreen mode

I can get the page variable from useContent() then assign that as the event to use in my template. More details on useContent() are in the docs here.

Applying Layouts

Right now, the <NuxtPage /> component in my app.vue handles injecting the content from my Markdown files directly into the page. For my blog posts, however, I’d like to show the title of the post and add some navigation on the bottom to go back to the posts list. I could use MDC Syntax, but would have to add this to every Markdown file. Because I know I want to use this for every blog post, I can create a layout instead.

Inside my layouts directory, I have a blog.vue file.

<template>
  <div>
    <BlogHeader />
    <slot />
    <BlogFooter />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

In this layout, I’m adding a <BlogHeader /> component that renders the blog post title, and a <BlogFooter /> component that adds some navigation. These are wrapped around <slot /> component, which is required to ensure the Markdown component is injected in the page.

⚠️ Make sure you do not have a <NuxtLayout /> component in your app.vue.

With Document-Driven Mode, I can set the layout in the Markdown front matter with the layout field. I’d recommend adding this to your VS Code snippet so it’s set for every blog post.

---
title: Using FontAwesome Icons with Nuxt 3
description:
date: 2023-01-08
img:
tags: ["nuxt", "icons"]
layout: blog
---
Enter fullscreen mode Exit fullscreen mode

Next Steps

This is an early implementation of using Document-Driven Mode in Nuxt Content v2, and there are other features we can add in the future. For example:

  • Creating an event layout instead of inserting the <Event> component directly in the Markdown files
  • Add next/previous navigation to individual blog and event pages using useContent()
  • Adding a tag filter feature to content category index pages
  • Adding a range prop to the <ContentList> component to show upcoming or past events
  • Add code syntax highlighting to blog posts
  • Adding more content categories like videos, podcasts, tutorials, etc.

If you are interested in contributing to Nuxt Content, check out the GitHub repository here. Also, if you are migrating from Nuxt 2 to Nuxt 3, I recommend Debbie O'Brien's blog post here, which was very helpful as I worked on my own site.

If you end up using Document-Driven Mode for your own site, share the link! I’d love to see how others use this and the various implementations possible.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .