Key-Value Source
The unstorage source is a generic adapter that supports 20+ popular drivers such as Redis, S3, and more. Use it directly when you need a backend the built-ins don't cover.
unstorageSource(options)
Wraps any unstorage driver as a source.
import { comarkContent } from 'comark-content'
import unstorageSource from 'comark-content/sources/unstorage'
import s3Driver from 'unstorage/drivers/s3'
const content = comarkContent({
source: unstorageSource({
driver: s3Driver({
accessKeyId: process.env.AWS_ACCESS_KEY!,
secretAccessKey: process.env.AWS_SECRET_KEY!,
bucket: 'my-content',
region: 'us-east-1',
}),
}),
})Parameters:
options- Configuration object.
Returns: Source. Pass it to comarkContent({ source }), or into a named sources map.
Options
| Option | Type | Default | Description |
|---|---|---|---|
driver | Driver | - | Any unstorage driver. Required. |
prefix | string | undefined | Prepended to each entry's path (e.g. /blog). |
exclude | string[] | undefined | Picomatch globs of keys to drop before parsing. |
schema | JsonSchema | undefined | Explicit document data schema for types and query columns. |
driver
Any unstorage driver. The source delegates keys, getItem, getItemRaw, and watch straight to the driver, so there's no extra surface to learn beyond unstorage itself. See Compatible drivers for the common ones.
prefix
Prepended to every entry's public path. The prefix does not affect the key (which stays <source>/<stem> inside the manifest), only the public path returned by content.get() and content.list().
exclude
Picomatch globs matched against each entry's key. Excluded files are dropped before parsing, so they never enter the manifest:
unstorageSource({
driver: s3Driver({ /* ... */ }),
exclude: ['drafts/**', '**/*.draft.md'],
})schema
An explicit JSON Schema for the source's data. When set, the Content instance reports Markdown, JSON, and YAML frontmatter validation issues during source load and on direct updates, and uses the schema to generate types and query columns.
Compatible drivers
Every unstorage driver works. The most common ones for Content use:
S3 / R2
Cloudflare KV
HTTP
Redis
In-memory
Vercel Runtime Cache
Refresh
Unlike the filesystem driver, remote unstorage drivers (HTTP, S3, KV, Redis, memory) don't emit change events. The manifest is built once when the Content instance initialises and won't pick up new or changed files on its own, so refresh on your own schedule rather than relying on a watcher.
If you keep a cache, give it a ttl. With the default swr strategy this is lazy, not a timer: the next read of an entry older than ttl serves the cached (stale) copy immediately and re-reads the source in the background, so fresh content lands on the following read. Nothing refreshes until the content is read again after it ages out.
const content = comarkContent({
source: unstorageSource({ driver }),
cache: { ttl: 60000 }, // after 60s, the next read serves stale and refreshes in the background
})Or refresh explicitly from a webhook or admin action. You can re-read a whole source, or drop a single entry so the next read re-parses it:
import { content } from './content'
await content.cache.refresh('default') // re-read every entry for the source
await content.cache.invalidate('default:posts/hello.md') // drop one entry (key is `<source>:<path>`)watch (the filesystem driver does, and custom drivers can), content.watch() picks it up automatically. See watch API reference for the full event model.