Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/content/docs/1.getting-started/3.configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ export default defineTransformer({

::

Read more about transformers in the [Transformers](/docs/advanced/transformers) documentation.

## `database`

By default Nuxt Content uses a local SQLite database to store and query content. If you like to use another database or you plan to deploy on Cloudflare Workers, you can modify this option.
Expand Down
139 changes: 139 additions & 0 deletions docs/content/docs/7.advanced/8.transformers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
title: Transformers
description: Transformers in Nuxt Content allow you to programmatically parse, modify, or analyze your content files as they are processed.
---

Transformers in Nuxt Content allow you to programmatically parse, modify, or analyze your content files as they are processed. They are especially useful for:

- Adding or modifying fields (e.g., appending to the title, generating slugs)
- Extracting metadata (e.g., listing used components)
- Enriching content with computed data
- Supporting new content types

## Defining a Transformer

You can define a transformer using the `defineTransformer` helper from `@nuxt/content`:

```ts [~~/transformers/title-suffix.ts]
import { defineTransformer } from '@nuxt/content'

export default defineTransformer({
name: 'title-suffix',
extensions: ['.md'], // File extensions to apply this transformer to
transform(file) {
// Modify the file object as needed
return {
...file,
title: file.title + ' (suffix)',
}
},
})
```

### Transformer Options

- `name` (string): A unique name for your transformer.
- `extensions` (string[]): File extensions this transformer should apply to (e.g., `['.md']`).
- `transform` (function): The function that receives the file object and returns the modified file.

## Registering Transformers

Transformers are registered in your `nuxt.config.ts`:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
content: {
build: {
transformers: [
'~~/transformers/title-suffix',
'~~/transformers/my-custom-transformer',
],
},
},
})
```

## Example: Adding Metadata

Transformers can add a `__metadata` field to the file. This field is not stored in the database but can be used for runtime logic.

```ts [~~/transformers/component-metadata.ts]
import { defineTransformer } from '@nuxt/content'

export default defineTransformer({
name: 'component-metadata',
extensions: ['.md'],
transform(file) {
// Example: Detect if a custom component is used
const usesMyComponent = file.body?.includes('<MyCustomComponent>')
return {
...file,
__metadata: {
components: usesMyComponent ? ['MyCustomComponent'] : [],
},
}
},
})
```

**Note:** The `__metadata` field is only available at runtime and is not persisted in the content database.


## API Reference

```ts
interface Transformer {
name: string
extensions: string[]
transform: (file: ContentFile) => ContentFile
}
```

- `ContentFile` is the object representing the parsed content file, including frontmatter, body, and other fields.


## Supporting New File Formats with Transformers

Transformers are not limited to modifying existing contentβ€”they can also be used to add support for new file formats in Nuxt Content. By defining a transformer with a custom `parse` method, you can instruct Nuxt Content how to read and process files with new extensions, such as YAML.

### Example: YAML File Support

Suppose you want to support `.yml` and `.yaml` files in your content directory. You can create a transformer that parses YAML frontmatter and body, and registers it for those extensions:

```ts [~~/transformers/yaml.ts]
import { defineTransformer } from '@nuxt/content'

