Nuxt Content Compatibility
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 includescontent/. Nuxt Content'ssourceglob (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: trueare 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 nestedfields) →z.object()recursively,rich-text→z.any()(Tina stores rich-text as an AST, not a string). - Fields with
list: trueare wrapped inz.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.