N
Nuxt3w ago
peoray

Nuxt Content: How do I get the page to render via the slug set as a frontmatter property?

In my blog/index.vue, I have the following:
<script setup>
const contentQuery = await queryContent('blog')
.where({ draft: false })
.sort({ date: 1 })
.find()
</script>

<template>
<div>
<ul class="list-disc space-y-2">
<li
v-for="article in contentQuery"
:key="article.slug"
class="text-lg font-medium"
>
<NuxtLink :to="`/blog/${article.slug}`">{{ article.title }}</NuxtLink>
</li>
</ul>
</div>
</template>
<script setup>
const contentQuery = await queryContent('blog')
.where({ draft: false })
.sort({ date: 1 })
.find()
</script>

<template>
<div>
<ul class="list-disc space-y-2">
<li
v-for="article in contentQuery"
:key="article.slug"
class="text-lg font-medium"
>
<NuxtLink :to="`/blog/${article.slug}`">{{ article.title }}</NuxtLink>
</li>
</ul>
</div>
</template>
In the /blog/[...slug].vue, I have the following:
<template>
<main class="prose mx-auto">
<ContentDoc v-slot="{ doc }">
<article>
<h1>{{ doc.title }}</h1>
<ContentRenderer :value="doc" class="text-[17px]" />
</article>
</ContentDoc>
</main>
</template>
<template>
<main class="prose mx-auto">
<ContentDoc v-slot="{ doc }">
<article>
<h1>{{ doc.title }}</h1>
<ContentRenderer :value="doc" class="text-[17px]" />
</article>
</ContentDoc>
</main>
</template>
In my article markdown file, I have the following:
---
title: 'Blog Title Goes Here'
description: 'meta description of the page'
draft: false
date: '2024-10-30'
slug: 'hello-world'
---
---
title: 'Blog Title Goes Here'
description: 'meta description of the page'
draft: false
date: '2024-10-30'
slug: 'hello-world'
---
The name of the markdown file is this-is-the-beginning.md and the page works as /blog/this-is-the-beginning, but I want it to use the slug from the frontmatter instead and display the content at /blog/hello-world. At the moment, when it goes to /blog/hello-world, I get a Document not found page error
12 Replies
kapa.ai
kapa.ai3w ago
Hi @peoray. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- To render the page using the slug set in the frontmatter property instead of the filename, you need to make a few adjustments to your setup. Here's how you can achieve this: 1. First, update your blog/index.vue file to use the slug from the frontmatter:
<script setup>
const contentQuery = await queryContent('blog')
.where({ draft: false })
.sort({ date: 1 })
.find()
</script>

<template>
<div>
<ul class="list-disc space-y-2">
<li
v-for="article in contentQuery"
:key="article.slug"
class="text-lg font-medium"
>
<NuxtLink :to="`/blog/${article.slug}`">{{ article.title }}</NuxtLink>
</li>
</ul>
</div>
</template>
<script setup>
const contentQuery = await queryContent('blog')
.where({ draft: false })
.sort({ date: 1 })
.find()
</script>

<template>
<div>
<ul class="list-disc space-y-2">
<li
v-for="article in contentQuery"
:key="article.slug"
class="text-lg font-medium"
>
<NuxtLink :to="`/blog/${article.slug}`">{{ article.title }}</NuxtLink>
</li>
</ul>
</div>
</template>
2. In your /blog/[...slug].vue file, you need to modify the <ContentDoc> component to use the path prop:
<script setup>
const { path } = useRoute()
</script>

<template>
<main class="prose mx-auto">
<ContentDoc :path="path" v-slot="{ doc }">
<article>
<h1>{{ doc.title }}</h1>
<ContentRenderer :value="doc" class="text-[17px]" />
</article>
</ContentDoc>
</main>
</template>
<script setup>
const { path } = useRoute()
</script>