export default defineTransformer({
name: 'Yaml',
extensions: ['.yml', '.yaml'],
parse: (file) => {
const { id, body } = file

// parse the body with your favorite yaml parser
const parsed = parseYaml(body)

return {
...parsed,
id,
}
},
})
```


Register your YAML transformer in your Nuxt config just like any other transformer:

```ts
export default defineNuxtConfig({
content: {
build: {
transformers: [
'~~/transformers/yaml',
// ...other transformers
],
},
},
})
```

This approach allows you to extend Nuxt Content to handle any custom file format you need.
14 changes: 13 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type { Manifest } from './types/manifest'
import { setupPreview, shouldEnablePreview } from './utils/preview/module'
import { parseSourceBase } from './utils/source'
import { databaseVersion, getLocalDatabase, refineDatabaseConfig, resolveDatabaseAdapter } from './utils/database'
import type { ParsedContentFile } from './types'

// Export public utils
export * from './utils'
Expand Down Expand Up @@ -253,6 +254,11 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio
let cachedFilesCount = 0
let parsedFilesCount = 0

// Store components used in the content provided by
// custom parsers using the `__metadata.components` field.
// This will allow to correctly generate production imports
const usedComponents: Array<string> = []

// Remove all existing content collections to start with a clean state
db.dropContentTables()
// Create database dump
Expand Down Expand Up @@ -302,7 +308,7 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio
const content = await source.getItem?.(key) || ''
const checksum = getContentChecksum(configHash + collectionHash + content)

let parsedContent
let parsedContent: ParsedContentFile
if (cache && cache.checksum === checksum) {
cachedFilesCount += 1
parsedContent = JSON.parse(cache.value)
Expand All @@ -319,6 +325,11 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio
}
}

// Add manually provided components from the content
if (parsedContent?.__metadata?.components) {
usedComponents.push(...parsedContent.__metadata.components)
}

const { queries, hash } = generateCollectionInsert(collection, parsedContent)
list.push([key, queries, hash])
}
Expand Down Expand Up @@ -366,6 +377,7 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio
const uniqueTags = [
...Object.values(options.renderer.alias || {}),
...new Set(tags),
...new Set(usedComponents),
]
.map(tag => getMappedTag(tag, options?.renderer?.alias))
.filter(tag => !htmlTags.includes(kebabCase(tag)))
Expand Down
12 changes: 11 additions & 1 deletion src/types/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ export interface ContentFile extends Record<string, unknown> {

export interface TransformedContent {
id: string
/**
* `__metadata` is a special field that transformers can provide information about the file.
* This field will not be stored in the database.
*/
__metadata?: {
components?: string[]

[key: string]: unknown
}
[key: string]: unknown
}

Expand Down Expand Up @@ -81,4 +90,5 @@ export interface MarkdownRoot extends MinimalTree {
toc?: Toc
}

export type ParsedContentFile = Record<string, unknown>
export interface ParsedContentFile extends TransformedContent {
}
8 changes: 5 additions & 3 deletions src/utils/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { createJiti } from 'jiti'
import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
import { visit } from 'unist-util-visit'
import type { ResolvedCollection } from '../../types/collection'
import type { FileAfterParseHook, FileBeforeParseHook, ModuleOptions, ContentFile, ContentTransformer } from '../../types'
import type { FileAfterParseHook, FileBeforeParseHook, ModuleOptions, ContentFile, ContentTransformer, ParsedContentFile } from '../../types'
import { logger } from '../dev'
import { transformContent } from './transformers'

Expand Down Expand Up @@ -168,7 +168,7 @@ export async function createParser(collection: ResolvedCollection, nuxt?: Nuxt)
...beforeParseCtx.parserOptions,
transformers: extraTransformers,
})
const { id: id, ...parsedContentFields } = parsedContent
const { id: id, __metadata, ...parsedContentFields } = parsedContent
const result = { id } as typeof collection.extendedSchema._type
const meta = {} as Record<string, unknown>

Expand All @@ -184,6 +184,8 @@ export async function createParser(collection: ResolvedCollection, nuxt?: Nuxt)

result.meta = meta

result.__metadata = __metadata || {}

// Storing `content` into `rawbody` field
if (collectionKeys.includes('rawbody')) {
result.rawbody = result.rawbody ?? file.body
Expand All @@ -195,7 +197,7 @@ export async function createParser(collection: ResolvedCollection, nuxt?: Nuxt)
result.seo.description = result.seo.description || result.description
}

const afterParseCtx: FileAfterParseHook = { file: hookedFile, content: result, collection }
const afterParseCtx: FileAfterParseHook = { file: hookedFile, content: result as ParsedContentFile, collection }
await nuxt?.callHook?.('content:file:afterParse', afterParseCtx)
return afterParseCtx.content
}
Expand Down