Content model

Documents, media, and the shape of an entry.

Every entry Comark Content knows about is one of two kinds: a document or a media file. Each is described as a ContentFile with three parts: data, nodes, and meta. Understanding that shape explains what every read API hands back.

Documents vs Media

A source exposes keys. The extension decides how each is treated. Keys ending in .md are parsed into a document. Everything else (images, videos, downloads) is tracked as media (indexed by path, served as raw bytes, never parsed).

const page = await content.get('/guide')        // kind: 'document'
page?.meta.kind    // 'document'
page?.nodes        // parsed AST

// media is addressed by its path but carries no `nodes`

Other extensions become documents only when a plugin registers a parser for them (the json and yaml plugins do exactly this).

The shape of a document

content.get() returns a ContentFile. Its content lives in three fields:

interface ContentFile {
  path: string                 // public URL path, e.g. '/guide'
  data: Record<string, any>    // frontmatter
  nodes: Node[]                // the parsed body (a MarkdownDocument's nodes)
  meta: ContentFileMeta            // key, source, stem, extension, kind, type, partial
}
  • data is the frontmatter, typed per source (see Type safety).
  • nodes is the parsed markdown body as an array of nodesMarkdownDocument
  • meta is what the Content instance knows about the file, not its content.

When getting a Markdown document, you can use Comark's <MarkdownDocument> to render it in your favourite UI library.

Keys vs paths

Two identifiers travel with every entry, and mixing them up is the most common source of confusion.

  • meta.key is the entry's identity inside the manifest: <source>/<stem><ext>, e.g. docs/guide.md. It's stable and never affected by a source's prefix.
  • path is the public address you read by: /guide (or /blog/guide if the source has prefix: '/blog').
keys.ts
// source 'docs' mounted with prefix '/handbook'
const page = await content.get('/handbook/onboarding') // path
page?.meta.key   // 'docs/onboarding.md' (prefix does NOT appear here)

The read must be done by path, but the key is the identifier that powers how sources, the manifest, and the cache address the same entry internally.

Full files vs listings

Not every API returns the body. get() returns the full ContentFile with nodes; list(), stat(), and query() return a lighter ContentListFile (the same shape without nodes) because listings don't need parsed bodies.

listing.ts
const page = await content.get('/guide')  // ContentFile (has nodes)
const all = await content.list()          // ContentListFile[] (no nodes)

Listings stay cheap: list() touches only frontmatter, while get() parses the body on demand and caches it.