N
Nuxt2d ago
nathanr

Nuxt Content ContentRenderer not rendering Markdown

For some reason this is not rendering a markdown file in Markdown, it's just plain text <ContentRenderer v-if="page" :value="page" />
18 Replies
kapa.ai
kapa.ai2d ago
Hi @nathanr. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- It looks like you're having an issue with the <ContentRenderer> component not properly rendering your Markdown content. This is a common issue that can happen for several reasons. Based on the knowledge sources, here are some potential solutions: 1. Make sure you have the correct setup First, ensure you have properly set up Nuxt Content in your project:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/content']
})
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/content']
})
And that you have a proper content configuration:
// content.config.ts
import { defineContentConfig, defineCollection } from '@nuxt/content'

export default defineContentConfig({
collections: {
content: defineCollection({
type: 'page',
source: '**/*.md'
})
}
})
// content.config.ts
import { defineContentConfig, defineCollection } from '@nuxt/content'

export default defineContentConfig({
collections: {
content: defineCollection({
type: 'page',
source: '**/*.md'
})
}
})
2. Check your query and component usage Make sure you're correctly querying the content and using the ContentRenderer component:
<script setup lang="ts">
const route = useRoute()
const { data: page } = await useAsyncData(route.path, () => {
return queryCollection('content').path(route.path).first()
})
</script>

<template>
<ContentRenderer v-if="page" :value="page" />
</template>
<script setup lang="ts">
const route = useRoute()
const { data: page } = await useAsyncData(route.path, () => {
return queryCollection('content').path(route.path).first()
})
</script>

<template>
<ContentRenderer v-if="page" :value="page" />
</template>
3. Add typography styling If your Markdown is rendering as plain text, you might be missing the typography styling. For Tailwind users, install the typography plugin:
npm install -D @tailwindcss/typography
npm install -D @tailwindcss/typography
Then add it to your Tailwind config:
// tailwind.config.ts
import tailwindTypography from '@tailwindcss/typography'

export default {
// ...
plugins: [tailwindTypography()]
}
// tailwind.config.ts
import tailwindTypography from '@tailwindcss/typography'

export default {
// ...
plugins: [tailwindTypography()]
}
And wrap your ContentRenderer with a prose class:
<template>
<div class="prose">
<ContentRenderer v-if="page" :value="page" />
</div>
</template>
<template>
<div class="prose">
<ContentRenderer v-if="page" :value="page" />
</div>
</template>
4. Check the prose prop The ContentRenderer component has a prose prop that controls whether to render Prose components instead of HTML tags. Try setting it explicitly:
<ContentRenderer v-if="page" :value="page" :prose="true" />
<ContentRenderer v-if="page" :value="page" :prose="true" />
For landing pages or non-prose content, you might want to set it to false:
<ContentRenderer v-if="page" :value="page" :prose="false" />
<ContentRenderer v-if="page" :value="page" :prose="false" />
5. Verify your Markdown file Make sure your Markdown file is properly formatted and doesn't have any syntax issues. If you're still having issues, it might be helpful to check the console for any errors and verify that your content is being properly loaded by logging the page value. Nuxt Content documentation provides more details about the ContentRenderer component and its usage. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2d ago
<@243051127904534528> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
nathanr
nathanrOP2d ago
@kapa.ai tailwind.config.ts is outdated now
kapa.ai
kapa.ai2d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- You're right! With the latest versions of Nuxt UI and Nuxt Content, the approach to Tailwind CSS configuration has changed significantly, especially with Tailwind CSS v4. Tailwind CSS v4 in Nuxt: CSS-first approach Tailwind CSS v4 now uses a CSS-first configuration approach instead of the JavaScript-based configuration in tailwind.config.ts. This is a major change when working with Nuxt UI v3 and Nuxt Content v3. New approach: 1. Create a CSS file (typically assets/css/main.css) and import it in your Nuxt config:
@import "tailwindcss";
@import "@nuxt/ui";

@theme static {
/* Your theme variables */
--font-sans: 'Public Sans', sans-serif;

/* Colors */
--color-green-50: #EFFDF5;
--color-green-100: #D9FBE8;
/* ... other color variables */
}

