diff --git a/docs/content/4.api/4.advanced.md b/docs/content/4.api/4.advanced.md new file mode 100644 index 000000000..54406a53b --- /dev/null +++ b/docs/content/4.api/4.advanced.md @@ -0,0 +1,79 @@ +--- +title: 'Advanced' +description: 'Nuxt Content is highly customizable, giving you freedom and control over how the data is transformed.' +icon: heroicons-outline:lightning-bolt +--- + +## Hooks + +The module adds some hooks you can use: + +`content:file:*` hooks are available in nitro runtime, in order to use them you need to create custom [nitro plugin](https://nitro.unjs.io/guide/plugins.html). + +- Create a plugin in the `server/plugins/` directory + + ```ts [server/plugins/content.ts] + export default defineNitroPlugin((nitroApp) => { + // ... + }) + ``` + +- Register the plugin in `nuxt.config.ts` + + ```ts [nuxt.config.ts] + export default defineNuxtConfig({ + // ... + nitro: { + plugins: ['~/server/plugins/content.ts'] + } + }) + ``` + +### `content:file:beforeParse` + +Allows you to modify the contents of a file before it is handled by the parsers. + +Arguments: +- file + - Type: `Object` + - Properties: + - _id: `String` + - body: `String` + +### Example + +Changing all occurrences of react to vue in all Markdown files: + + +```ts [server/plugins/content.ts] +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook('content:file:beforeParse', (file) => { + if (file._id.endsWith('.md')) { + file.body = file.body.replace(/react/g, 'vue') + } + }) +}) +``` + +### `content:file:afterParse` + +Allows you to modify a document after being parsed by parsers. + +### Example + +Using content's first picture as cover image. + +```ts [server/plugins/content.ts] +import { visit } from 'unist-util-visit' + +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook('content:file:afterParse', (file) => { + if (file._id.endsWith('.md')) { + visit(file.body, (n:any) => n.tag === 'img', (node) => { + file.coverImage = node.props.src + }) + } + }) +}) + +``` \ No newline at end of file diff --git a/src/runtime/server/transformers/index.ts b/src/runtime/server/transformers/index.ts index 279b6e396..c4fbbd2fc 100644 --- a/src/runtime/server/transformers/index.ts +++ b/src/runtime/server/transformers/index.ts @@ -1,23 +1,27 @@ import { extname } from 'pathe' import type { ParsedContent, ContentTransformer } from '../../types' import { getParser, getTransformers } from '#content/virtual/transformers' - +// eslint-disable-next-line import/named +import { useNitroApp } from '#imports' /** * Parse content file using registered plugins */ export async function parseContent (id: string, content: string) { + const nitroApp = useNitroApp() + + // Call hook before parsing the file + const file = { _id: id, body: content } + await nitroApp.hooks.callHook('content:file:beforeParse', file) + const ext = extname(id) const plugin: ContentTransformer = getParser(ext) if (!plugin) { // eslint-disable-next-line no-console console.warn(`${ext} files are not supported, "${id}" falling back to raw content`) - return { - _id: id, - body: content - } + return file } - const parsed: ParsedContent = await plugin.parse!(id, content) + const parsed: ParsedContent = await plugin.parse!(file._id, file.body) const transformers = getTransformers(ext) const result = await transformers.reduce(async (prev, cur) => { @@ -26,5 +30,8 @@ export async function parseContent (id: string, content: string) { return cur.transform!(next) }, Promise.resolve(parsed)) + // Call hook after parsing the file + await nitroApp.hooks.callHook('content:file:afterParse', result) + return result } diff --git a/test/basic.test.ts b/test/basic.test.ts index f2d606e52..e4f74fdc8 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -11,6 +11,7 @@ import { testJSONParser } from './features/parser-json' import { testCSVParser } from './features/parser-csv' import { testRegex } from './features/regex' import { testMarkdownParserExcerpt } from './features/parser-markdown-excerpt' +import { testParserHooks } from './features/parser-hooks' describe('fixtures:basic', async () => { await setup({ @@ -114,4 +115,6 @@ describe('fixtures:basic', async () => { // testMDCComponent() testRegex() + + testParserHooks() }) diff --git a/test/features/parser-hooks.ts b/test/features/parser-hooks.ts new file mode 100644 index 000000000..ba7e63009 --- /dev/null +++ b/test/features/parser-hooks.ts @@ -0,0 +1,37 @@ +import { describe, test, expect, assert } from 'vitest' +import { $fetch } from '@nuxt/test-utils' + +export const testParserHooks = () => { + describe('parser:hooks', () => { + test('beforeParse', async () => { + const parsed = await $fetch('/api/parse', { + method: 'POST', + body: { + id: 'content:index.md', + content: '# hello' + } + }) + + expect(parsed).toHaveProperty('_id') + assert(parsed._id === 'content:index.md') + + expect(parsed).toHaveProperty('__beforeParse', true) + }) + + test('afterParse', async () => { + const parsed = await $fetch('/api/parse', { + method: 'POST', + body: { + id: 'content:index.md', + content: '# hello' + } + }) + + expect(parsed).toHaveProperty('_id') + assert(parsed._id === 'content:index.md') + + expect(parsed).haveOwnProperty('body') + expect(parsed).toHaveProperty('__afterParse', true) + }) + }) +} diff --git a/test/fixtures/basic/addons/nitro-plugin.ts b/test/fixtures/basic/addons/nitro-plugin.ts new file mode 100644 index 000000000..1ffc16453 --- /dev/null +++ b/test/fixtures/basic/addons/nitro-plugin.ts @@ -0,0 +1,17 @@ +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook('content:file:beforeParse', (file) => { + if (file._id.endsWith('.md')) { + if (file.body.startsWith('---')) { + const lines = file.body.split('\n') + lines.splice(1, 0, '__beforeParse: true') + file.body = lines.join('\n') + } else { + file.body = '---\n__beforeParse: true\n---\n' + file.body + } + } + }) + + nitroApp.hooks.hook('content:file:afterParse', (file) => { + file.__afterParse = true + }) +}) diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index 1c1abfc92..d06739c2c 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -3,6 +3,11 @@ import { resolve } from 'pathe' import contentModule from '../../..' export default defineNuxtConfig({ + nitro: { + plugins: [ + '~/addons/nitro-plugin.ts' + ] + }, components: { dirs: [ {