Skip to content

Commit baff541

Browse files
Mw3yfarnabaz
andauthored
feat(parser): allow extra transformers to provide components used (#3355)
--------- Co-authored-by: Farnabaz <[email protected]>
1 parent 2552bdb commit baff541

File tree

5 files changed

+170
-5
lines changed

5 files changed

+170
-5
lines changed

docs/content/docs/1.getting-started/3.configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ export default defineTransformer({
287287

288288
::
289289

290+
Read more about transformers in the [Transformers](/docs/advanced/transformers) documentation.
291+
290292
## `database`
291293

292294
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.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
---
2+
title: Transformers
3+
description: Transformers in Nuxt Content allow you to programmatically parse, modify, or analyze your content files as they are processed.
4+
---
5+
6+
Transformers in Nuxt Content allow you to programmatically parse, modify, or analyze your content files as they are processed. They are especially useful for:
7+
8+
- Adding or modifying fields (e.g., appending to the title, generating slugs)
9+
- Extracting metadata (e.g., listing used components)
10+
- Enriching content with computed data
11+
- Supporting new content types
12+
13+
## Defining a Transformer
14+
15+
You can define a transformer using the `defineTransformer` helper from `@nuxt/content`:
16+
17+
```ts [~~/transformers/title-suffix.ts]
18+
import { defineTransformer } from '@nuxt/content'
19+
20+
export default defineTransformer({
21+
name: 'title-suffix',
22+
extensions: ['.md'], // File extensions to apply this transformer to
23+
transform(file) {
24+
// Modify the file object as needed
25+
return {
26+
...file,
27+
title: file.title + ' (suffix)',
28+
}
29+
},
30+
})
31+
```
32+
33+
### Transformer Options
34+
35+
- `name` (string): A unique name for your transformer.
36+
- `extensions` (string[]): File extensions this transformer should apply to (e.g., `['.md']`).
37+
- `transform` (function): The function that receives the file object and returns the modified file.
38+
39+
## Registering Transformers
40+
41+
Transformers are registered in your `nuxt.config.ts`:
42+
43+
```ts [nuxt.config.ts]
44+
export default defineNuxtConfig({
45+
content: {
46+
build: {
47+
transformers: [
48+
'~~/transformers/title-suffix',
49+
'~~/transformers/my-custom-transformer',
50+
],
51+
},
52+
},
53+
})
54+
```
55+
56+
## Example: Adding Metadata
57+
58+
Transformers can add a `__metadata` field to the file. This field is not stored in the database but can be used for runtime logic.
59+
60+
```ts [~~/transformers/component-metadata.ts]
61+
import { defineTransformer } from '@nuxt/content'
62+
63+
export default defineTransformer({
64+
name: 'component-metadata',
65+
extensions: ['.md'],
66+
transform(file) {
67+
// Example: Detect if a custom component is used
68+
const usesMyComponent = file.body?.includes('<MyCustomComponent>')
69+
return {
70+
...file,
71+
__metadata: {
72+
components: usesMyComponent ? ['MyCustomComponent'] : [],
73+
},
74+
}
75+
},
76+
})
77+
```
78+
79+
**Note:** The `__metadata` field is only available at runtime and is not persisted in the content database.
80+
81+
82+
## API Reference
83+
84+
```ts
85+
interface Transformer {
86+
name: string
87+
extensions: string[]
88+
transform: (file: ContentFile) => ContentFile
89+
}
90+
```
91+
92+
- `ContentFile` is the object representing the parsed content file, including frontmatter, body, and other fields.
93+
94+
95+
## Supporting New File Formats with Transformers
96+
97+
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.
98+
99+
### Example: YAML File Support
100+
101+
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:
102+
103+
```ts [~~/transformers/yaml.ts]
104+
import { defineTransformer } from '@nuxt/content'
105+
106+
export default defineTransformer({
107+
name: 'Yaml',
108+
extensions: ['.yml', '.yaml'],
109+
parse: (file) => {
110+
const { id, body } = file
111+
112+
// parse the body with your favorite yaml parser
113+
const parsed = parseYaml(body)
114+
115+
return {
116+
...parsed,
117+
id,
118+
}
119+
},
120+
})
121+
```
122+
123+
124+
Register your YAML transformer in your Nuxt config just like any other transformer:
125+
126+
```ts
127+
export default defineNuxtConfig({
128+
content: {
129+
build: {
130+
transformers: [
131+
'~~/transformers/yaml',
132+
// ...other transformers
133+
],
134+
},
135+
},
136+
})
137+
```
138+
139+
This approach allows you to extend Nuxt Content to handle any custom file format you need.

src/module.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type { Manifest } from './types/manifest'
3333
import { setupPreview, shouldEnablePreview } from './utils/preview/module'
3434
import { parseSourceBase } from './utils/source'
3535
import { databaseVersion, getLocalDatabase, refineDatabaseConfig, resolveDatabaseAdapter } from './utils/database'
36+
import type { ParsedContentFile } from './types'
3637

3738
// Export public utils
3839
export * from './utils'
@@ -256,6 +257,11 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio
256257
let cachedFilesCount = 0
257258
let parsedFilesCount = 0
258259

260+
// Store components used in the content provided by
261+
// custom parsers using the `__metadata.components` field.
262+
// This will allow to correctly generate production imports
263+
const usedComponents: Array<string> = []
264+
259265
// Remove all existing content collections to start with a clean state
260266
db.dropContentTables()
261267
// Create database dump
@@ -305,7 +311,7 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio
305311
const content = await source.getItem?.(key) || ''
306312
const checksum = getContentChecksum(configHash + collectionHash + content)
307313

308-
let parsedContent
314+
let parsedContent: ParsedContentFile
309315
if (cache && cache.checksum === checksum) {
310316
cachedFilesCount += 1
311317
parsedContent = JSON.parse(cache.value)
@@ -322,6 +328,11 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio
322328
}
323329
}
324330

331+
// Add manually provided components from the content
332+
if (parsedContent?.__metadata?.components) {
333+
usedComponents.push(...parsedContent.__metadata.components)
334+
}
335+
325336
const { queries, hash } = generateCollectionInsert(collection, parsedContent)
326337
list.push([key, queries, hash])
327338
}
@@ -369,6 +380,7 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio
369380
const uniqueTags = [
370381
...Object.values(options.renderer.alias || {}),
371382
...new Set(tags),
383+
...new Set(usedComponents),
372384
]
373385
.map(tag => getMappedTag(tag, options?.renderer?.alias))
374386
.filter(tag => !htmlTags.includes(kebabCase(tag)))

src/types/content.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ export interface ContentFile extends Record<string, unknown> {
1313

1414
export interface TransformedContent {
1515
id: string
16+
/**
17+
* `__metadata` is a special field that transformers can provide information about the file.
18+
* This field will not be stored in the database.
19+
*/
20+
__metadata?: {
21+
components?: string[]
22+
23+
[key: string]: unknown
24+
}
1625
[key: string]: unknown
1726
}
1827

@@ -81,4 +90,5 @@ export interface MarkdownRoot extends MinimarkTree {
8190
toc?: Toc
8291
}
8392

84-
export type ParsedContentFile = Record<string, unknown>
93+
export interface ParsedContentFile extends TransformedContent {
94+
}

src/utils/content/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { createJiti } from 'jiti'
1010
import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
1111
import { visit } from 'unist-util-visit'
1212
import type { ResolvedCollection } from '../../types/collection'
13-
import type { FileAfterParseHook, FileBeforeParseHook, ModuleOptions, ContentFile, ContentTransformer } from '../../types'
13+
import type { FileAfterParseHook, FileBeforeParseHook, ModuleOptions, ContentFile, ContentTransformer, ParsedContentFile } from '../../types'
1414
import { logger } from '../dev'
1515
import { transformContent } from './transformers'
1616

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

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

185185
result.meta = meta
186186

187+
result.__metadata = __metadata || {}
188+
187189
// Storing `content` into `rawbody` field
188190
if (collectionKeys.includes('rawbody')) {
189191
result.rawbody = result.rawbody ?? file.body
@@ -195,7 +197,7 @@ export async function createParser(collection: ResolvedCollection, nuxt?: Nuxt)
195197
result.seo.description = result.seo.description || result.description
196198
}
197199

198-
const afterParseCtx: FileAfterParseHook = { file: hookedFile, content: result, collection }
200+
const afterParseCtx: FileAfterParseHook = { file: hookedFile, content: result as ParsedContentFile, collection }
199201
await nuxt?.callHook?.('content:file:afterParse', afterParseCtx)
200202
return afterParseCtx.content
201203
}

0 commit comments

Comments
 (0)