/* For content in markdown files */
@source "../../../content";
@import "tailwindcss";
@import "@nuxt/ui";

@theme static {
/* Your theme variables */
--font-sans: 'Public Sans', sans-serif;

/* Colors */
--color-green-50: #EFFDF5;
--color-green-100: #D9FBE8;
/* ... other color variables */
}

/* For content in markdown files */
@source "../../../content";
2. Import this CSS file in your Nuxt config:
// nuxt.config.ts
export default defineNuxtConfig({
css: ['~/assets/css/main.css']
})
// nuxt.config.ts
export default defineNuxtConfig({
css: ['~/assets/css/main.css']
})
Key changes: - The @theme directive replaces the theme.extend key from your old tailwind.config.ts - The @source directive is crucial for Tailwind to detect classes in markdown files with Nuxt Content - If you're using Nuxt UI Pro, you'd import @nuxt/ui-pro instead of @nuxt/ui Nuxt UI Theme documentation provides more details about this new approach to theming. This change is part of the broader migration to Nuxt UI v3 and Nuxt Content v3, which includes many other breaking changes to components, APIs, and styling approaches. Would you like more specific guidance on migrating your particular Tailwind configuration to this new approach? ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2d ago
<@243051127904534528> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
HugoRCD
HugoRCD2d ago
Would you be able to provide more information, such as the package.json, nuxt.config.ts, and so on?
nathanr
nathanrOP2d ago
export default defineNuxtConfig({
ssr: true,
modules: [
'@nuxt/ui',
'nuxt-gtag',
'@nuxt/image',
'@nuxtjs/seo',
'@nuxt/content',
],
css: ['~/assets/css/main.css'],
ui: {
colorMode: false
},
nitro: {
prerender: {
routes: ['/articles']
}
},
site: {
url: 'https://protesthealth.com',
name: 'Protest Health',
indexable: true,
content: {
renderer: {
anchorLinks: false,
},
preview: {
api: 'https://api.nuxt.studio',
dev: true,
},
},
mdc: {
highlight: {
theme: {
dark: 'github-dark',
default: 'github-dark',
light: 'github-light',
},
},
},

app: {
head: {
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
}
},
compatibilityDate: '2024-11-01',
devtools: { enabled: true },
})
export default defineNuxtConfig({
ssr: true,
modules: [
'@nuxt/ui',
'nuxt-gtag',
'@nuxt/image',
'@nuxtjs/seo',
'@nuxt/content',
],
css: ['~/assets/css/main.css'],
ui: {
colorMode: false
},
nitro: {
prerender: {
routes: ['/articles']
}
},
site: {
url: 'https://protesthealth.com',
name: 'Protest Health',
indexable: true,
content: {
renderer: {
anchorLinks: false,
},
preview: {
api: 'https://api.nuxt.studio',
dev: true,
},
},
mdc: {
highlight: {
theme: {
dark: 'github-dark',
default: 'github-dark',
light: 'github-light',
},
},
},

app: {
head: {
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
}
},
compatibilityDate: '2024-11-01',
devtools: { enabled: true },
})
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/content": "^3.0.1",
"@nuxt/image": "^1.10.0",
"@nuxt/ui": "^3.0.2",
"@nuxtjs/seo": "^3.0.3",
"nodemailer": "^6.10.0",
"nuxt": "^3.6.0",
"nuxt-gtag": "^3.0.2",
"tailwindcss": "^4.1.4",
"vue": "latest",
"vue-router": "latest",
"zod": "^3.24.3"
},
"devDependencies": {
"@iconify-json/fa6-brands": "^1.2.5",
"@nuxtjs/tailwindcss": "^6.13.1",
"@tailwindcss/typography": "^0.5.16",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0"
}
}
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/content": "^3.0.1",
"@nuxt/image": "^1.10.0",
"@nuxt/ui": "^3.0.2",
"@nuxtjs/seo": "^3.0.3",
"nodemailer": "^6.10.0",
"nuxt": "^3.6.0",
"nuxt-gtag": "^3.0.2",
"tailwindcss": "^4.1.4",
"vue": "latest",
"vue-router": "latest",
"zod": "^3.24.3"
},
"devDependencies": {
"@iconify-json/fa6-brands": "^1.2.5",
"@nuxtjs/tailwindcss": "^6.13.1",
"@tailwindcss/typography": "^0.5.16",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0"
}
}
HugoRCD
HugoRCD2d ago
Ok thanks! And the file in which you have the ContentRenderer and your content.config.ts?
nathanr
nathanrOP2d ago
<template>
<div v-if="page" class="h-full p-10">
<FolioMeta :page :is-article="route.path.includes('/articles/')" />
<NuxtLink to="/articles"
class="mx-auto my-8 flex cursor-pointer items-center gap-2 px-4 py-4 text-white hover:text-neutral-500 transition-colors duration-200 sm:max-w-2xl md:max-w-3xl lg:max-w-4xl">
<UIcon name="lucide:arrow-left" class="size-4" />
Articles
</NuxtLink>
<article class="bg-white mx-auto p-5 rounded-xl sm:max-w-2xl md:max-w-3xl lg:max-w-4xl">
<h1 class="text-5xl">
{{ page?.title }}
</h1>
<div class="mt-1 flex flex-col gap-2 sm:flex-row sm:gap-4">
<p>{{ page?.date }}</p>
<p class="hidden sm:block">
|
</p>
<p>{{ page?.tags }}</p>
<p class="hidden sm:block">
|
</p>
</div>
<USeparator class="my-5" size="lg"></USeparator>
<ContentRenderer v-if="page" :value="page" />
</article>
</div>
</template>
<template>
<div v-if="page" class="h-full p-10">
<FolioMeta :page :is-article="route.path.includes('/articles/')" />
<NuxtLink to="/articles"
class="mx-auto my-8 flex cursor-pointer items-center gap-2 px-4 py-4 text-white hover:text-neutral-500 transition-colors duration-200 sm:max-w-2xl md:max-w-3xl lg:max-w-4xl">
<UIcon name="lucide:arrow-left" class="size-4" />
Articles
</NuxtLink>
<article class="bg-white mx-auto p-5 rounded-xl sm:max-w-2xl md:max-w-3xl lg:max-w-4xl">
<h1 class="text-5xl">
{{ page?.title }}
</h1>
<div class="mt-1 flex flex-col gap-2 sm:flex-row sm:gap-4">
<p>{{ page?.date }}</p>
<p class="hidden sm:block">
|
</p>
<p>{{ page?.tags }}</p>
<p class="hidden sm:block">
|
</p>
</div>
<USeparator class="my-5" size="lg"></USeparator>
<ContentRenderer v-if="page" :value="page" />
</article>
</div>
</template>
import { defineCollection, z } from '@nuxt/content'
import { asSeoCollection } from '@nuxtjs/seo/content'

