From 7085e2e6886c84595fee2e3cfef0ffe5c84bfa8d Mon Sep 17 00:00:00 2001 From: hareland Date: Thu, 26 Feb 2026 02:42:28 +0100 Subject: [PATCH 1/8] feat: manifest hook --- docs/content/docs/8.advanced/5.hooks.md | 16 ++++++++++ src/types/hooks.ts | 2 ++ test/unit/hooks.test.ts | 40 +++++++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/docs/content/docs/8.advanced/5.hooks.md b/docs/content/docs/8.advanced/5.hooks.md index 5c8d48769..fcc73a54c 100644 --- a/docs/content/docs/8.advanced/5.hooks.md +++ b/docs/content/docs/8.advanced/5.hooks.md @@ -36,6 +36,22 @@ export default defineNuxtConfig({ }) ``` + +## `content:manifest`{lang="ts"} + +This hook is called after the manifest is generated. + +```ts +export default defineNuxtConfig({ + hooks: { + 'content:manifest'(ctx) { + // ... + // ctx.collections.push(defineCollection()) + } + } +}) +``` + ## Example Usage ```ts [nuxt.config.ts] diff --git a/src/types/hooks.ts b/src/types/hooks.ts index 331b22035..737d91e5f 100644 --- a/src/types/hooks.ts +++ b/src/types/hooks.ts @@ -1,6 +1,7 @@ import type { ResolvedCollection } from './collection' import type { ContentFile, ParsedContentFile } from './content' import type { PathMetaOptions } from './path-meta' +import type { Manifest } from './manifest'; // Parser options interface interface ParserOptions { @@ -32,5 +33,6 @@ declare module '@nuxt/schema' { interface NuxtHooks { 'content:file:beforeParse': (ctx: FileBeforeParseHook) => Promise | void 'content:file:afterParse': (ctx: FileAfterParseHook) => Promise | void + 'content:manifest': (manifest: Manifest) => Promise | void } } diff --git a/test/unit/hooks.test.ts b/test/unit/hooks.test.ts index 6b02335e3..aefd60166 100644 --- a/test/unit/hooks.test.ts +++ b/test/unit/hooks.test.ts @@ -5,6 +5,7 @@ import { resolveCollection } from '../../src/utils/collection' import { parseContent } from '../utils/content' import type { FileAfterParseHook, FileBeforeParseHook } from '../../src/types' import { initiateValidatorsContext } from '../../src/utils/dependencies' +import type { Manifest } from '../../src/types/manifest' describe('Hooks', async () => { await initiateValidatorsContext() @@ -57,4 +58,43 @@ foo: 'bar' expect(parsed.foo).toEqual('bar') expect(parsed.bar).toEqual('foo') }) + + it('content:manifest', async () => { + let hookCtx: Manifest | undefined + + const extraCollection = resolveCollection('injected', defineCollection({ + type: 'data', + source: 'extra/**', + schema: z.object({ + body: z.any(), + }), + }))! + + const manifest: Manifest = { + checksumStructure: {}, + checksum: {}, + dump: {}, + components: [], + collections: [collection], + } + + const nuxtMock = { + callHook(hook: string, ctx: Manifest) { + if (hook === 'content:manifest') { + ctx.collections.push(extraCollection) + hookCtx = ctx + } + }, + } + + // Simulate the hook call as done in the module setup + nuxtMock.callHook('content:manifest', manifest) + + expect(hookCtx).toBeDefined() + expect(hookCtx!.collections).toHaveLength(2) + expect(hookCtx!.collections[0].name).toEqual('hookTest') + expect(hookCtx!.collections[1].name).toEqual('injecte2') + // Ensure the manifest object is mutated by reference + expect(manifest.collections).toHaveLength(2) + }) }) From 2c5b6c38155c800def7c84bce97496258733e219 Mon Sep 17 00:00:00 2001 From: hareland Date: Thu, 26 Feb 2026 02:46:38 +0100 Subject: [PATCH 2/8] refactor: lint --- docs/content/docs/8.advanced/5.hooks.md | 1 - src/types/hooks.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/content/docs/8.advanced/5.hooks.md b/docs/content/docs/8.advanced/5.hooks.md index fcc73a54c..b5e259ad2 100644 --- a/docs/content/docs/8.advanced/5.hooks.md +++ b/docs/content/docs/8.advanced/5.hooks.md @@ -36,7 +36,6 @@ export default defineNuxtConfig({ }) ``` - ## `content:manifest`{lang="ts"} This hook is called after the manifest is generated. diff --git a/src/types/hooks.ts b/src/types/hooks.ts index 737d91e5f..69004a1ef 100644 --- a/src/types/hooks.ts +++ b/src/types/hooks.ts @@ -1,7 +1,7 @@ import type { ResolvedCollection } from './collection' import type { ContentFile, ParsedContentFile } from './content' import type { PathMetaOptions } from './path-meta' -import type { Manifest } from './manifest'; +import type { Manifest } from './manifest' // Parser options interface interface ParserOptions { From 71c808fa0a4726eb58f7f641e746a0332614b695 Mon Sep 17 00:00:00 2001 From: hareland Date: Thu, 26 Feb 2026 02:49:05 +0100 Subject: [PATCH 3/8] fix: test --- test/unit/hooks.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/hooks.test.ts b/test/unit/hooks.test.ts index aefd60166..03ecd456b 100644 --- a/test/unit/hooks.test.ts +++ b/test/unit/hooks.test.ts @@ -93,7 +93,7 @@ foo: 'bar' expect(hookCtx).toBeDefined() expect(hookCtx!.collections).toHaveLength(2) expect(hookCtx!.collections[0].name).toEqual('hookTest') - expect(hookCtx!.collections[1].name).toEqual('injecte2') + expect(hookCtx!.collections[1].name).toEqual('injected') // Ensure the manifest object is mutated by reference expect(manifest.collections).toHaveLength(2) }) From 55444297086c7fa3d522e3aefeba080fe7a26a4b Mon Sep 17 00:00:00 2001 From: hareland Date: Thu, 26 Feb 2026 02:54:53 +0100 Subject: [PATCH 4/8] fix: tests --- src/module.ts | 1 + test/unit/hooks.test.ts | 17 +++++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/module.ts b/src/module.ts index 99603aae6..f713b4d98 100644 --- a/src/module.ts +++ b/src/module.ts @@ -120,6 +120,7 @@ export default defineNuxtModule({ const { collections } = await loadContentConfig(nuxt, options) manifest.collections = collections + nuxt.callHook('content:manifest', manifest) nuxt.options.vite.optimizeDeps = defu(nuxt.options.vite.optimizeDeps, { exclude: ['@sqlite.org/sqlite-wasm'], diff --git a/test/unit/hooks.test.ts b/test/unit/hooks.test.ts index 03ecd456b..758b6265b 100644 --- a/test/unit/hooks.test.ts +++ b/test/unit/hooks.test.ts @@ -59,9 +59,7 @@ foo: 'bar' expect(parsed.bar).toEqual('foo') }) - it('content:manifest', async () => { - let hookCtx: Manifest | undefined - + it('content:manifest mutations are reflected in manifest', async () => { const extraCollection = resolveCollection('injected', defineCollection({ type: 'data', source: 'extra/**', @@ -78,23 +76,22 @@ foo: 'bar' collections: [collection], } + // Simulate the module calling the hook const nuxtMock = { callHook(hook: string, ctx: Manifest) { if (hook === 'content:manifest') { ctx.collections.push(extraCollection) - hookCtx = ctx } }, } - // Simulate the hook call as done in the module setup nuxtMock.callHook('content:manifest', manifest) - expect(hookCtx).toBeDefined() - expect(hookCtx!.collections).toHaveLength(2) - expect(hookCtx!.collections[0].name).toEqual('hookTest') - expect(hookCtx!.collections[1].name).toEqual('injected') - // Ensure the manifest object is mutated by reference + // must be visible on original manifest object expect(manifest.collections).toHaveLength(2) + // new collection is visible + expect(manifest.collections.find(c => c.name === 'injected')).toBeDefined() + // original collection is still exists + expect(manifest.collections.find(c => c.name === 'hookTest')).toBeDefined() }) }) From 633b11f906c3125764f9d679c37521d08cb7ac7e Mon Sep 17 00:00:00 2001 From: hareland Date: Thu, 26 Feb 2026 02:56:33 +0100 Subject: [PATCH 5/8] fix: typo --- test/unit/hooks.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/hooks.test.ts b/test/unit/hooks.test.ts index 758b6265b..1a1ac07a9 100644 --- a/test/unit/hooks.test.ts +++ b/test/unit/hooks.test.ts @@ -91,7 +91,7 @@ foo: 'bar' expect(manifest.collections).toHaveLength(2) // new collection is visible expect(manifest.collections.find(c => c.name === 'injected')).toBeDefined() - // original collection is still exists + // original collection still exists expect(manifest.collections.find(c => c.name === 'hookTest')).toBeDefined() }) }) From d236e62942aa296417baafcb75ffacd96d21a00c Mon Sep 17 00:00:00 2001 From: hareland Date: Thu, 26 Feb 2026 03:15:39 +0100 Subject: [PATCH 6/8] refactor: export resolveCollection --- src/utils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/index.ts b/src/utils/index.ts index a4088891e..992ffd218 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,5 @@ export { metaStandardSchema, pageStandardSchema, property } from './schema' -export { defineCollection, defineCollectionSource } from './collection' +export { resolveCollection, defineCollection, defineCollectionSource } from './collection' export { defineContentConfig } from './config' export { defineTransformer } from './content/transformers/utils' From 4e036799b74f72d728b2bf3b1c55900a249bbd7c Mon Sep 17 00:00:00 2001 From: hareland Date: Thu, 26 Feb 2026 03:33:43 +0100 Subject: [PATCH 7/8] refactor: improve formatting and ensure async call for hooks --- src/module.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/module.ts b/src/module.ts index f713b4d98..0843832d4 100644 --- a/src/module.ts +++ b/src/module.ts @@ -22,7 +22,13 @@ import { kebabCase, pascalCase } from 'scule' import defu from 'defu' import { version } from '../package.json' import { generateCollectionInsert, generateCollectionTableDefinition } from './utils/collection' -import { componentsManifestTemplate, contentTypesTemplate, fullDatabaseRawDumpTemplate, manifestTemplate, moduleTemplates } from './utils/templates' +import { + componentsManifestTemplate, + contentTypesTemplate, + fullDatabaseRawDumpTemplate, + manifestTemplate, + moduleTemplates, +} from './utils/templates' import type { ResolvedCollection } from './types/collection' import type { ModuleOptions } from './types/module' import { getContentChecksum, logger, chunks, NuxtContentHMRUnplugin } from './utils/dev' @@ -120,7 +126,7 @@ export default defineNuxtModule({ const { collections } = await loadContentConfig(nuxt, options) manifest.collections = collections - nuxt.callHook('content:manifest', manifest) + await nuxt.callHook('content:manifest', manifest) nuxt.options.vite.optimizeDeps = defu(nuxt.options.vite.optimizeDeps, { exclude: ['@sqlite.org/sqlite-wasm'], @@ -192,7 +198,10 @@ export default defineNuxtModule({ const preset = findPreset(nuxt) await preset.setupNitro(config, { manifest, resolver, moduleOptions: options, nuxt }) - const resolveOptions = { resolver, sqliteConnector: options.experimental?.sqliteConnector || (options.experimental?.nativeSqlite ? 'native' : undefined) } + const resolveOptions = { + resolver, + sqliteConnector: options.experimental?.sqliteConnector || (options.experimental?.nativeSqlite ? 'native' : undefined), + } config.alias ||= {} config.alias['#content/adapter'] = await resolveDatabaseAdapter(config.runtimeConfig!.content!.database?.type || options.database.type, resolveOptions) config.alias['#content/local-adapter'] = await resolveDatabaseAdapter(options._localDatabase!.type || 'sqlite', resolveOptions) @@ -401,13 +410,20 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio // NOTE: all queries having the structure comment at the end, will be ignored at init if no // structure changes are detected in the structureVersion `${generateCollectionTableDefinition(infoCollection, { drop: false })} -- structure`, - ...generateCollectionInsert(infoCollection, { id: `checksum_${collection.name}`, version, structureVersion, ready: false }).queries.map(row => `${row} -- meta`), + ...generateCollectionInsert(infoCollection, { + id: `checksum_${collection.name}`, + version, + structureVersion, + ready: false, + }).queries.map(row => `${row} -- meta`), // Insert queries for the collection ...collectionQueries, // and finally when we are finished, we update the info table to say that the init is done - `UPDATE ${infoCollection.tableName} SET ready = true WHERE id = 'checksum_${collection.name}'; -- meta`, + `UPDATE ${infoCollection.tableName} + SET ready = true + WHERE id = 'checksum_${collection.name}'; -- meta`, ] } @@ -461,6 +477,7 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio } const proseTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'strong', 'em', 's', 'code', 'span', 'blockquote', 'pre', 'hr', 'img', 'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'th', 'td'] + function getMappedTag(tag: string, additionalTags: Record = {}) { if (proseTags.includes(tag)) { return `prose-${tag}` From 763d5cada83441a459d2a0781253947dd5b4d726 Mon Sep 17 00:00:00 2001 From: hareland Date: Thu, 26 Feb 2026 03:35:57 +0100 Subject: [PATCH 8/8] docs: missing full usage of manifest hook --- docs/content/docs/8.advanced/5.hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/8.advanced/5.hooks.md b/docs/content/docs/8.advanced/5.hooks.md index b5e259ad2..363c6d272 100644 --- a/docs/content/docs/8.advanced/5.hooks.md +++ b/docs/content/docs/8.advanced/5.hooks.md @@ -45,7 +45,7 @@ export default defineNuxtConfig({ hooks: { 'content:manifest'(ctx) { // ... - // ctx.collections.push(defineCollection()) + // ctx.collections.push(resolveCollection('name', defineCollection())) } } })