Artifacts & Hydration
The cache produces two kinds of CacheArtifact:
- The manifest artifact: the whole manifest index (every entry's metadata, no bodies).
- The snapshot artifact: one source's fully-parsed files (bodies included).
Both are compact, checksum-protected exports that decouple the cost of parsing from the cost of serving: parse once, distribute, hydrate many times.
Usage
Static deploys
Browser-side Content
sqliteWasm() hydrates from a snapshot: no server needed.Predictable startup
API
The artifact surface is small: one interface and three operations (generate an artifact, load it back, and verify its integrity).
Interface
Both kinds are a CacheArtifact: the manifest artifact (from content.cache.manifest()) carries the manifest index; a snapshot (from content.cache.snapshot()) carries one source's parsed files. Each is serialised with a checksum.
interface CacheArtifact {
name: string // 'manifest' or a source name
data: string // JSON, optionally gzipped + base64
checksum: string // SHA-256 of `data`
size: number // bytes
}The data is either raw JSON or gzipped JSON encoded as base64 (detected by an H4sI prefix). The Content instance handles both on load.
Generate
Build artifacts on the server or at build time. Export the manifest and each source:
import { writeFile, mkdir } from 'node:fs/promises'
import { content } from './content'
await mkdir('.cache', { recursive: true })
const manifest = await content.cache.manifest()
await writeFile('.cache/manifest.json', JSON.stringify(manifest))
for (const source of content.manifest.sources) {
const snapshot = await content.cache.snapshot(source)
if (snapshot) await writeFile(`.cache/${source}.json`, JSON.stringify(snapshot))
}partial is set to false in the example above so every body is parsed and stored in the cache. By default, only metadata is parsed and stored in the manifest.content.cache.manifest(options?)
Serialises the manifest into a CacheArtifact. By default (fresh: true) it runs content.init() first, so you can call it without initialising the Content instance yourself. Pass fresh: false to return the persisted index artifact straight from the cache driver (or null when nothing is persisted).
Parameters:
options: an optionalCacheArtifactOptionsobject.
Returns: Promise<CacheArtifact | null>
content.cache.snapshot(source, options?)
Serialises one source's parsed files into a CacheArtifact, or null when the source is unknown.
Parameters:
source: the name of the source to export.options: an optionalCacheArtifactOptionsobject.
Returns: Promise<CacheArtifact | null>
Options
Both methods accept a CacheArtifactOptions:
| Option | Type | Default | Description |
|---|---|---|---|
compress | boolean | true | Gzip + base64 the payload. false keeps raw JSON, handy for debugging or for storage layers that compress their own data. |
fresh | boolean | true | Rebuild from the source before exporting (manifest() runs init(); snapshot() re-reads the source). false uses what's already cached — the persisted index for manifest(), prior-init() entries for snapshot(). |
const raw = await content.cache.snapshot('content', { compress: false })Decode
sqlQuery and full-text search plugins both do this). Most apps hydrate the Content instance itself with loadManifest/loadSnapshot below instead.readArtifact<T>(artifact) decodes a CacheArtifact back into its parsed value, verifying its checksum:
import { readArtifact } from 'comark-content'
const snapshot = await content.cache.snapshot('content', { compress: false })
const files = snapshot ? await readArtifact<ContentFile[]>(snapshot) : []Parameters:
artifact: aCacheArtifact(fromcontent.cache.manifest()orcontent.cache.snapshot(), or one you loaded yourself).
Returns: Promise<T>, rejecting if the checksum doesn't match.
Load
Pass the loadManifest and loadSnapshot loaders on CacheOptions when creating the Content instance. They're called lazily: loadManifest on content.init() (for the manifest), then loadSnapshot per source on the first content.get():
import { comarkContent } from 'comark-content'
export const content = comarkContent({
cache: {
loadManifest: () =>
fetch('/api/content/manifest.json').then(r => r.ok ? r.json() : null),
loadSnapshot: (name) =>
fetch(`/api/content/snapshot/${name}.json`).then(r => r.ok ? r.json() : null),
},
})
// First call to any source's get() triggers that source's hydration:
const page = await content.get('/posts/hello')loadManifest
() => Promise<CacheArtifact | null>. Called once on content.init() to hydrate the manifest from a manifest artifact instead of listing the source.
loadSnapshot
(source: string) => Promise<CacheArtifact | null>. Called the first time each source is read to hydrate its parsed bodies.
Verify
Every loaded artifact is verified against its checksum before installing. A tampered or truncated artifact throws during hydration (inside init() or the first get()):
try {
await content.init()
} catch (err) {
console.error('Failed to verify manifest artifact:', err)
}The checksum is SHA-256 of the data string (gzipped or raw, as-stored).
Lifecycle
The same artifacts travel from the build step to the browser, verified and installed on demand:
The Content instance tracks which sources have been hydrated so each snapshot is fetched at most once per Content instance.
Size
| Corpus | Manifest artifact | Source snapshot | Notes |
|---|---|---|---|
| Small docs site (50 pages) | 5-15 KB | 30-80 KB | Ship inline |
| Medium docs (200 pages) | 15-40 KB | 100-300 KB | Lazy per source |
| Large docs (1k+ pages) | 50-150 KB | 500 KB+ | Split per section / source |
All numbers are gzipped + base64. Raw JSON is roughly 2-3× larger.
For framework wiring, see the Vite + Vue and Nuxt examples.