Skip to main content
DamienG
Code editor beside a diagram of connected services and databases

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 an image field declared in the collection schema. The original v2 versions used content.markdown and 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.

  1. Install remark-typography with your package manager of choice (bun/yarn/npm/pnpm)
  2. Add the plugin to your nuxt.config.js or ts file 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

  1. Information is only used to show your comment. See my Privacy Policy.