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
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
---
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
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>
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
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
}
})
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
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 INSIDEcomponents
. 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>
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[];
}
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"]
---
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>
Now, to use the component, I use the :
MDC Syntax in my index.md
file.
:hero
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>
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
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>
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>
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>
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>
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 yourapp.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
---
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.