
I've already covered reading time and generated excerpts for Nuxt3 content but there are still a couple more things you can do to enhance your content articles.
Updated October 2026: Updated for Nuxt Content v3. Remark plugins now live under
content.build.markdown, and the first-image code is now a build-time hook working on v3's "minimark" body with animagefield declared in the collection schema. The original v2 versions usedcontent.markdownand a Nitro plugin.
Enhanced typography
I love typography and that includes the use of em-dashes, smart-quotes and other typographic niceties. I do not however enjoy having to type them in my simple markdown files. Luckily, Nuxt Content is built on top of Remark which has many plugins including remark-typography which does exactly what we want.
- Install
remark-typographywith your package manager of choice (bun/yarn/npm/pnpm) - Add the plugin to your
nuxt.config.jsortsfile via:
content: {
build: {
markdown: {
remarkPlugins: {
"remark-typography": {},
},
},
},
},
In Content v2 the markdown block sat directly under content; in v3 it moved under content.build. I've checked that build.markdown.remarkPlugins is the right place in v3's types but haven't re-tested remark-typography itself against the current remark version, so give your output a quick look after enabling it.
If you want more control also consider checking out the Remark-Textr plugin which lets you build the options you like.
Set front-matter image from content
It's great when rendering cards or links to other pages (or when using RSS) to be able to specify an image for the post but again, it's a pain to do it manually!
Fortunately, a tiny bit of code can be used to grab the first image from the content and set it as the image property on the content object in much the same pattern as the reading time and excerpt plugins.
As with those, v3 needs the field in your collection schema in content.config.ts first:
schema: z.object({
// ...your other front matter
image: z
.object({ src: z.string(), alt: z.string().optional() })
.passthrough()
.optional(),
}),
Then a small local module (e.g. modules/content-image.ts) can find the first img anywhere in the body, keeping its src and alt:
import { defineNuxtModule } from "@nuxt/kit"
type MinimarkElement = [string, Record<string, any>?, ...MinimarkNode[]]
type MinimarkNode = string | MinimarkElement
export default defineNuxtModule({
meta: { name: "content-image" },
setup(_options, nuxt) {
nuxt.hook("content:file:afterParse", (ctx) => {
const { file, content } = ctx
if (file.id.endsWith(".md") && content.body) {
addFirstImage(content)
}
})
},
})
const addFirstImage = (content: any) => {
if (content.image) return // front matter wins
const img = findFirstImage((content.body?.value as MinimarkNode[]) ?? [])
if (img) content.image = img[1] ?? {}
}
const findFirstImage = (nodes: MinimarkNode[]): MinimarkElement | undefined => {
for (const node of nodes) {
if (!Array.isArray(node)) continue
if (node[0] === "img") return node
const child = findFirstImage(node.slice(2) as MinimarkNode[])
if (child) return child
}
}
That's the same approach this site uses, so article.image.src and article.image.alt are there for the cards and the RSS feed.
Original (Content v2)
For reference, the v2 version was a Nitro plugin that only kept the src:
import type { MarkdownNode, MarkdownRoot, ParsedContent } from "@nuxt/content"
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook("content:file:afterParse", (file) => {
if (file._id.endsWith(".md") && file.body) {
addFirstImage(file)
}
})
})
const addFirstImage = (file: ParsedContent) => {
if (file.image) return
visit(file.body!.children, (n) => {
if (n?.tag == "img") {
file.image = n.props.src
}
})
}
Damien
0 responses