[Skip to content](#main-content)

[![Jx Suite](/favicon.svg)
Suite](/)Search⌘KESC

Jump to

[Install Jx Studio](/docs/start/install)[Build your first project](/docs/start/first-project)[Document format](/docs/framework/concepts/documents)[Formula catalog](/docs/framework/reference/formulas)

↵
select↑↓
navigateesc
close

[Studio](/studio)[Framework](/framework)[Templates](/templates)[Compare](/compare)[Docs](/docs/start/first-project)[GitHub](https://github.com/jxsuite/jx)[Download](/download)

✕

[Studio](/studio)[Framework](/framework)[Templates](/templates)[Compare](/compare)[Docs](/docs/start/first-project)[GitHub](https://github.com/jxsuite/jx)

Color scheme&#xA;

[Download](/download)

☰

Docs menu

Start here[Overview](/docs/start/)Get started[Install Jx Studio](/docs/start/install/)[Your first project](/docs/start/first-project/)[A tour of Jx Studio](/docs/start/studio-tour/)Tutorials[Tutorial: interactive component](/docs/start/first-component/)[Tutorial: blog with collections](/docs/start/first-collection/)[Video tutorials](/docs/start/videos/)Coming from another tool[Coming from WordPress](/docs/start/coming-from-wordpress/)[Coming from Webflow or Framer](/docs/start/coming-from-webflow/)[Coming from Wix or Squarespace](/docs/start/coming-from-wix/)[Coming from Lovable or Builder.io](/docs/start/coming-from-ai-builders/)Studio[Overview](/docs/studio/)[The desktop app](/docs/studio/desktop/)The workspace[Overview](/docs/studio/interface/)[Welcome screen](/docs/studio/interface/welcome-screen/)[Editors and views](/docs/studio/interface/modes/)[The canvas](/docs/studio/interface/canvas/)[Documents and panes](/docs/studio/interface/tabs/)[Quick Access](/docs/studio/interface/quick-access/)[Keyboard shortcuts](/docs/studio/interface/shortcuts/)[Preferences](/docs/studio/interface/preferences/)[Languages](/docs/studio/interface/languages/)[Problems and progress](/docs/studio/interface/problems-and-progress/)[Commands](/docs/studio/interface/commands/)Projects[Overview](/docs/studio/projects/)[Create a project](/docs/studio/projects/create/)[Pages, layouts, components](/docs/studio/projects/pages-layouts-components/)[The Content workspace](/docs/studio/projects/browse/)[Starter templates](/docs/studio/projects/starters/)[Media](/docs/studio/projects/media/)[Content types](/docs/studio/projects/content-types/)[Project settings](/docs/studio/projects/settings/)[Dependencies and imports](/docs/studio/projects/dependencies/)Edit mode[Overview](/docs/studio/editing/)[Writing and formatting](/docs/studio/editing/writing/)[Slash commands](/docs/studio/editing/slash-commands/)[Frontmatter and metadata](/docs/studio/editing/frontmatter/)[Grid mode](/docs/studio/editing/grid/)Design mode[Overview](/docs/studio/design/)[Outline](/docs/studio/design/layers/)[Insert](/docs/studio/design/elements/)[Content tab](/docs/studio/design/properties/)[Style inspector](/docs/studio/design/style-inspector/)[Breakpoints](/docs/studio/design/breakpoints/)[States and selectors](/docs/studio/design/states-and-selectors/)[Design tokens](/docs/studio/design/tokens/)[Project Styles](/docs/studio/design/stylebook/)[Working with components](/docs/studio/design/components/)[Repeaters](/docs/studio/design/repeaters/)Logic[Overview](/docs/studio/logic/)[Formulas and expressions](/docs/studio/logic/formulas/)[Formula workspace](/docs/studio/logic/formula-workspace/)[Statements](/docs/studio/logic/statements/)[Logic tab](/docs/studio/logic/events/)[Data](/docs/studio/logic/data/)[Data sources](/docs/studio/logic/data-sources/)[Code editing](/docs/studio/logic/code/)Databases[Overview](/docs/studio/data/)[Connections](/docs/studio/data/connections/)[Data tables](/docs/studio/data/tables/)[Data grid](/docs/studio/data/grid/)[Auth and secrets](/docs/studio/data/auth-and-secrets/)AI assistant[Overview](/docs/studio/ai/)[The AI assistant](/docs/studio/ai/chat/)[Document assistant](/docs/studio/ai/document-assistant/)Publish[Overview](/docs/studio/publish/)[Source control](/docs/studio/publish/source-control/)[GitHub](/docs/studio/publish/github/)[Cloudflare Pages](/docs/studio/publish/cloudflare/)[Other hosts](/docs/studio/publish/other-hosts/)[Redirects](/docs/studio/publish/redirects/)[Real-time collaboration](/docs/studio/publish/collaboration/)Framework[Overview](/docs/framework/)Concepts[Documents](/docs/framework/concepts/documents/)[Elements](/docs/framework/concepts/elements/)[Components](/docs/framework/concepts/components/)[Props and scope](/docs/framework/concepts/props-and-scope/)[State](/docs/framework/concepts/state/)[Reactivity](/docs/framework/concepts/reactivity/)[References](/docs/framework/concepts/references/)[Expressions](/docs/framework/concepts/expressions/)[Statements](/docs/framework/concepts/statements/)[Functions and sidecars](/docs/framework/concepts/functions/)[Styling](/docs/framework/concepts/styling/)[Color schemes](/docs/framework/concepts/color-schemes/)[Popovers and overlays](/docs/framework/concepts/overlays/)[Accessibility](/docs/framework/concepts/accessibility/)[Lists and iteration](/docs/framework/concepts/lists/)[Data prototypes](/docs/framework/concepts/data-prototypes/)[Timing](/docs/framework/concepts/timing/)[Dynamic switching](/docs/framework/concepts/switching/)[Security](/docs/framework/concepts/security/)Site architecture[Overview](/docs/framework/site/)[project.json](/docs/framework/site/project-json/)[Routing](/docs/framework/site/routing/)[Layouts](/docs/framework/site/layouts/)[Content collections](/docs/framework/site/content-collections/)[Jx Markdown](/docs/framework/site/jx-markdown/)[Obsidian vault as content](/docs/framework/site/obsidian-vault/)[Relationships](/docs/framework/site/relationships/)[SEO and metadata](/docs/framework/site/seo/)[Locales and languages](/docs/framework/site/i18n/)[Images](/docs/framework/site/images/)[Redirects](/docs/framework/site/redirects/)[Feeds](/docs/framework/site/feeds/)[Site search](/docs/framework/site/search/)[Build output and adapters](/docs/framework/site/deployment/)How compilation works[Overview](/docs/framework/build/)[The dev server](/docs/framework/build/dev-server/)[CLI commands](/docs/framework/build/cli/)Working with agents[Overview](/docs/framework/agents/)[Authoring rules for agents](/docs/framework/agents/authoring-rules/)[Machine-readable docs](/docs/framework/agents/machine-readable/)Reference[Overview](/docs/framework/reference/)[Formula catalog](/docs/framework/reference/formulas/)[Operator reference](/docs/framework/reference/operators/)Extending[Overview](/docs/extending/)Extensions[Extension anatomy](/docs/extending/extensions/anatomy/)[Schema composition](/docs/extending/extensions/schema-composition/)[Custom classes](/docs/extending/extensions/classes/)[Custom formats](/docs/extending/extensions/formats/)[Capability methods](/docs/extending/extensions/capabilities/)[Project sections](/docs/extending/extensions/project-sections/)[Server mounts](/docs/extending/extensions/server/)[Connectors](/docs/extending/extensions/connectors/)[Search indexes](/docs/extending/extensions/search/)[Security and secrets](/docs/extending/extensions/security/)[Tutorial: a TOML format](/docs/extending/extensions/tutorial-toml-format/)[Tutorial: a guestbook](/docs/extending/extensions/tutorial-guestbook/)[First-party extensions](/docs/extending/extensions/first-party/)Embedding[Overview](/docs/extending/embedding/)[Hosting the Studio assets](/docs/extending/embedding/hosting/)[Writing a platform adapter](/docs/extending/embedding/platform-adapter/)[The backend protocol](/docs/extending/embedding/backend-protocol/)[Dev server internals](/docs/extending/embedding/dev-server/)[Embedding the runtime in your app](/docs/extending/embedding/runtime-host/)[Writing assistant tools](/docs/extending/embedding/assistant-harness/)UI kit[The Jx UI kit](/docs/extending/ui-kit/)Reference[Protocol routes](/docs/extending/reference/studio-routes/)[Implementation status](/docs/extending/reference/implementation-status/)[Spec changelog](/docs/extending/reference/spec-changelog/)[Standards alignment](/docs/extending/reference/standards/)Contributing[Working in the monorepo](/docs/extending/contributing/monorepo/)[Contributing to these docs](/docs/extending/contributing/docs/)[AI assistant evals](/docs/extending/contributing/ai-evals/)

# Content collections

> **Studio writes this format for you.** The [content-type builder](/docs/studio/projects/content-types) edits the `content` section visually, and entries show up in [the Content workspace](/docs/studio/projects/browse), where they are written inside their template with their fields as a schema-driven form.
>
> It also enforces the collection's format where you create files: **New File…** in a collection's folder is **New Entry**, and in a subfolder of it the format picker is locked to the collection's extension. See [Creating a file](/docs/studio/interface#creating-a-file).

Content collections are the data layer for content-driven sites: they turn folders of Markdown, JSON, or CSV files into typed, queryable data with JSON Schema validation. Each collection is a **content type**, declared in the `content` section of `project.json`.

Formats other than JSON come from extension packages, so a site using Markdown or CSV content enables the parser in `project.json`: `"extensions": ["@jxsuite/parser"]`.

## Defining a content type

```json&#x20;shiki
```

Each definition takes these keys, and only `source` is usually needed:

- **`source`**: a directory of entry files, a single file containing many entries (one CSV or JSON file), or an `https://` URL. jxsuite.com's `docs` type points outside the project entirely (`"source": "../../docs"`) to publish this documentation from the repo.
- **`format`**: the name of a format class provided by an enabled extension (`"Markdown"`, `"Csv"`); `"json"` is the only built-in. When omitted, the format is derived from the source file's extension; directory and remote sources require an explicit `format`, and remote sources need a format that supports remote loading (such as `Csv`).
- **`$elements`**: components available inside entries as directives, e.g. jxsuite.com registers `doc-note` and `doc-tip` for these docs. See [Jx Markdown](/docs/framework/site/jx-markdown).
- **`schema`**: a JSON Schema every entry's data must satisfy.
- **`exclude`** and **`where`**: which files are entries at all. See [Choosing which files are entries](#choosing-which-files-are-entries).
- **`idField`**, **`route`** and **`indexRoute`**: what each entry is called and where it lives. See [Ids and routes](#ids-and-routes).
- **`links`**: how a relative link to an entry with no page is reported. See [Links between entries](#links-between-entries).
- **`alerts`**: which component renders a `> [!NOTE]` callout, or `false` for none. See [Callouts](/docs/framework/site/jx-markdown#callouts).

An option name that is one typo away from a real one (`excludes`, `wher`, `idfield`) is a build error that names the one you meant, because these options keep things out of a site and a filter that is silently ignored publishes what it was written to hide. A key that resembles no option is ignored, with a warning.

## Entry ids

Every entry has an `id`, derived from the filesystem:

- **Flat directories**: the filename without extension: `content/blog/hello-world.md` → `hello-world`.
- **Nested directories**: a path-based id with `/` separators: `docs/framework/site/routing.md` → `framework/site/routing`, with a trailing `/index` or `/README` stripped, so `docs/studio/README.md` is `studio`. A `README.md` directly in the source folder keeps the id `README`. Path-based ids pair naturally with `[...param]` catch-all routes. See [Routing](/docs/framework/site/routing).
- **JSON files**: an array file yields one entry per item (each item's `id` field, or an indexed fallback); an object file is a single entry named after the file.

Ids keep the case and spaces of the file name exactly as written: `Linux/Swap Configuration.md` is the id `Linux/Swap Configuration`. They are stable for as long as the file keeps its name and place. They are also not pretty URLs, which is why a collection that publishes files named for people sets an `idField` or a `route` (below) rather than renaming files.

## Choosing which files are entries

By default every file under the source is an entry. Two options narrow that, and both are plain data: a `project.json` is read by Studio, the dev server and CI, so a filter that ran code would make each of them run your project's text.

```json&#x20;shiki
```

**`exclude`** is a list of glob patterns, relative to the source folder, for files that are never read.

| Pattern              | Excludes                                                                             |
| -------------------- | ------------------------------------------------------------------------------------ |
| `STYLE.md`           | that file at the source root only                                                    |
| `**/README.md`       | every `README.md`, at any depth                                                      |
| `internal/**`        | everything below `internal/`, however deep                                           |
| `drafts/`            | the same as `drafts/**`                                                              |
| `.*/**`              | every dot folder at the root, such as `.obsidian`                                    |
| `**/node_modules/**` | a `node_modules` folder anywhere                                                     |
| `{notes,scratch}/**` | either folder                                                                        |
| `internal`           | the folder `internal` (a bare name with no extension) as well as a file of that name |

`*` and `?` stop at a slash, `**` crosses folders, and a dotfile is an ordinary file, so a pattern that should skip `.obsidian` says so. A folder that a pattern excludes whole is never opened, which keeps a vault that sits next to a website's `node_modules` quick to build. Patterns cannot be negated (`!keep.md` is an error), cannot climb out of the source (`../x` is an error), and a pattern with an unclosed `{` or an invalid `[...]` class is an error that names the pattern. A segment may carry at most four `*` wildcards.

Excluded files are not served either. A directory source is also published at `/content/<type>` so that images and downloads beside the notes have a URL, and a type that declares `exclude`, `where` or `route` serves nothing that `exclude` names, nothing whose path has a dot segment (`.git`, `.env`, `.obsidian`) and no Markdown document. A note that refers to one of those gets a warning and the reference stays as written, so a published note cannot pull a private file into the site.

**`where`** keeps only the entries whose frontmatter passes. Every key is a frontmatter field, keys combine with AND, and a value is either a literal to equal or an operator object.

| Operator                          | Keeps an entry when the field                                      |
| --------------------------------- | ------------------------------------------------------------------ |
| `true`, `"review"`, `3`           | equals that value exactly (`"true"` is not `true`)                 |
| `{ "$ne": "draft" }`              | is anything else, or absent                                        |
| `{ "$in": ["review", "stable"] }` | is one of the values                                               |
| `{ "$nin": ["draft"] }`           | is none of them, or absent                                         |
| `{ "$exists": true }`             | is set (an empty YAML key counts as not set)                       |
| `{ "$gte": "2026-01-01" }`        | is at or after a date, number or string (`$gt`, `$lt`, `$lte` too) |

A plain value also matches an array that contains it, so `{ "tags": "frappe" }` keeps every entry tagged `frappe`. A dotted key such as `meta.public` reaches into nested data. A value that is an array, or an object with no `$` keys, is compared whole. There is no OR: a site that needs "either" declares two content types. An operator the filter does not know is a build error that names the key, and so is a malformed pattern. A filter that removes every entry, or an `exclude` that leaves no file at all, prints a warning, so a mistake in the filter does not publish an empty site without a word.

`where` is not the `filter` of a `ContentCollection`. `where` runs once, when the collection loads, and decides what exists; `filter` runs on a page and chooses among what exists, with the rule form `{ "field": "tags", "op": "contains", "value": "frappe" }` or the shorthand `{ "category": "Frappe" }`. The two differ on arrays on purpose: `where: { "tags": "frappe" }` matches an entry whose `tags` array contains `frappe`, while the shorthand `filter` compares with `==`, which an array never equals, so use the `contains` op to match inside an array on a page.

An entry that fails `where`, and a file that matches `exclude`, are gone for every consumer at once: they are not validated, so their missing frontmatter never warns, and they never appear in `ContentCollection` or `ContentEntry` results, `$paths` expansion, the sitemap, the [search index](/docs/framework/site/search), a [feed](/docs/framework/site/feeds), or a [relationship](/docs/framework/site/relationships). Files are read in sorted order (by name, folder by folder), so a collection loads in the same order on every machine. A symlinked folder is followed, as it always was, so a monorepo can mount shared documentation into a collection; a link back into a folder the walk is already inside is skipped. Symlinks in a source are trusted like any other file in it. A file that cannot be read, such as one with invalid YAML frontmatter, fails the build with its path, and `exclude` is how a folder of templates stays out.

## Ids and routes

Two more options say what an entry is called and where it lives.

**`idField`** takes a frontmatter field name and uses its value as the id: `"idField": "slug"` makes `slug: bench-operations` the id of that entry. An entry without the field keeps its path id and a warning says so, and so does one whose value could not be a folder name (`../../x`, or a backslash). Two entries with the same id are reported with both file names, because a lookup by id finds only the first. A folder holding both a `README.md` and an `index.md` gives the folder's id to the `index.md`, as it always had, and the README keeps `Folder/README`.

**`route`** is a template for the entry's URL (`indexRoute`, below, is its counterpart for a folder's README). It is the one place that URL is decided, and the pages `$paths` generates, the links between entries, the sitemap, the search index and feeds all read it.

```json&#x20;shiki
```

| Placeholder                    | Value                                                         |
| ------------------------------ | ------------------------------------------------------------- |
| `{id}`                         | the entry id                                                  |
| `{dir}`                        | the file's folder, relative to the source (empty at the root) |
| `{file}`                       | the file name without its extension                           |
| `{category}`, `{meta.section}` | a frontmatter field (dotted paths reach into nested data)     |

A transform follows a colon and they chain: `{category:slug}`, `{title:lower}`. `slug` lowercases, folds accents, reads `&` as "and" (so `Git & Dev Tools` becomes `git-and-dev-tools`), drops apostrophes, turns other punctuation into single hyphens, and keeps letters of every script. `lower`, `upper` and `raw` (no change) are the others. A value that cannot be turned into a path segment, such as a missing field, or one holding `.`, `..`, a backslash, `?`, `#` or `%` under the `raw` transform, leaves the entry without a route and a warning names the file; so does an entry whose route another entry already holds, and the first one wins. When more than three entries fail for the same reason (a placeholder naming a field no note has), one line gives the reason, the count and a few files. An entry with no route is left out of the collection, so the sitemap, the search index, a feed and every listing agree on the same set of pages. Whether the built URL ends in a slash follows `build.trailingSlash`, whatever the template ends with.

A folder's `README.md` or `index.md` stands for the folder, and `indexRoute` says where it goes:

```json&#x20;shiki
```

With that, `Frappe/README.md` is the section page `/kb/frappe/`, and `WordPress/Gravity Forms/README.md` is `/kb/wordpress/gravity-forms/`. Without `indexRoute`, a README is routed like any other entry.

With a `route`, a dynamic page needs no parameter name. The compiler hands the page's own URL pattern to the content type, so one `$paths` serves whichever page shape you choose:

```json&#x20;shiki
```

That page can be `pages/kb/[...path].json`, which serves every route under `/kb/`, or `pages/kb/[category]/[slug].json` plus `pages/kb/[category].json` for articles and section pages. A route that no dynamic page of the project produces (a `route` of `/kb/{id:slug}/` with only a `[category]/[slug]` page, so the entries at other depths have no page) is reported once, with a few of the routes, because links to those entries were rewritten to them. A `ContentEntry` with no `id` is bound to the entry whose route is the page's own URL. Name `param` or `field` in `$paths` and you get the older meaning instead: one parameter, valued by the id or that field.

Each entry also carries what the loader worked out: `_meta.path` is its file relative to the source, and, when there is a route, `_meta.route` and `_meta.url` are its path and its link target (`/kb/frappe/bench-operations/`). A listing page links to an entry with ${item.\_meta.url}.

## Links between entries

A document written for an editor links to its neighbours by file: `[Swap](../Linux/Swap%20Configuration.md)`. On the built site that has to be the neighbour's URL, so a content type with a `route` rewrites those links once the whole collection has loaded.

- The path is resolved from the entry's own folder, with `%20` and other escapes decoded and `..` followed, and it never leaves the source folder: a link that climbs out of it is not rewritten and is reported.
- A link to a folder (`../Frappe/`) goes to that folder's `README.md` or `index.md`.
- A `?query` is kept, and a `#fragment` becomes the heading id the page carries, so `Note.md#Heading%20Text` (Obsidian's spelling) and `#heading-text` both land on the heading. A `#Heading%20Text` link within the same page is fixed the same way.
- External links, `mailto:` and `tel:` links and absolute paths are left exactly as written.
- A link to a file that is not an entry, such as a PDF, is followed to the URL the collection publishes it at (`/content/<type>/files/report.pdf`), the same remap an image's `src` gets. A link to a folder with no README or index, and one to an attachment the type does not publish, is shown as plain text and reported.
- A link to an entry that is excluded, filtered out, has no route, or does not exist is shown as plain text, so the site never publishes a dead link, and the build names the file and the target. Names are compared in composed Unicode, so a link typed with a decomposed `é` finds a file saved with a composed one.

```text
Content links: "kb": "Frappe/Bench Operations.md" links to "Draft%20Recipe.md", which is not published (left out by where.status); it renders as plain text.
```

The `links` option sets how loud that is: `"warn"` (the default) prints each one, `"error"` fails the build with every broken link in one message, and `"ignore"` says nothing. The link is text in all three. A `warn` build names the first 25 and counts the rest, so a cause that sits outside the links (a route template naming a missing field leaves every entry unrouted) does not bury itself. Links are only rewritten when the type declares a `route`, so a collection without one behaves as it always did. [Obsidian vault as content](/docs/framework/site/obsidian-vault) walks through all of this on a real vault.

## Media beside your content

Entries reference images the way any markdown editor expects, relative to the file itself:

```markdown&#x20;shiki
```

Keep the images in the collection directory (`content/blog/images/diagram.png`), and the entry reads correctly in VS Code, Obsidian, GitHub, and on the built site alike. When the collection loads, that reference is remapped to the collection's own URL, `/content/blog/images/diagram.png`, which the build copies into `dist/` and the dev server serves straight from the source file. It works even when the source lives outside the project, which is how these docs ship their screenshots from `docs/images/`.

Only files an entry actually references are published; unreferenced siblings and the entry files themselves never reach `dist/`. A type that declares `exclude`, `where` or `route` also refuses what its own rules leave out (see [Choosing which files are entries](#choosing-which-files-are-entries)), whichever way a page refers to it.

Note

Remapping applies to element&#x20;
`src`
/
`poster`
&#x20;values and to frontmatter fields your schema declares as&#x20;
`"format": "uri-reference"`
&#x20;(that's also what gives Studio a&#x20;
[media picker](/docs/studio/projects/media)
&#x20;for the field). A path that doesn't resolve to a real file beside the entry is left exactly as written, with a build warning naming the entry, so root-relative paths into&#x20;
`public/`
&#x20;keep working unchanged.

## Querying with ContentCollection

Pages read a collection through a `state` entry with `$prototype: "ContentCollection"`:

```json&#x20;shiki
```

- **`filter`**: either a shorthand object (each key must equal its value, as above) or an array of rules: `{ "field": "tags", "op": "contains", "value": "intro" }`. Operators: `==`, `!=`, `>`, `<`, `>=`, `<=`, `contains`, `not contains`, `empty`, `not empty`. Use `"field": "id"` to match the entry id.
- **`sort`**: a rule or array of rules, `{ "field": "pubDate", "order": "desc" }`; `asc` is the default.
- **`limit`**: maximum number of entries.

The result is an array of entries, rendered with a [repeater](/docs/framework/concepts/lists):

```json&#x20;shiki
```

## Single entries with ContentEntry

`$prototype: "ContentEntry"` fetches one entry by id, usually bound to a route parameter:

```json&#x20;shiki
```

By default `id` matches the entry id; set `"field"` to match a data field instead (e.g. `"field": "sku"` to look up a product by SKU).

A resolved entry has this shape:

```json&#x20;shiki
```

Frontmatter and data fields live under `data` (${state.post.data.title}); for Markdown entries, `body` is the raw source and `$children` is the parsed Jx tree, rendered with "children": "${state.post.$children ?? \[]}".

Markdown headings in `$children` carry automatic anchor `id`s: the heading text lowercased, punctuation stripped, spaces hyphenated, with `-2`, `-3` suffixes deduplicating repeats in document order. The entry's table of contents (`_meta.toc`: `depth`, `text`, `id` per heading) uses the same ids, so TOC links, search results, and hand-written `#fragment` URLs all land on the rendered section.

Two details of that are worth knowing if you write in anything but English. **Letters outside ASCII are kept**, so a Japanese or Russian heading gets an anchor made of its own words rather than an empty one falling back to `section`. And an accented heading gets **one** anchor whichever way it was typed: `é` can be a single character or an `e` with a combining accent, they look identical and used to produce two different links, so the text is normalized before the id is built. Anchors for plain-ASCII headings are unchanged, so existing links still work.

`_meta.wordCount` and `_meta.readingTime` count **words as the language defines them**, not runs of text between spaces. A Japanese or Thai article has no spaces between its words and used to count as one word, and therefore as one minute to read however long it was. Reading time is word count at 200 words per minute; that rate is a single honest constant rather than a per-language table, so treat it as an estimate for prose in any script.

Fenced code blocks in `$children` arrive syntax-highlighted: recognized languages become token spans carrying `--shiki-light`/`--shiki-dark` color variables that follow the site's [color scheme](/docs/framework/concepts/color-schemes). See [Jx Markdown](/docs/framework/site/jx-markdown) for the language set.

## Schema validation

Every entry is validated against its content type's `schema` when collections load, at build time and on the dev server alike. Missing required fields and type mismatches are reported with the content type and entry id, so a bad frontmatter key fails loudly instead of rendering an empty spot. The same schema drives Studio's [frontmatter forms](/docs/studio/editing/frontmatter) and the content-type builder's field editor.

## Dates

Declare a date field with `format`, and the loader normalizes it so sorting and filtering work:

```json&#x20;shiki
```

| `format`      | Stored as                          |
| ------------- | ---------------------------------- |
| `"date"`      | `2025-03-04`                       |
| `"date-time"` | `2025-03-04T16:00:00Z`, always UTC |

Date-times are converted to UTC because otherwise they don't sort: `2025-03-04T01:00:00+02:00` looks *later* than `2025-03-04T00:00:00Z` as text and is actually two hours *earlier*. If you need the original offset back, as an events collection meaning "7pm local" would, it's kept at `_meta.rawDates`.

You can write a `Date` from YAML, a full RFC 3339 timestamp, or a bare `YYYY-MM-DD`. Anything else is left exactly as you wrote it and reported as a warning naming the entry and field:

```text
Content dates: "blog/my-post" field "pubDate" is "03/04/2025", which is not an
unambiguous date. Write it as YYYY-MM-DD — a form like "03/04/2025" means two
different days depending on who reads it, so it is left as authored rather than guessed.
```

Note

Without a&#x20;
`format`
, a date is just a string.&#x20;
`MarkdownCollection`
's default sort compares text, which works for&#x20;
`YYYY-MM-DD`
&#x20;and not for timestamps with offsets. Declare the field in a content type and the loader handles it.

## Relationships

A schema field can reference another content type:

```json&#x20;shiki
```

An entry then stores the target's id (`author: jane-doe` in frontmatter), and loading resolves it to the full entry, so templates read ${state.post.data.author.data.name}. Array fields whose `items` carry the `$ref` are to-many. Resolution rules, editing support, and modeling patterns are covered in [Relationships](/docs/framework/site/relationships).

## Upgrading

Three behaviors changed for a content type that declares none of the new options, because the old behavior lost data or contradicted itself. They are marked as a breaking change in the release notes.

- **Callouts are on.** A blockquote that begins `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` renders as a callout. To keep the old blockquote, set `"alerts": false` on the type.
- **A `README.md` in a folder has the folder's id.**`docs/sub/README.md` is the entry `docs/sub`, as `docs/sub/index.md` always was, so its URL under a path-based route moves from `/docs/sub/README/` to `/docs/sub/`. A folder holding both keeps `index.md` as the folder and the README keeps `docs/sub/README`. A README directly in the source keeps the id `README`. Set `idField` to pin ids to a frontmatter field.
- **${...} in Markdown text is text.** A fence that showed a template literal, a shell ${HOME} or a ${{ secrets.X }} used to be evaluated and lose the code. It is now left exactly as written, and so are image descriptions and callout titles. A link's `href` is still read as a template.

Files are also discovered in sorted order now, so the first of two entries that claim one id or route is the same on every machine. Symlinked folders are still followed.

## Related

- [Obsidian vault as content](/docs/framework/site/obsidian-vault): publishing a folder of notes as it is
- [Routing](/docs/framework/site/routing): generating one page per entry with `$paths`
- [Jx Markdown](/docs/framework/site/jx-markdown): the entry format for prose content
- [project.json](/docs/framework/site/project-json): where the `content` section lives

✕

Start here[Overview](/docs/start/)Get started[Install Jx Studio](/docs/start/install/)[Your first project](/docs/start/first-project/)[A tour of Jx Studio](/docs/start/studio-tour/)Tutorials[Tutorial: interactive component](/docs/start/first-component/)[Tutorial: blog with collections](/docs/start/first-collection/)[Video tutorials](/docs/start/videos/)Coming from another tool[Coming from WordPress](/docs/start/coming-from-wordpress/)[Coming from Webflow or Framer](/docs/start/coming-from-webflow/)[Coming from Wix or Squarespace](/docs/start/coming-from-wix/)[Coming from Lovable or Builder.io](/docs/start/coming-from-ai-builders/)Studio[Overview](/docs/studio/)[The desktop app](/docs/studio/desktop/)The workspace[Overview](/docs/studio/interface/)[Welcome screen](/docs/studio/interface/welcome-screen/)[Editors and views](/docs/studio/interface/modes/)[The canvas](/docs/studio/interface/canvas/)[Documents and panes](/docs/studio/interface/tabs/)[Quick Access](/docs/studio/interface/quick-access/)[Keyboard shortcuts](/docs/studio/interface/shortcuts/)[Preferences](/docs/studio/interface/preferences/)[Languages](/docs/studio/interface/languages/)[Problems and progress](/docs/studio/interface/problems-and-progress/)[Commands](/docs/studio/interface/commands/)Projects[Overview](/docs/studio/projects/)[Create a project](/docs/studio/projects/create/)[Pages, layouts, components](/docs/studio/projects/pages-layouts-components/)[The Content workspace](/docs/studio/projects/browse/)[Starter templates](/docs/studio/projects/starters/)[Media](/docs/studio/projects/media/)[Content types](/docs/studio/projects/content-types/)[Project settings](/docs/studio/projects/settings/)[Dependencies and imports](/docs/studio/projects/dependencies/)Edit mode[Overview](/docs/studio/editing/)[Writing and formatting](/docs/studio/editing/writing/)[Slash commands](/docs/studio/editing/slash-commands/)[Frontmatter and metadata](/docs/studio/editing/frontmatter/)[Grid mode](/docs/studio/editing/grid/)Design mode[Overview](/docs/studio/design/)[Outline](/docs/studio/design/layers/)[Insert](/docs/studio/design/elements/)[Content tab](/docs/studio/design/properties/)[Style inspector](/docs/studio/design/style-inspector/)[Breakpoints](/docs/studio/design/breakpoints/)[States and selectors](/docs/studio/design/states-and-selectors/)[Design tokens](/docs/studio/design/tokens/)[Project Styles](/docs/studio/design/stylebook/)[Working with components](/docs/studio/design/components/)[Repeaters](/docs/studio/design/repeaters/)Logic[Overview](/docs/studio/logic/)[Formulas and expressions](/docs/studio/logic/formulas/)[Formula workspace](/docs/studio/logic/formula-workspace/)[Statements](/docs/studio/logic/statements/)[Logic tab](/docs/studio/logic/events/)[Data](/docs/studio/logic/data/)[Data sources](/docs/studio/logic/data-sources/)[Code editing](/docs/studio/logic/code/)Databases[Overview](/docs/studio/data/)[Connections](/docs/studio/data/connections/)[Data tables](/docs/studio/data/tables/)[Data grid](/docs/studio/data/grid/)[Auth and secrets](/docs/studio/data/auth-and-secrets/)AI assistant[Overview](/docs/studio/ai/)[The AI assistant](/docs/studio/ai/chat/)[Document assistant](/docs/studio/ai/document-assistant/)Publish[Overview](/docs/studio/publish/)[Source control](/docs/studio/publish/source-control/)[GitHub](/docs/studio/publish/github/)[Cloudflare Pages](/docs/studio/publish/cloudflare/)[Other hosts](/docs/studio/publish/other-hosts/)[Redirects](/docs/studio/publish/redirects/)[Real-time collaboration](/docs/studio/publish/collaboration/)Framework[Overview](/docs/framework/)Concepts[Documents](/docs/framework/concepts/documents/)[Elements](/docs/framework/concepts/elements/)[Components](/docs/framework/concepts/components/)[Props and scope](/docs/framework/concepts/props-and-scope/)[State](/docs/framework/concepts/state/)[Reactivity](/docs/framework/concepts/reactivity/)[References](/docs/framework/concepts/references/)[Expressions](/docs/framework/concepts/expressions/)[Statements](/docs/framework/concepts/statements/)[Functions and sidecars](/docs/framework/concepts/functions/)[Styling](/docs/framework/concepts/styling/)[Color schemes](/docs/framework/concepts/color-schemes/)[Popovers and overlays](/docs/framework/concepts/overlays/)[Accessibility](/docs/framework/concepts/accessibility/)[Lists and iteration](/docs/framework/concepts/lists/)[Data prototypes](/docs/framework/concepts/data-prototypes/)[Timing](/docs/framework/concepts/timing/)[Dynamic switching](/docs/framework/concepts/switching/)[Security](/docs/framework/concepts/security/)Site architecture[Overview](/docs/framework/site/)[project.json](/docs/framework/site/project-json/)[Routing](/docs/framework/site/routing/)[Layouts](/docs/framework/site/layouts/)[Content collections](/docs/framework/site/content-collections/)[Jx Markdown](/docs/framework/site/jx-markdown/)[Obsidian vault as content](/docs/framework/site/obsidian-vault/)[Relationships](/docs/framework/site/relationships/)[SEO and metadata](/docs/framework/site/seo/)[Locales and languages](/docs/framework/site/i18n/)[Images](/docs/framework/site/images/)[Redirects](/docs/framework/site/redirects/)[Feeds](/docs/framework/site/feeds/)[Site search](/docs/framework/site/search/)[Build output and adapters](/docs/framework/site/deployment/)How compilation works[Overview](/docs/framework/build/)[The dev server](/docs/framework/build/dev-server/)[CLI commands](/docs/framework/build/cli/)Working with agents[Overview](/docs/framework/agents/)[Authoring rules for agents](/docs/framework/agents/authoring-rules/)[Machine-readable docs](/docs/framework/agents/machine-readable/)Reference[Overview](/docs/framework/reference/)[Formula catalog](/docs/framework/reference/formulas/)[Operator reference](/docs/framework/reference/operators/)Extending[Overview](/docs/extending/)Extensions[Extension anatomy](/docs/extending/extensions/anatomy/)[Schema composition](/docs/extending/extensions/schema-composition/)[Custom classes](/docs/extending/extensions/classes/)[Custom formats](/docs/extending/extensions/formats/)[Capability methods](/docs/extending/extensions/capabilities/)[Project sections](/docs/extending/extensions/project-sections/)[Server mounts](/docs/extending/extensions/server/)[Connectors](/docs/extending/extensions/connectors/)[Search indexes](/docs/extending/extensions/search/)[Security and secrets](/docs/extending/extensions/security/)[Tutorial: a TOML format](/docs/extending/extensions/tutorial-toml-format/)[Tutorial: a guestbook](/docs/extending/extensions/tutorial-guestbook/)[First-party extensions](/docs/extending/extensions/first-party/)Embedding[Overview](/docs/extending/embedding/)[Hosting the Studio assets](/docs/extending/embedding/hosting/)[Writing a platform adapter](/docs/extending/embedding/platform-adapter/)[The backend protocol](/docs/extending/embedding/backend-protocol/)[Dev server internals](/docs/extending/embedding/dev-server/)[Embedding the runtime in your app](/docs/extending/embedding/runtime-host/)[Writing assistant tools](/docs/extending/embedding/assistant-harness/)UI kit[The Jx UI kit](/docs/extending/ui-kit/)Reference[Protocol routes](/docs/extending/reference/studio-routes/)[Implementation status](/docs/extending/reference/implementation-status/)[Spec changelog](/docs/extending/reference/spec-changelog/)[Standards alignment](/docs/extending/reference/standards/)Contributing[Working in the monorepo](/docs/extending/contributing/monorepo/)[Contributing to these docs](/docs/extending/contributing/docs/)[AI assistant evals](/docs/extending/contributing/ai-evals/)

[![Jx Suite](/favicon.svg)
Suite](/)The visual IDE for the web. Build any website, save it as plain files, own it forever. MIT licensed.

Product

[Studio](/studio)[Download](/download)[Templates](/templates)[Features](/features)[Compare](/compare)[Jx Cloud (soon)](/cloud)

Developers

[Docs](/docs/start/first-project)[Framework](/framework)[Get Studio](/docs/start/install)[Spec](/docs/framework)[GitHub](https://github.com/jxsuite/jx)

Project

[MIT License](https://github.com/jxsuite/jx/blob/main/LICENSE)[Releases](https://github.com/jxsuite/jx/releases/latest)[Privacy](/privacy)[Terms](/terms)[Support](/support)

Built with Jx Suite. © 2026 Avunu LLC. Open source under the MIT License.