const commonContentSchema = z.object({
title: z.string().nonempty(),
description: z.string().nonempty(),
date: z.string().nonempty(),
})

const commonArticleSchema = z.object({
title: z.string().nonempty(),
category: z.string().nonempty(),
description: z.string().nonempty(),
date: z.string().nonempty(),
featuredImage: z.string().url(),
// readingTime: z.string().nonempty(),
tags: z.array(z.string().nonempty()),
})


export const collections = {
content: defineCollection(
asSeoCollection({
type: 'page',
source: {
include: '**/*.md',
exclude: ['articles/*.md'],
},
schema: commonContentSchema,
}),
),
articles: defineCollection(
asSeoCollection({
type: 'page',
source: {
include: 'articles/**/*.md',
prefix: '/articles',
},
schema: commonArticleSchema,
}),
)
}
import { defineCollection, z } from '@nuxt/content'
import { asSeoCollection } from '@nuxtjs/seo/content'

const commonContentSchema = z.object({
title: z.string().nonempty(),
description: z.string().nonempty(),
date: z.string().nonempty(),
})

const commonArticleSchema = z.object({
title: z.string().nonempty(),
category: z.string().nonempty(),
description: z.string().nonempty(),
date: z.string().nonempty(),
featuredImage: z.string().url(),
// readingTime: z.string().nonempty(),
tags: z.array(z.string().nonempty()),
})


