Create a Comark Content Plugin
A plugin is a typed factory that extends the Content instance at construction time: it can register a parser for new file extensions, subscribe to hooks, and expose new methods on the instance.
Two plugin APIs are exposed to extend the Content instance:
comark-contentexportsdefineContentPluginfor the Content instance.comark-content/clientexportsdefineContentClientPluginfor the client.
defineContentPlugin(factory)
Wraps a plugin factory, providing type safety for both its options and the methods it contributes to the Content instance.
Parameters:
factory: a function(options?: Options) => { setup(content: ContentPluginContext): Methods | void | Promise<Methods | void> }. Thesetuphook runs at construction: it registers parsers, subscribes to hooks, and returns the methods to merge onto the instance. SeeContentPluginContext.
Type parameters:
| Parameter | Default | Description |
|---|---|---|
Options | unknown | Shape of the options the factory accepts. |
Methods | {} | Methods the plugin contributes; intersected into comarkContent()'s return type. |
Returns: a typed plugin factory (options?: Options) => ContentPlugin<Methods>, ready to pass to comarkContent({ plugins }).
import { defineContentPlugin } from 'comark-content'
export default defineContentPlugin(() => ({
setup(content) {
return { hello: (name: string) => `Hello, ${name}` }
},
}))defineContentClientPlugin(factory)
The client counterpart, imported from comark-content/client. Same shape, but setup receives the client (with its resolved options: baseURL, basePath, fetch) instead of the Content instance plugin context, and runs synchronously. Use it to add a method that calls a serve handler.
Parameters:
factory: a function(options?: Options) => { setup(client: ContentClientWithOptions): Methods | void }.
Type parameters:
| Parameter | Default | Description |
|---|---|---|
Options | unknown | Shape of the options the factory accepts. |
Methods | {} | Methods the plugin contributes; intersected into createContentClient()'s return type. |
Returns: a typed plugin factory (options?: Options) => ContentClientPlugin<Methods>, for createContentClient({ plugins }).
import { defineContentClientPlugin } from 'comark-content/client'
export default defineContentClientPlugin(() => ({
setup(client) {
return {
hello: (name: string) =>
client.options.fetch(`${client.options.basePath}/hello?name=${name}`),
}
},
}))Usage
A full round trip: a server plugin that adds content.wordCount(path), a route that exposes it, and a client plugin that consumes it from the browser.
Create plugin
The server plugin adds content.wordCount(path) and keeps a per-entry count in sync via hooks:
import { defineContentPlugin, type ContentFile } from 'comark-content'
interface WordCountMethods {
wordCount: (path: string) => Promise<number>
}
export default defineContentPlugin<{}, WordCountMethods>(() => {
const counts = new Map<string, number>()
function count(item: ContentFile): number {
let total = 0
function walk(node: unknown) {
if (typeof node === 'string') total += node.trim().split(/\s+/).filter(Boolean).length
else if (Array.isArray(node)) node.slice(2).forEach(walk)
}
item.nodes.forEach(walk)
return total
}
return {
setup(content) {
content.hooks.hook('file:upsert', (_source, _key, file) => counts.set(file.path, count(file)))
content.hooks.hook('file:remove', (_source, key) => counts.delete(key))
return {
async wordCount(path: string) {
if (counts.has(path)) return counts.get(path)!
const item = await content.get(path)
if (!item) return 0
const n = count(item)
counts.set(path, n)
return n
},
}
},
}
})Expose server route
Register a serve handler so wordCount is reachable under the word-count section, then forward the route to content.handler():
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'
import wordCount from './word-count'
const content = comarkContent({ source: fs('./content'), plugins: [wordCount()] })
content.addServeHandler('word-count', async (request) => {
const path = new URL(request.url).searchParams.get('path') ?? ''
return Response.json(await content.wordCount(path))
})
// Forward every /api/content/** request to the Content instance handler (Nitro shown).
export default defineEventHandler(event => content.handler(toWebRequest(event)))Consume from client
A client plugin calls the word-count section. Registered on a createContentClient() instance, it adds a typed content.wordCount() method to the browser client:
import { createContentClient, defineContentClientPlugin } from 'comark-content/client'
const wordCountClient = defineContentClientPlugin<{}, { wordCount: (path: string) => Promise<number> }>(() => ({
setup(client) {
return {
wordCount: (path) =>
client.options.fetch(`${client.options.basePath}/word-count?path=${encodeURIComponent(path)}`),
}
},
}))
const content = createContentClient({ basePath: '/api/content', plugins: [wordCountClient()] })
const words = await content.wordCount('/blog/hello')
// ^? number — computed on the server, fetched over HTTPawait long-running work inside setup: do it lazily inside the methods you expose.Hooks
The content.hooks bus (typed as ContentHooks) fires on every content change. Subscribe with content.hooks.hook(name, callback); handlers run in registration order.
| Hook | When | Payload |
|---|---|---|
file:parsed | After a file parser assembles a ContentFile | FileParsedContext — mutate or null file |
typegen:field | While generating source types | TypegenFieldContext — override type, register imports |
file:upsert | After content.get() parses, content.update(), or a watcher update | (source, key, file) |
file:remove | After content.remove() or a watcher removal | (source, key) |
file:bulk:load | After a source finishes parsing a batch | (source, files) |
watch:file:update | Raw watcher event before bookkeeping | (source, key, file) |
watch:file:remove | Raw watcher event before bookkeeping | (source, key) |
See content.hooks for the full event reference.