diff --git a/docs/content/docs/1.getting-started/3.configuration.md b/docs/content/docs/1.getting-started/3.configuration.md index 830bd6b2f..2ab90b164 100644 --- a/docs/content/docs/1.getting-started/3.configuration.md +++ b/docs/content/docs/1.getting-started/3.configuration.md @@ -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. diff --git a/docs/content/docs/7.advanced/8.transformers.md b/docs/content/docs/7.advanced/8.transformers.md new file mode 100644 index 000000000..51ba0deec --- /dev/null +++ b/docs/content/docs/7.advanced/8.transformers.md @@ -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('') + 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. diff --git a/src/module.ts b/src/module.ts index 10f4b542e..ee5d3df3f 100644 --- a/src/module.ts +++ b/src/module.ts @@ -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' @@ -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 = [] + // Remove all existing content collections to start with a clean state db.dropContentTables() // Create database dump @@ -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) @@ -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]) } @@ -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))) diff --git a/src/types/content.ts b/src/types/content.ts index 15f0b1dbb..3a8786abe 100644 --- a/src/types/content.ts +++ b/src/types/content.ts @@ -13,6 +13,15 @@ export interface ContentFile extends Record { 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 } @@ -81,4 +90,5 @@ export interface MarkdownRoot extends MinimalTree { toc?: Toc } -export type ParsedContentFile = Record +export interface ParsedContentFile extends TransformedContent { +} diff --git a/src/utils/content/index.ts b/src/utils/content/index.ts index c8e3c1e6c..2e80aaf3e 100644 --- a/src/utils/content/index.ts +++ b/src/utils/content/index.ts @@ -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' @@ -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 @@ -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 @@ -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 }