Filesystem Source

Mount a local directory of Markdown files to query and watch for changes.

The fs source reads documents from a local directory through unstorage's filesystem driver, and adds chokidar-based watching that content.watch() wires into the manifest and cache.

fs(base, options?)

Creates a filesystem source from a local content/ directory.

content.ts
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'

const content = comarkContent({
  source: fs('./content'),
})

Parameters:

  • base - Root directory to scan, or the path to a single file. A relative path resolves against the process working directory, pass an absolute path for predictable behaviour across tools or use the cwd option to resolve against a module.
  • options? - Optional source options.

If base points at an existing file, the source serves just that file; otherwise it scans the directory. Missing paths are treated as directories. Use single to set this explicitly — for example when the file may not exist yet.

Returns: Source. Pass it to comarkContent({ source }), or into a named sources map.

Options

The optional second argument, FSSourceOptions:

OptionTypeDefaultDescription
cwdstring | URLprocess.cwd()Directory that a relative base is resolved against. Accepts a directory path or a file URL such as import.meta.url.
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.
singlebooleanauto-detectedTreat base as a single file rather than a directory.
ignorestring[]unstorage defaultGlobs forwarded to the unstorage fs driver.
watchOptionsWatchOptionsunstorage defaultChokidar options for the watcher.
Any other option is forwarded to unstorage/drivers/fs.

cwd

Specify the working directory for resolving the path of the source directory.

content.ts
const content = comarkContent({
  source: fs('content', { cwd: import.meta.url }),
})

This will resolve content against the current module's directory, ensuring consistent behaviour across tools.

cwd also accepts a directory path (defaults to process.cwd()). An absolute base is used as-is regardless of cwd.

prefix

Prepended to every entry's public path. Useful when a folder represents a section of the URL space:

prefixed.ts
const content = comarkContent({
  sources: {
    docs: fs('./content/docs'),
    blog: fs('./content/blog', { prefix: '/blog' }),
  },
})

await content.get('/blog/2026/hello')  // → content/blog/2026/hello.md

The prefix does not affect the key (which stays <source>/<stem>, e.g. blog/2026/hello.md, 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
fs('./content', {
  exclude: [
    'drafts/**',
    '**/*.draft.md',
    '**/_*',
  ],
})

schema

An explicit JSON Schema for the source's data. The Content instance uses it to generate types and query columns. Register the schema-validation plugin to validate documents against it.

single

Whether to treat base as a single file instead of a directory. By default the source inspects the filesystem and infers this automatically; set it explicitly to skip that check — for example when the target file may not exist at construction time.

single-file.ts
// A single YAML config file that may be created later
fs('./data/config.yaml', { single: true })

ignore

Globs forwarded to the underlying unstorage fs driver, which skips matching files at the filesystem layer before the source lists them. Reach for exclude instead when you want source-level filtering that also suppresses watcher events.

watchOptions

Chokidar options forwarded to the fs driver's watcher (for example ignored or awaitWriteFinish). See Watching for how to consume the events it produces.

Watch

The fs source ships chokidar watch support out of the box. content.watch() starts a watcher for every source whose driver supports it, keeps the manifest and cache in sync as files change, and returns a stop function. React to changes through the watch:file:update and watch:file:remove hooks:

watch.ts
import { content } from './content'

content.hooks.hook('watch:file:update', (source, key, file) => {
  console.log('update', source, key)  // 'default' 'posts/hello.md'
  // `file` is the freshly parsed ContentFile
})

content.hooks.hook('watch:file:remove', (source, key) => {
  console.log('remove', source, key)
})

const stop = await content.watch()

// later, when shutting down:
await stop()
See watch API reference for the full event model, rename semantics, and multi-source watching.