<template>
<main class="prose mx-auto">
<ContentDoc :path="path" v-slot="{ doc }">
<article>
<h1>{{ doc.title }}</h1>
<ContentRenderer :value="doc" class="text-[17px]" />
</article>
</ContentDoc>
</main>
</template>
3. In your nuxt.config.ts, you need to configure Nuxt Content to use the slug from the frontmatter:
export default defineNuxtConfig({
// ... other configurations
content: {
documentDriven: true,
markdown: {
anchorLinks: false
},
navigation: {
fields: ['slug', 'title']
}
}
})
export default defineNuxtConfig({
// ... other configurations
content: {
documentDriven: true,
markdown: {
anchorLinks: false
},
navigation: {
fields: ['slug', 'title']
}
}
})
This configuration tells Nuxt Content to use the slug field from the frontmatter for navigation and routing. With these changes, Nuxt Content should now use the slug from the frontmatter to generate the URL for your blog posts, rather than using the filename. Remember that you need to ensure that each of your markdown files has a unique slug in its frontmatter. If there are duplicate slugs, you might encounter unexpected behavior. These modifications should allow your content to be accessible at /blog/hello-world as desired, based on the slug in the frontmatter, rather than the filename. Nuxt Content Configuration ContentDoc Component If you're still encountering issues after making these changes, you may need to clear your Nuxt cache and rebuild your project. You can do this by running npx nuxi cleanup in your project directory before rebuilding. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3w ago
<@355872920062525451> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
peoray
peorayOP2w ago
@kapa.ai this doesn't work. are you sure I have to have the content Driven set to true. Also how does it tell if that I want the content shown to be the one with the slug in the frontmatter
kapa.ai
kapa.ai2w ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- I apologize for the confusion in my previous answer. You're right to question this, and I'll provide a more accurate explanation based on the Nuxt Content documentation. You don't necessarily need to have documentDriven set to true for this to work. Let's break down the correct approach: 1. The slug in the frontmatter is not automatically used for routing by Nuxt Content. Instead, we need to modify our query to fetch the content based on the slug. 2. In your /blog/[...slug].vue file, you should update it to query the content based on the slug from the route:
<script setup>
const { path } = useRoute()
const { data } = await useAsyncData(`content-${path}`, () => queryContent().where({ slug: path.split('/').pop() }).findOne())
</script>

<template>
<main class="prose mx-auto">
<ContentRenderer v-if="data" :value="data" />
<p v-else>Document not found</p>
</main>
</template>
<script setup>
const { path } = useRoute()
const { data } = await useAsyncData(`content-${path}`, () => queryContent().where({ slug: path.split('/').pop() }).findOne())
</script>

<template>
<main class="prose mx-auto">
<ContentRenderer v-if="data" :value="data" />
<p v-else>Document not found</p>
</main>
</template>
This approach queries the content where the slug in the frontmatter matches the last part of the current path. 3. In your blog/index.vue, ensure you're using the slug from the frontmatter:
<script setup>
const contentQuery = await queryContent('blog')
.where({ draft: false })
.sort({ date: 1 })
.find()
</script>

<template>
<div>
<ul class="list-disc space-y-2">
<li
v-for="article in contentQuery"
:key="article.slug"
class="text-lg font-medium"
>
<NuxtLink :to="`/blog/${article.slug}`">{{ article.title }}</NuxtLink>
</li>
</ul>
</div>
</template>
<script setup>
const contentQuery = await queryContent('blog')
.where({ draft: false })
.sort({ date: 1 })
.find()
</script>

<template>
<div>
<ul class="list-disc space-y-2">
<li
v-for="article in contentQuery"
:key="article.slug"
class="text-lg font-medium"
>
<NuxtLink :to="`/blog/${article.slug}`">{{ article.title }}</NuxtLink>
</li>
</ul>
</div>
</template>
4. In your nuxt.config.ts, you don't need to set documentDriven to true. Instead, ensure you have the basic Nuxt Content configuration:
export default defineNuxtConfig({
modules: ['@nuxt/content'],
content: {
// Your content options here
}
})
export default defineNuxtConfig({
modules: ['@nuxt/content'],
content: {
// Your content options here
}
})
This setup tells Nuxt Content to use the slug field from the frontmatter for querying and linking, rather than using the filename. The key here is that we're explicitly querying the content based on the slug in the [...slug].vue file, which allows us to match the content with the slug specified in the frontmatter. Nuxt Content Querying If you're still facing issues after these changes, it might be helpful to check if your content files are being parsed correctly. You can do this by using the queryContent().find() method in a component or page to log all your content and ensure the slugs are being read correctly from the frontmatter. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2w ago
<@355872920062525451> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
peoray
peorayOP2w ago
@kapa.ai explain const { data } = await useAsyncData(content-${path}, () => queryContent().where({ slug: path.split('/').pop() }).findOne()) what does the content-path mean exactly
kapa.ai
kapa.ai2w ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Certainly! Let's break down this line of code and explain its components:
const { data } = await useAsyncData(`content-${path}`, () => queryContent().where({ slug: path.split('/').pop() }).findOne())
const { data } = await useAsyncData(`content-${path}`, () => queryContent().where({ slug: path.split('/').pop() }).findOne())
This line uses the useAsyncData composable, which is a Nuxt 3 feature for handling asynchronous data fetching. Let's examine each part: 1. useAsyncData: This is a Nuxt 3 composable that wraps asynchronous operations. It's used to fetch data in an SSR-friendly way, ensuring that the data is available both on the server and the client. 2. content-${path}: This is the first argument to useAsyncData. It's a unique key for caching the result of the async operation. The content- prefix is just a convention to indicate that this is content-related data. The ${path} part is likely a variable containing the current route path. This ensures that each unique path has its own cached data. 3. The second argument is an arrow function that performs the actual data fetching:
() => queryContent().where({ slug: path.split('/').pop() }).findOne()

