---
title: "Files and paths"
description: "How the files in your content folder become the paths you read."
canonical_url: "https://content.comark.dev/guide/files-and-paths"
---
# Files and paths

> How the files in your content folder become the paths you read.

A Content instance reads one folder and gives every Markdown file inside it a **public path**. You pass that path to [`content.get()`](https://content.comark.dev/guide/read-and-list), and it's the `path` you find on every page in [`content.list()`](https://content.comark.dev/guide/read-and-list#list-pages) and [`content.navigation()`](https://content.comark.dev/guide/navigation).

The rule is short: take the file's location inside the source folder, drop the extension, and put a `/` in front.

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

| File                    | Public path   |
| ----------------------- | ------------- |
| `content/index.md`      | `/`           |
| `content/about.md`      | `/about`      |
| `content/blog/hello.md` | `/blog/hello` |
| `content/blog/index.md` | `/blog`       |

An `index.md` stands for its folder, so the root `index.md` is `/` and `blog/index.md` is `/blog`.

## Nested folders

One source includes every subfolder, at any depth. You don't need a second instance or any configuration to add `content/blog/` or `content/docs/guides/`. Folders only shape the paths and the [navigation tree](https://content.comark.dev/guide/navigation).

## Numeric prefixes for ordering

You can prefix file and folder names with a number and a dot to control their order in [`navigation()`](https://content.comark.dev/guide/navigation). The prefix is removed from the public path:

| File                                          | Public path                     |
| --------------------------------------------- | ------------------------------- |
| `content/1.getting-started/2.installation.md` | `/getting-started/installation` |
| `content/2.guide/1.files-and-paths.md`        | `/guide/files-and-paths`        |

The prefix affects ordering only. `10.advanced/` sorts after `2.guide/`, not between `1.` and `2.`, because ordering uses natural number comparison. Version-like segments such as `1.2` or `2.x` are kept as they are.

## Lowercase paths

Public paths are lowercased when the instance indexes them, and lookups are case-insensitive. `content/About.md` is served at `/about`, and `get('/About')` finds it.

Two files that differ only by case, such as `About.md` and `about.md`, map to the same path. The one loaded last wins, and load order can vary between filesystems. Avoid case-only duplicates.

## Add a URL prefix

Sometimes a folder holds one section of your site and you want its paths to say so. Pass `prefix` to the source:

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

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

Without the prefix, `content/blog/hello.md` in that instance would be `/hello`. With it, the path is `/blog/hello`, which is useful when you [combine several sources](https://content.comark.dev/advanced/hub) that would otherwise compete for the same paths.

## Files that aren't Markdown

Only `.md` files become pages by default. Other files are still tracked:

- **Images, videos, and downloads** are indexed as *media* entries at their path with the extension kept, such as `/images/logo.svg`. They have no body to parse. To serve their bytes, add the [`media` plugin](https://content.comark.dev/plugins/built-in/media).
- **JSON and YAML** files become data-only pages when you add the [`json`](https://content.comark.dev/plugins/built-in/json) or [`yaml`](https://content.comark.dev/plugins/built-in/yaml) plugin. The `yaml` plugin is also what reads [`.navigation.yml`](https://content.comark.dev/guide/navigation#directory-metadata-with-navigationyml) files.

To keep some files out of the index altogether, such as drafts, pass [`exclude`](https://content.comark.dev/sources/filesystem#options-exclude) globs to the source.

## Internal identifiers

Everything above is what you need to read and list pages. This section is for when you inspect `page.meta` or read logs and see identifiers that aren't public paths.

Every page carries a `meta` object with, among other fields:

| Field            | Example for `content/blog/hello.md` | Meaning                                                                                     |
| ---------------- | ----------------------------------- | ------------------------------------------------------------------------------------------- |
| `meta.key`       | `default/blog/hello.md`             | The instance name, a slash, and the file's location inside the source                       |
| `meta.source`    | `default`                           | The instance name                                                                           |
| `meta.stem`      | `blog/hello`                        | The file's location inside the source, without the extension and with numeric prefixes kept |
| `meta.extension` | `.md`                               | The file extension                                                                          |

`default` is the **instance name**. Every instance has one; when you call `comarkContent({ source })` without a name, it's `default`. So `default/blog/hello.md` is an identifier, not a folder: the file on disk is `content/blog/hello.md`.

Naming an instance changes these identifiers but never its public paths:

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

const post = await blog.get('/hello') // reads ./content/blog/hello.md
post?.path // '/hello'
post?.meta.key // 'blog/hello.md'
```

Add `prefix: '/blog'` to that source and `post.path` becomes `/blog/hello`, while `meta.key` stays `blog/hello.md`. Three things are at play, and they're independent:

- The **folder** passed to `fs()` decides which files exist.
- The **instance name** appears in `meta.key`, cache keys, and [generated type names](https://content.comark.dev/guide/typescript). You only need to set it when you run [more than one instance](https://content.comark.dev/advanced/hub).
- The source **`prefix`** changes public paths and nothing else.

`meta.key` is also accepted by `get()`, so `content.get('default/blog/hello.md')` and `content.get('/blog/hello')` read the same file. Prefer the public path in application code; the key form is used by [hubs](https://content.comark.dev/advanced/hub#name-prefixed-keys-skip-the-search) and in [watch events](https://content.comark.dev/guide/watch).


## Sitemap

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