Key-Value Source

Wrap any Key-Value driver as a source, powered by [unstorage](https://unstorage.unjs.io/).

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.

content.ts
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

OptionTypeDefaultDescription
driverDriver-Any unstorage driver. Required.
prefixstringundefinedPrepended to each entry's path (e.g. /blog).
excludestring[]undefinedPicomatch globs of keys to drop before parsing.
schemaJsonSchemaundefinedExplicit 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:

drafts.ts
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

Object storage with prefix-based listing.

Cloudflare KV

Durable KV for edge workers.

HTTP

Read-only over any HTTP endpoint that lists keys.

Redis

Drop-in for ephemeral or self-hosted Redis.

In-memory

Ideal for tests and seed fixtures.

Vercel Runtime Cache

Managed key-value on Vercel.
Checkout all the supported drivers for unstorage.

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.

ttl.ts
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:

refresh.ts
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>`)
See caching docs for TTL, the swr / none strategies, and shipping a static snapshot.
If a driver does implement watch (the filesystem driver does, and custom drivers can), content.watch() picks it up automatically. See watch API reference for the full event model.