() => queryContent().where({ slug: path.split('/').pop() }).findOne()

- queryContent() is a Nuxt Content composable used to query your content. - .where({ slug: path.split('/').pop() }) filters the content to find an item where the slug matches the last segment of the current path. - .findOne() retrieves the first (and presumably only) item that matches the query. 4. const { data } = await ...: This destructures the result of useAsyncData, extracting the data property. This data will contain the content item that matches the slug. The content-${path} key is important because: 1. It provides a unique identifier for caching the result of this specific query. 2. It allows Nuxt to avoid refetching data unnecessarily, improving performance. 3. If the path changes (i.e., the user navigates to a different page), a new query will be executed because the key will be different. For example, if the current path is /blog/hello-world, the key would be content-/blog/hello-world. This ensures that each blog post has its own cached data. useAsyncData Remember, using useAsyncData in this way is beneficial because it integrates well with Nuxt's server-side rendering capabilities, ensuring that your content is available both during server-side rendering and client-side navigation. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2w ago
<@355872920062525451> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
peoray
peorayOP2w ago
@kapa.ai how do I make sure that the seo title and description is comig from the frontmatter
kapa.ai
kapa.ai2w ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- To ensure that the SEO title and description are coming from the frontmatter of your Markdown files, you can use the useSeoMeta composable in Nuxt 3. This composable is specifically designed for managing SEO metadata. Here's how you can implement it: 1. In your /blog/[...slug].vue file, update your script section to include useSeoMeta:
<script setup>
const { path } = useRoute()
const { data: article } = await useAsyncData(`content-${path}`, () =>
queryContent().where({ slug: path.split('/').pop() }).findOne()
)

useSeoMeta({
title: () => article.value?.title,
ogTitle: () => article.value?.title,
description: () => article.value?.description,
ogDescription: () => article.value?.description
})
</script>

<template>
<main class="prose mx-auto">
<ContentRenderer v-if="article" :value="article" />
<p v-else>Document not found</p>
</main>
</template>
<script setup>
const { path } = useRoute()
const { data: article } = await useAsyncData(`content-${path}`, () =>
queryContent().where({ slug: path.split('/').pop() }).findOne()
)

useSeoMeta({
title: () => article.value?.title,
ogTitle: () => article.value?.title,
description: () => article.value?.description,
ogDescription: () => article.value?.description
})
</script>

<template>
<main class="prose mx-auto">
<ContentRenderer v-if="article" :value="article" />
<p v-else>Document not found</p>
</main>
</template>
This setup does the following: 1. It fetches the article data based on the slug in the URL. 2. It uses useSeoMeta to set the SEO metadata. 3. The title and description are set using arrow functions that return the values from the article's frontmatter. 4. We're also setting ogTitle and ogDescription for Open Graph metadata, which is useful for social media sharing. The useSeoMeta composable is reactive, so if the article data changes, the SEO metadata will update automatically. useSeoMeta If you want to add more metadata or customize it further, you can add more fields to the useSeoMeta call. For example:
useSeoMeta({
title: () => article.value?.title,
ogTitle: () => article.value?.title,
description: () => article.value?.description,
ogDescription: () => article.value?.description,
ogImage: () => article.value?.image, // If you have an image in your frontmatter
twitterCard: 'summary_large_image',
})
useSeoMeta({
title: () => article.value?.title,
ogTitle: () => article.value?.title,
description: () => article.value?.description,
ogDescription: () => article.value?.description,
ogImage: () => article.value?.image, // If you have an image in your frontmatter
twitterCard: 'summary_large_image',
})
This approach ensures that your SEO metadata is dynamically generated from the frontmatter of each Markdown file, providing accurate and specific metadata for each of your blog posts. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2w ago
<@355872920062525451> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
peoray
peorayOP2w ago
where is the article.value coming from

Did you find this page helpful?