Guide

Nuxt Content Compatibility

nuxt-tina works standalone: this covers the optional, manual bridge to @nuxt/content.

You don't need Nuxt Content at all. useTina, TinaMarkdown, TinaProvider, and the visual editing bridge all read directly from Tina's own generated GraphQL client and have zero dependency on @nuxt/content. It isn't installed by this module, and nothing breaks without it.

Everything below is for people who also want to query the same content through Nuxt Content (search, listings, queryCollection()) alongside Tina's editing. @nuxt/content is not a dependency of this module; install it yourself:

npm install @nuxt/content

The two systems don't talk to each other automatically

You keep writing fields once, in tina/config.ts, and point both configs at the same files by hand:

  • Folder path: Tina's collection path (e.g. content/pages) is relative to the project root and includes content/. Nuxt Content's source glob (e.g. pages/*.md) is relative to its own content directory (content/ by default) and does not repeat that prefix. Get this wrong and the two systems silently point at different files.
  • Schema: instead of hand-writing the frontmatter shape twice (once as Tina fields, once as a Zod schema), derive Nuxt Content's schema from the Tina fields you already wrote.

tinaFieldsToZodShape()

Exported from nuxt-tina/utils. It's a one-way, build-time helper: Tina fields → Zod shape: not a runtime sync. There's no helper for the reverse direction, so tina/config.ts stays the one place field definitions belong.

// content.config.ts
import { defineCollection, defineContentConfig, z } from '@nuxt/content'
import { tinaFieldsToZodShape } from 'nuxt-tina/utils'
import tinaConfig from './tina/config'

const postFields = tinaConfig.schema.collections.find(c => c.name === 'post')!.fields

export default defineContentConfig({
  collections: {
    posts: defineCollection({
      type: 'page',
      source: 'posts/*.md',
      schema: z.object(tinaFieldsToZodShape(postFields, z)),
    }),
  },
})

content.config.ts should contain no manually written field definitions: just this derive call per collection.

How the mapping works

  • Fields marked isBody: true are skipped: Nuxt Content parses the markdown body itself, it isn't a frontmatter field.
  • Type mapping: string/datetime/image → z.string(), number → z.number(), boolean → z.boolean(), object (with nested fields) → z.object() recursively, rich-text → z.any() (Tina stores rich-text as an AST, not a string).
  • Fields with list: true are wrapped in z.array().
  • Every field ends up .optional() on the Zod side: "required" validation is still handled by Tina's editor form; Nuxt Content here only needs to know the data's shape for querying.

If one field needs stricter Zod validation than the derived type (an email format, say), override it after the spread instead of writing the whole schema by hand:

schema: z.object({
  ...tinaFieldsToZodShape(postFields, z),
  email: z.string().email(),
})

When this doesn't fit

The mapping is one-way and only covers the common cases. If a collection needs more specific Zod validation (regex, enums, custom refinements) across the board, write that schema by hand as usual: this helper is optional, not required.

Copyright © 2026