Caching
The cache is on by default (in-memory) and configured through the cache option (CacheOptions) on comarkContent(). This page covers what it stores, how to configure freshness and storage, and how to refresh it at runtime.
What's cached
The cache holds two things:
Document bodies
Document bodies are stored once parsed. They are keyed by <source>:<path> (e.g. docs:guide.md).
Manifest index
The manifest index is under a single manifest key. The manifest is held in memory as the working copy and persisted to the cache, so with a shared or persistent driver a fresh instance restores it instead of re-reading the source.
How it works
The two live in the same cache but populate on different schedules: the manifest once at init, bodies lazily per read.
Manifest
Built once, then reused across instances:
- On
content.init()the cache is checked for a stored manifest. - On a hit, it's restored into memory with no source walk.
- On a miss, it's built from the source (reading each file's frontmatter) and persisted to the cache.
- Mutations (
update,remove,content.cache.refresh) re-persist it.
With cache: false there's nothing to restore from, so it's rebuilt in memory on every instance.
Document bodies
Populated lazily, one entry at a time:
- The first
content.get()for a path parses the body. - The body is stored in the cache.
- Later reads return the stored copy until it's invalidated or expires.
Options
Pass a CacheOptions object as cache to comarkContent() (or false to disable it):
const content = comarkContent({
source: fs('./content'),
cache: {
ttl: 1000 * 60 * 60, // 1 hour
strategy: 'swr', // default
},
})| Option | Type | Default | Description |
|---|---|---|---|
driver | Driver | in-memory | unstorage driver backing the cache. |
ttl | number | undefined | Milliseconds before an entry is stale. Omit to never expire. |
strategy | CacheStrategy | 'swr' | How a stale entry is served ('swr' or 'none'). |
loadManifest | () => Promise<CacheArtifact> | undefined | Hydrate the manifest from an external manifest artifact (overrides cache persistence). |
loadSnapshot | (source) => Promise<CacheArtifact> | undefined | Hydrate a source's bodies from a snapshot. |
driver
Any unstorage driver. Defaults to an in-memory driver, which is per-instance. Point it at Redis, a Vercel Runtime Cache, or anything unstorage supports to share the cache across instances:
import redisDriver from 'unstorage/drivers/redis'
comarkContent({
source: fs('./content'),
cache: { driver: redisDriver({ /* ... */ }) },
})ttl
Milliseconds before a cached entry is considered stale. Omit it (the default) and entries never expire on their own. What happens once an entry is stale is governed by strategy.
strategy
swr(default): stale-while-revalidate. A read of a stale entry returns the cached copy immediately and re-reads the source in the background, so fresh content lands on the following read. It's lazy, not a timer: nothing refreshes until the entry is read again afterttl.none: no background refresh. A stale entry is treated as a cache miss and re-read from the source on the nextget(). With nottlset, entries never go stale, so reads always hit the cache.
loadManifest
An explicit loader that returns a CacheArtifact (or null) to hydrate the manifest from a pre-built manifest artifact on content.init(). Use it when there's no shared or persistent driver to restore from (a browser/WASM client, or a static deploy): it takes precedence over the cache's own manifest persistence. See Artifacts & hydration.
loadSnapshot
A loader that returns a CacheArtifact (or null) to hydrate a single source's bodies from a snapshot the first time that source is read. See Artifacts & hydration.
Refresh
The resolved Cache is exposed as content.cache. When a source's content changes and there's no watcher (the usual case for remote sources), refresh it yourself from a webhook or admin action:
| Method | Effect |
|---|---|
content.cache.refresh(source) | Re-read a whole source: re-parse its entries and reconcile the manifest (add new files, drop deleted ones). |
content.cache.invalidate(key) | Remove one entry (<source>:<path>) so the next get() re-parses it. |
content.cache.expire(key) | Mark one entry stale without removing it, so swr refreshes it on the next read. |
await content.cache.refresh('default') // re-read every entry for the source
await content.cache.invalidate('default:posts/hello.md') // drop a single entryDisable
Pass cache: false and every content.get() re-parses from the source:
comarkContent({ source: fs('./content'), cache: false })With the cache off, the manifest is rebuilt in memory on each instance and never persisted, so list() and navigation() still work, just without the cold-start shortcut.