export const collections = {
content: defineCollection(
asSeoCollection({
type: 'page',
source: {
include: '**/*.md',
exclude: ['articles/*.md'],
},
schema: commonContentSchema,
}),
),
articles: defineCollection(
asSeoCollection({
type: 'page',
source: {
include: 'articles/**/*.md',
prefix: '/articles',
},
schema: commonArticleSchema,
}),
)
}
@HugoRCD Thanks for looking, I was actually following how you have achieved this on your blog/ example Nuxt Content page
HugoRCD
HugoRCD2d ago
No worries 😁 You just forgot the script part with the queryCollection didn't you?
nathanr
nathanrOP2d ago
<script lang="ts" setup>
import { USeparator } from '#components'
import type { Collections } from '@nuxt/content'
import { withLeadingSlash, joinURL } from 'ufo'

const route = useRoute()
// const { locale, t, localeProperties } = useI18n()

const slug = computed(() => {
const s = route.params.slug
return Array.isArray(s) ? s : [s]
})

const category = computed(() => slug.value[0] || '')

const path = computed(() => withLeadingSlash(joinURL('articles', ...slug.value)))

const collection = computed(() => 'articles' as const)

const { data: page } = await useAsyncData(path.value, async () =>
await queryCollection(collection.value).path(path.value).first() as Collections['articles']
)

console.log(collection.value)

if (!page.value)
throw createError({ statusCode: 404, statusMessage: 'Page not found' })

defineOgImage({
url: page.value.featuredImage,
})
</script>
<script lang="ts" setup>
import { USeparator } from '#components'
import type { Collections } from '@nuxt/content'
import { withLeadingSlash, joinURL } from 'ufo'

const route = useRoute()
// const { locale, t, localeProperties } = useI18n()

const slug = computed(() => {
const s = route.params.slug
return Array.isArray(s) ? s : [s]
})

const category = computed(() => slug.value[0] || '')

const path = computed(() => withLeadingSlash(joinURL('articles', ...slug.value)))

const collection = computed(() => 'articles' as const)

const { data: page } = await useAsyncData(path.value, async () =>
await queryCollection(collection.value).path(path.value).first() as Collections['articles']
)

console.log(collection.value)

if (!page.value)
throw createError({ statusCode: 404, statusMessage: 'Page not found' })

defineOgImage({
url: page.value.featuredImage,
})
</script>
Sorry I took it out the message as it wasn't formatting properly The thing is the page is loading with the right meta data, I can search and query etc, it's just not rendering markdown as I'd have expected it
HugoRCD
HugoRCD2d ago
How would you like the markdown to be rendered?
nathanr
nathanrOP2d ago
Nothing too crazy, just '#' for headers, and '-' for lists, as well as '1.' working for numbered lists too
nathanr
nathanrOP2d ago
Nuxt Content
Prose Components - Nuxt Content
A list of Prose components.
HugoRCD
HugoRCD2d ago
These styles are generated by NuxtUI Pro, which overrides all proses components!
HugoRCD
HugoRCD2d ago
If you want to have a relatively simple "similar" style, you can use the tailwind typography plugin: https://github.com/tailwindlabs/tailwindcss-typography
GitHub
GitHub - tailwindlabs/tailwindcss-typography: Beautiful typographic...
Beautiful typographic defaults for HTML you don't control. - tailwindlabs/tailwindcss-typography
nathanr
nathanrOP2d ago
Ah yes, I think I use this before, but didn't know how to customise it, would you say nuxt pro is better?
HugoRCD
HugoRCD2d ago
I'm a bit biased but yes, UI Pro will be more easily customizable and consistent with the theme of your app, you'll have all the features like '#' on headers and scroll that will be done the right way, etc. ... Then accessorily all the rest of the components and templates! 😁

Did you find this page helpful?