---
title: "Filesystem source"
description: "Mount a local directory of Markdown files to query and watch for changes."
canonical_url: "https://content.comark.dev/sources/filesystem"
---
# Filesystem source

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

The `fs` source reads a folder of Markdown on the machine your code runs on. It's the source every guide starts with: point it at a directory, and each file inside becomes a page. It's also the only built-in source that can watch for changes, which is what makes [edits show up while you develop](https://content.comark.dev/guide/watch).

It needs a filesystem, so it fits development, Node servers, and containers. For hosts that don't ship your files, see [Choose how to deploy](https://content.comark.dev/deployment).

## `fs(base, options?)`{lang="ts"}

Creates a filesystem source from a local directory.

```ts [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 behavior across tools, or use the `cwd` option to resolve against a module.
- `options?` - Optional source [options](#options).

If `base` points at an existing file, the source serves only that file; otherwise it scans the directory. Missing paths are treated as directories. Use [`single`](#options-single) to set this explicitly — for example when the file may not exist yet.

**Returns:** [`Source`](https://content.comark.dev/reference/types/content#source). Pass it to `comarkContent({ source })`.

## Options

The optional second argument, [`FSSourceOptions`](https://content.comark.dev/reference/types/content#fssourceoptions):

| Option                                  | Type            | Default           | Description                                                                                                             |
| --------------------------------------- | --------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [`cwd`](#options-cwd)                   | `string \| URL` | `process.cwd()`   | Directory that a relative `base` is resolved against. Accepts a directory path or a file URL such as `import.meta.url`. |
| [`prefix`](#options-prefix)             | `string`        | `undefined`       | Prepended to each entry's `path` (for example `/blog`).                                                                 |
| [`exclude`](#options-exclude)           | `string[]`      | `undefined`       | Picomatch globs of keys to drop before parsing.                                                                         |
| [`schema`](#options-schema)             | `JsonSchema`    | `undefined`       | Explicit document `data` schema for types and query columns.                                                            |
| [`single`](#options-single)             | `boolean`       | auto-detected     | Treat `base` as a single file rather than a directory.                                                                  |
| [`ignore`](#options-ignore)             | `string[]`      | unstorage default | Globs forwarded to the unstorage `fs` driver.                                                                           |
| [`watchOptions`](#options-watchoptions) | `WatchOptions`  | unstorage default | Chokidar options for the watcher.                                                                                       |

::note
Any other option is forwarded to [`unstorage/drivers/fs`](https://unstorage.unjs.io/drivers/fs).
::

### `cwd`

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

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

This resolves `content` against the current module's directory, ensuring consistent behavior 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:

```ts [prefixed.ts]
const blog = comarkContent('blog', {
  source: fs('./content/blog', { prefix: '/blog' }),
})

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

The prefix does **not** affect [`meta.key`](https://content.comark.dev/guide/files-and-paths#internal-identifiers) (which stays `<instance name>/<file key>`, here `blog/2026/hello.md`), only the public `path` returned by [`content.get()`](https://content.comark.dev/reference/content/get) and [`content.list()`](https://content.comark.dev/reference/content/list).

### `exclude`

Picomatch globs matched against each entry's key. Excluded files are dropped **before** parsing, so they never enter the manifest.

```ts [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`](https://content.comark.dev/plugins/built-in/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.

```ts [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`](#options-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](#watch) for how to consume the events it produces.

## Watch

[`content.watch()`](https://content.comark.dev/reference/content/watch) starts a file watcher on the folder, keeps the index and cache in sync as files change, and returns a stop function. [Watch content changes](https://content.comark.dev/guide/watch) shows where to call it in each framework. React to changes through the `watch:file:update` and `watch:file:remove` hooks:

```ts [watch.ts]
import { content } from './content'

content.hooks.hook('watch:file:update', (source, key, file) => {
  console.log('update', source, key) // 'default' (instance name), 'posts/hello.md' (file key)
  // `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()
```

::tip{to="https://content.comark.dev/reference/content/watch"}
See watch API reference for the full event model and rename semantics.
::


## Sitemap

See the full [sitemap](https://content.comark.dev/sitemap.md) for all pages.
