> For the complete documentation index, see [llms.txt](https://ascribed-dev.com/docs/llms.txt). This documentation describes Ascribe 0.4.4. Anything marked in main, not yet released isn't in that release yet.

# `ascribe.toml` reference

> Reference · The content model: what pages are and hold, how content varies, where pages are published, what they draw from, and the checks.

`ascribe.toml` is a project's [content model](https://ascribed-dev.com/docs/glossary.md#content-model): its schema (SPEC §7). It sits at the project root and declares the content types and their frontmatter, the [dimensions](https://ascribed-dev.com/docs/glossary.md#dimension) content varies along, phrases, the glossary, project widgets, how the site output fits Astro, and the builds. The editor, `ascribe check`, and `ascribe build` all read it, so they can't disagree about what's valid.

A file containing only `spec = "0.1"` is valid; every section has a default ([§24](https://ascribed-dev.com/docs/content-model/checks.md#24-defaults-what-an-absent-section-means)). Example files:

- [`examples/content-models/minimal.toml`](https://github.com/ascribed-dev/ascribe/blob/main/examples/content-models/minimal.toml): the smallest valid model.
- [`examples/quill/ascribe.toml`](https://github.com/ascribed-dev/ascribe/blob/main/examples/quill/ascribe.toml): a small, complete project.
- [`examples/content-models/full.toml`](https://github.com/ascribed-dev/ascribe/blob/main/examples/content-models/full.toml): every section and key.

Every problem the loader reports is in the diagnostics reference, on [Diagnostics: the content model](https://ascribed-dev.com/docs/checks/diagnostics/content-model.md) and [Diagnostics: the content model's builds and site](https://ascribed-dev.com/docs/checks/diagnostics/content-model-builds.md), with its fix. References to "SPEC" are to the [Ascribe specification](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md). The rules a loader enforces, stated for someone writing one, are in the [`ascribe.toml` format contract](https://ascribed-dev.com/docs/contracts/content-model.md), with the same section numbers and key tables.

In VS Code, [Project Settings](https://ascribed-dev.com/docs/editor/settings.md#project-settings) shows a project's settings beside its `ascribe.toml`, each with its value, its default, and its line, and changes a value the file sets in place.

## Contents

The sections are grouped by what they're for, a page each.

What pages are, on this page:

1. [Conventions](https://ascribed-dev.com/docs/content-model.md#1-conventions)
2. [Top level: `spec`](https://ascribed-dev.com/docs/content-model.md#2-top-level-spec), and [taking from another project](https://ascribed-dev.com/docs/content-model.md#21-taking-from-another-project)
3. [`[project]`](https://ascribed-dev.com/docs/content-model.md#3-project)
4. [`[types.<name>]` and `[fragments]`](https://ascribed-dev.com/docs/content-model.md#4-typesname-and-fragments)
5. [Field and attribute types](https://ascribed-dev.com/docs/content-model.md#5-field-and-attribute-types)

[What pages hold](https://ascribed-dev.com/docs/content-model/content.md):

6. [`[notes.<type>]`](https://ascribed-dev.com/docs/content-model/content.md#6-notestype)
7. [`[phrases]`](https://ascribed-dev.com/docs/content-model/content.md#7-phrases)
8. [`[glossary]`](https://ascribed-dev.com/docs/content-model/content.md#8-glossary)
9. [`[images]`](https://ascribed-dev.com/docs/content-model/content.md#9-images)
10. [`[widgets.<name>]`](https://ascribed-dev.com/docs/content-model/content.md#10-widgetsname)

[How content varies](https://ascribed-dev.com/docs/content-model/variation.md):

11. [`[dimensions.<name>]`](https://ascribed-dev.com/docs/content-model/variation.md#11-dimensionsname)
12. [`[versions]`](https://ascribed-dev.com/docs/content-model/variation.md#12-versions), and [`[releases.<target>]`](https://ascribed-dev.com/docs/content-model/variation.md#121-releasestarget)
13. [`[lifecycle.<state>]`](https://ascribed-dev.com/docs/content-model/variation.md#13-lifecyclestate)
14. [`[features.<key>]`](https://ascribed-dev.com/docs/content-model/variation.md#14-featureskey)

[Where pages are published](https://ascribed-dev.com/docs/content-model/output.md):

15. [`[consumer]`](https://ascribed-dev.com/docs/content-model/output.md#15-consumer)
16. [`[builds.<name>]`](https://ascribed-dev.com/docs/content-model/output.md#16-buildsname)
17. [`[listing]`](https://ascribed-dev.com/docs/content-model/output.md#17-listing)
18. [`[crawlers]`](https://ascribed-dev.com/docs/content-model/output.md#18-crawlers)
19. [`[permalinks]`](https://ascribed-dev.com/docs/content-model/output.md#19-permalinks)

[What pages draw from](https://ascribed-dev.com/docs/content-model/sources.md):

20. [`[sources.<name>]`](https://ascribed-dev.com/docs/content-model/sources.md#20-sourcesname)
21. [`[apis.<name>]`](https://ascribed-dev.com/docs/content-model/sources.md#21-apisname)

[Checks and the editor](https://ascribed-dev.com/docs/content-model/checks.md):

22. [`[checks]`](https://ascribed-dev.com/docs/content-model/checks.md#22-checks)
23. [`[editor]`](https://ascribed-dev.com/docs/content-model/checks.md#23-editor)
24. [Defaults: what an absent section means](https://ascribed-dev.com/docs/content-model/checks.md#24-defaults-what-an-absent-section-means)
25. [Validation](https://ascribed-dev.com/docs/content-model/checks.md#25-validation)

---

## 1\. Conventions

### 1\.1 The file

- The content model is a TOML 1.0 file named `ascribe.toml`. The directory that contains it is the **project root**. Every path in the file is relative to the project root unless a key says otherwise.

- The file is UTF-8.

- TOML's equivalent spellings are all accepted: a table can be written as a `[header]` section, as an inline table (`key = { … }`), or with dotted keys. This reference shows the most readable form for each section. For example, these are the same:

  ```toml
  [dimensions.pm]
  values = ["npm", "pnpm", "yarn"]

  # same as
  dimensions.pm.values = ["npm", "pnpm", "yarn"]
  ```

- **Keys are kebab-case** (`content-root`, `trailing-slash`), matching Ascribe's attribute keys. TOML allows hyphens in bare keys, so they need no quotes.

- **Unknown keys are errors**, everywhere, with a did-you-mean suggestion. A misspelled key is otherwise a silently ignored setting. The exceptions are the tables whose keys are names the project chooses (`[phrases]`, `[types]`, `[dimensions]`, and so on); their keys are validated as names instead.

- **Declaration order is kept where it matters.** Attribute keys keep the order they're declared in (`[images.attributes]`, `[widgets.<name>.attributes]`), because canonical form writes attributes in declared order ([SPEC §8.3](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#83-canonical-form)). The `[dimensions]` tables keep theirs too: it's the canonical order of `@variant` attributes, and decides which dimension a tab group syncs on (the [element contract](https://github.com/ascribed-dev/ascribe/blob/main/packages/elements/CONTRACT.md), §3). Order is kept elsewhere for stable output (for example, the order of dimension values in a tab switcher is the order of `values`). Nothing else in the model depends on order.

### 1\.2 Names

Several kinds of name appear in the file. Each has a grammar from SPEC Appendix A or from this reference:

| Name | Grammar | Rule | Used for |
| --- | --- | --- | --- |
| name-word | SPEC A `name-word` | A letter, then letters, digits, `_`, or `-` | Dimension values, lifecycle states, feature keys, frontmatter field names |
| key | SPEC A `key` | A lowercase letter, then lowercase letters, digits, or `-` | Dimension names, phrase keys, note types, attribute keys, content type names, source names |
| widget name | SPEC A `widget-name` | Lowercase words of letters and digits joined by single hyphens, with at least one hyphen; starting with a letter | Widget names |
| build name | This reference | A letter, then letters, digits, `_`, `-`, or `.` | Build names |

Dimension names follow the stricter `key` rule, not just `name-word`, because they're also written as attribute keys in `@variant {deployment=cloud}` ([SPEC §3.3](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#33-attributes), §4.3).

Build names allow `.` so that names like `self-managed-3.3` work. A name containing `.` must be quoted in a TOML header: `[builds."self-managed-3.3"]`.

Names are case-sensitive and spelled exactly as declared ([SPEC §4.4](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#44-available)).

### 1\.3 Paths and patterns

- Paths use `/` as the separator on every platform.

- Filesystem paths in `[project]` and `[sources]` are relative to the project root. They can't be absolute, and they can use `..`.

- A path in `[glossary] pages` (§8) is written like a link destination in a document ([SPEC §5.2](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#52-links)), relative to the content root.

- **Patterns** (globs) are matched against a source file's path relative to the content root, including its extension, with `/` separators. They are case-sensitive. The syntax:

  | Syntax | Matches |
  | --- | --- |
  | `*` | Any run of characters within one path segment, not including `/` |
  | `**` | Any number of whole segments, including none. It's only ever a whole segment (`a/**/b`, `a/**`, `**/b`) |
  | `?` | Any one character except `/` |
  | `{a,b}` | Either alternative. Alternatives don't nest |
  | `\` | Escapes the next character |

  Every other character matches itself. A pattern doesn't start with `/`, and has no `..` segment. Patterns only ever apply to Markdown source files (`*.md`); other files under the content root are assets. The exception is a source's `include` and `ignore` (§20), which are matched against any file's path relative to the source's folder.

### 1\.4 How this reference describes keys

Each section has a table of keys with these columns:

- **Type:** the TOML type. "Field type", "attribute type", and "[availability](https://ascribed-dev.com/docs/glossary.md#availability) spec" are strings with the grammars in §5 and [SPEC §4.4](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#44-available).
- **Default:** the value when the key is absent, or **required**.
- **Description.**

---

## 2\. Top level: `spec`

```toml
spec = "0.1"
```

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `spec` | string | **required** | The version of the Ascribe specification this project targets (SPEC §11). It MUST be quoted: `spec = 0.1` is a TOML float and is an error. A processor accepts only the spec versions it implements, compared as exact strings; this reference defines `"0.1"`. |

The only other top-level keys are `[from]` (§2.1) and the tables in §3–§23, `[releases]` (§12.1) and `[[intended]]` (§22.1) among them. Anything else is an unknown key.

### 2\.1 Taking from another project

Two projects beside each other, such as a product's docs and its landing site, often share facts: the releases, the site's address, a phrase or two. One of them writes each, and the other takes it with `[from]`, so it's written once, and a change to it reaches both. Ascribe's own landing site takes its releases, its address, and the source its examples come from from the docs' `ascribe.toml`.

```toml
[from."../../docs/ascribe.toml"]
take = ["releases", "dimensions.pm", "phrases.version", "consumer.site"]
```

The key after `from` is the other file's path, relative to the folder `ascribe.toml` is in. A project can take from more than one file, each a table of its own.

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `take` | array of strings | **required** | What's taken from the file, each a table or a key as a dotted path: `releases` takes the whole table, `consumer.site` one key of `[consumer]`. It's read as if written here, and can't be written here too. |

What's taken is read from the other file as if written in this one, and every rule of this reference holds it there. A source's `path` is the one exception: it's relative to the folder of the file it's written in, so a taken source is the same folder from either project. The other file's own `[from]` isn't followed: what's taken is always written in the file named.

`spec`, `[project]`, and `[from]` itself can't be taken (`model-from-take`): they're what makes a project its own. Name each table or key once, and not a table and a key in it. Nothing taken can also be written here (`model-from-written`): to change a taken value for this project, remove it from `take` and write it here.

**Rules.** The file must be there and be TOML (`model-from-file`), inside the git repository the project is in, as a source's folder must, and what `take` names must be in it (`model-from-take`). With no repository, the editor and `ascribe mcp` read it only inside the folder they were opened on.

**Editing.** A taken key is changed in the file that writes it, wherever the edit starts:

- The editor's actions, Project Settings, and a rename write a taken key in the other file, at the same path, and say which file they changed; renaming a key `take` names, such as a phrase, renames it in `take` too. A rename of a phrase or a value the other file writes is made in that project's pages as well, so both projects stay as they were with the new name; when it can't be made there, as when that project has the new name already, the rename is refused, naming the file.
- `ascribe versions release` and `ascribe versions retire` run in a project that takes the releases are made in the project that writes them: its `ascribe.toml` and its pages' marks change, and the text says so. One release moves the lines of every project that takes them.
- `ascribe templates eject` and `ascribe vale eject` refuse a taken table, naming its file, since the files they write beside this `ascribe.toml` couldn't be named from there; run them in the other project, or stop taking the table and eject then.
- In the editor, the hover on a name in `take` says where it's written, Go to Definition opens it there, and a change to the file reaches the project at once.

---

## 3\. `[project]`

Where the source lives and where builds write.

```toml
[project]
content-root = "docs"
output-dir = ".ascribe/build"
```

Tested with ascribe 0.4.4 on Linux.

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `content-root` | string (path) | `"docs"` | The content root ([SPEC §2.2](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#22-pages-and-fragments)): the directory holding every source file. Paths in links and includes that begin with `/` are relative to it. It must exist and be a directory. |
| `output-dir` | string (path) | `".ascribe/build"` | Where `ascribe build` writes output. Each build and emitter writes under `<output-dir>/<build>/<emitter>/`. It need not exist. It may be outside the project's folder, with `..`, and a build then writes there. A build never writes or removes through a symbolic link on the way to it, or in it ([output-layout contract](https://ascribed-dev.com/docs/contracts/output-layout.md#2-what-ascribe-owns)). |

**Rules:** both paths are relative to the project root (`model-path-absolute`). Neither folder can be inside the other, and they can't be one folder (`model-output-overlaps-content`): a build would read its own output as source, or delete source as stale output. Ascribe tells them apart after resolving `.`, `..`, and, for folders that are there, symbolic links, so a link can't hide one inside the other.

The defaults: `".ascribe/build"` keeps generated output out of the way of both the source and a consumer's own `dist/`.

---

## 4\. `[types.<name>]` and `[fragments]`

### 4\.1 Page types

A **content type** is a frontmatter schema for a kind of page. A page names its type in its frontmatter, with `type`.

```toml
[frontmatter]
description = "string?"

[types.guide]

[types.reference.frontmatter]
api-version = "string"
```

`<name>` is the type's name (`key` rule). It appears in diagnostics, in generated code (the Zod schema's name), and on each page of the type, as the value of `type`.

**Frontmatter every type shares.** `[frontmatter]` declares the fields every page type has, written as a type's own are, so a field every page has is written once. A type's own `frontmatter` adds fields to them. A type that declares a field `[frontmatter]` has replaces it whole, for that type: an index page's type can make a shared `description` optional, or give it a default. The two aren't merged key by key, so a field reads as one declaration, wherever it's written. The implicit `page` type of a project that declares no types has them too.

**Where a field is declared.** In VS Code, hover a field's key in `ascribe.toml`, or a frontmatter key in a page, to see whether the field is `[frontmatter]`'s, the type's own, or the `title` every type has; on a field of `[frontmatter]`, the hover names the types that replace it. `ascribe model --section types` says the same after each field's type, and its JSON in each field's `declared`: `frontmatter`, `type`, or `implicit`. A type's field that's the same as `[frontmatter]`'s, key for key, changes nothing, and is reported as advice at the type's field (`frontmatter-field-repeated`), with a fix that deletes it; written in another form, as `{ type = "string" }` for `"string"`, it's still the same field.

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `frontmatter` | table of fields | `{}` (no fields) | The fields every page type has, as a type's own `frontmatter` declares them (below), so a field every page has is written once. A type's own fields add to them, and one of the same name replaces the shared field whole, for that type. The implicit `page` type of a project with no types has them too. |

Every page type has a required string `title`, whether it's declared or not (below), so a type with nothing else of its own needs no keys: `[types.guide]` above is a type a page can name, with `title` and the shared fields.

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `frontmatter` | table of fields | `{}` (no fields of its own) | The type's own frontmatter schema: one entry per field, keyed by field name (`name-word`). Values are field types (§5). Every type has a required string `title`, declared or not (below). A type can't declare a field `type`, which is reserved (`model-field-reserved`). |
| `purpose` | string | none | What a page of the type is for, in a few words. The rules `ascribe agents sync` writes say it on the type's line, and `ascribe model` shows it. A type copied from the starter set says its summary there without one (below). |
| `template` | string (path) | none | The page a page of the type starts from: a file from the folder `ascribe.toml` is in, outside the content root (below). A type with no `template` and no `guide` takes both from `templates/templates.lock`, when the lock `ascribe templates eject` wrote records the type. |
| `guide` | array of paths | `[]` | What to read before writing a page of the type, in the order to read it, such as a guide and a project's own notes on it: files from the folder `ascribe.toml` is in, outside the content root (below). Without it or `template`, from `templates/templates.lock`, as `template` is. |

**Which type a page is.** A page says what type it is with `type` in its frontmatter, the first key by convention, wherever its file is: a folder can hold pages of every type.

```yaml
---
type: reference
title: Commands
api-version: "3"
---
```

- **A project that declares no types** has one, the implicit `page`, whose fields are a required string `title` and `[frontmatter]`'s. Its pages need no `type`, and a page that has one is an error (`content-type-unknown`), with a fix that removes it.
- **A project that declares types** has no implicit one. A page that names no type is an error on the page (`content-type-missing`), with a fix for each declared type that adds `type:` to the page's frontmatter. A page whose `type` names a type that isn't declared is an error at the value (`content-type-unknown`), with a fix to the closest name when one is close.
- **[Fragments](https://ascribed-dev.com/docs/glossary.md#fragment)** have no type (§4.3), and can't use `type` (`frontmatter-reserved-in-fragment`).

A type's table has no keys that say which pages it applies to: `files` and `default` are unknown keys (`model-unknown-key`), whose message says to name the type on its pages instead.

**A type's template and guide.** Both are paths from the folder `ascribe.toml` is in, and both name files that aren't pages:

```toml
[types.how-to]
template = "templates/how-to.md"
guide = ["templates/how-to.guide.md", "templates/how-to.ascribe.md"]
```

Each file must be there, with its name's case as it is on disk, inside the folder `ascribe.toml` is in, and outside the content root, where it would be read as a page, and the output directory, which a build writes and removes. A symbolic link is followed to where it leads (`model-type-file`). `guide` is always a list, even with one file in it (`model-wrong-type`). The template is read when the content model loads, and must read as a page: valid YAML between its `---` lines, and no error in its Ascribe syntax (`model-type-template`). Either is a problem with the content model, reported at the path, once, rather than on every page made from the template.

**A template from the lock.** A type with neither key takes its template and guide from `templates/templates.lock`, the file [`ascribe templates eject`](https://ascribed-dev.com/docs/templates/command.md#ascribe-templates-eject) writes, when the lock records a template under the type's name: its template, then their guide and Ascribe's notes on the type, in the order to read them. That's how a type `eject` added or kept has its template, with nothing about it in `ascribe.toml`. A type that names either key keeps what it names and takes nothing from the lock, so a project without one, or a template written by hand, names its files as above.

The files the lock gives are held to the same rules, and a problem with one is the type's (`model-type-template`), reported at the type's `[types.<name>]`, since `ascribe.toml` has no path to point at. A lock that isn't there, or isn't one `eject` wrote, gives nothing; [`ascribe templates status`](https://ascribed-dev.com/docs/templates/command.md#ascribe-templates-status) says what's wrong with it. In VS Code, the hover on a type's name in `ascribe.toml` says where its template comes from.

**What a type is for.** `purpose` is one string, for a project's own types, and for a team that wants to say what one of the starter set's types means to it:

```toml
[types.how-to]
purpose = "One task, done step by step, for someone who knows why they're doing it."
```

The rules for agents say it at the end of the type's line. A type `ascribe templates eject` added and that has no `purpose` says their index's summary instead, from the `templates.lock` beside its template. When the rules don't fit in their 4,000 characters, a type's line keeps its name and what it's for after the rest of it is cut, and the page types are [the last thing cut](https://ascribed-dev.com/docs/agents/instructions.md#when-the-rules-are-long), with `ascribe model --section types` named for the rest.

**A page that still has its template's text.** On a page of a type with a template, `ascribe check` reports each of the template's placeholders the page still has word for word, and a frontmatter field with the template's value: as advice in a draft (`template-placeholder-left`), and as an error in a page that publishes (`template-placeholder-published`). The [guide to page templates](https://ascribed-dev.com/docs/templates.md#whats-left-to-fill-in) says what it finds and what it can't.

**A section the template leaves to the page.** A section the template gives nothing under its heading but [memos](https://ascribed-dev.com/docs/glossary.md#memo), and that a page of the type still leaves empty, builds as a heading with nothing under it; `ascribe check` reports it as advice at its heading (`template-section-empty`). The [guide to page templates](https://ascribed-dev.com/docs/templates.md#whats-left-to-fill-in) says what counts as empty.

**The page title.** Every page type has `title`, a required `string` field, whether it declares it or not. It's the page's title everywhere Ascribe needs one: an empty link to the page shows it as its text, for one ([SPEC §5.2](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#52-links)). Declaring it, in `[frontmatter]` or in a type's own `frontmatter`, changes only how it's read: it can accept phrases (§11), and code spans (§5.3). A declared `title` that's optional, has a default, or isn't a string is an error at the field (`model-field-reserved`).

**Reserved keys.** These are reserved frontmatter keys. Any page can use them, whatever its type says, and they mean what SPEC says; a type, or `[frontmatter]`, that declares one is an error (`model-field-reserved`). Generated consumer schemas include them automatically, except the three that reach a site another way.

| Key | What it is | In generated consumer schemas |
| --- | --- | --- |
| `type` | The page's [content type](https://ascribed-dev.com/docs/content-model.md#4-typesname-and-fragments) | Included |
| `available` | [SPEC §4.4](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#44-available) | Included |
| `variant` | [SPEC §4.3](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#43-variant) | Included |
| `permalink` | [SPEC §2.1](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#21-files), a page's [permalink](https://ascribed-dev.com/docs/writing/frontmatter.md#a-pages-permalink) | Included |
| `listing` | [Listing a page](https://ascribed-dev.com/docs/content-model.md#listing-a-page), below | Left out: it reaches a site in the page's record (output-layout contract §1.3) |
| `replaces` | [SPEC §2.1](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#21-files), the page [a page replaces](https://ascribed-dev.com/docs/writing/frontmatter.md#a-page-that-replaces-another) | Left out: it reaches a site in the versions file (output-layout contract §1.5) |
| `nav-label` | [SPEC §2.1](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#21-files), a page's name in the sidebar and the previous and next links | Left out: it reaches a site in the navigation (output-layout contract §1.6) |

`intended` ([SPEC §4.9](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#49-intended)) is reserved too, and accepted by every page and every fragment: it [acknowledges a check's problem](https://ascribed-dev.com/docs/writing/directives.md#intended), and since no output has it, generated consumer schemas leave it out. With the `astro` profile, a type can't declare `slug` either (`model-field-reserved`): Astro would take a page's `slug` as its entry id instead of its path, and publish the page at an address Ascribe never worked out.

`formatted` is reserved on page types too: the site output writes the formatted form of fields that set `inline` under it (§5.3), so a type must not declare it (`model-field-reserved`).

`source` is reserved on page types too: the site output writes each page's path from the content root under it, for a layout's edit link, so a type must not declare it (`model-field-reserved`).

### Listing a page

A page says whether readers that aren't people are offered it with `listing` ([SPEC §2.1](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#21-files)), one of three values, from open to closed: `listed`, the default when a page doesn't set it, `unquoted`, and `unlisted`. What each tells search engines and agents is in [Publishing](https://ascribed-dev.com/docs/publishing.md#how-a-page-is-listed). Any other value, or one that isn't a string, is an error (`frontmatter-type-mismatch`).

```yaml
---
title: Migrating from 3.x
listing: unlisted
---
```

Each build writes every page's record, with its `listing`, to `_ascribe/pages.json` in the site output. The site output's frontmatter doesn't carry `listing`, since the record does.

A project can set `listing` for the whole site, a folder, or a content type, in [`[listing]`](https://ascribed-dev.com/docs/content-model/output.md#17-listing). A page under a rule can set the same value or a stricter one, never a looser one (`listing-loosened`).

**Unknown frontmatter keys.** A page whose frontmatter has a key its type doesn't declare (and that isn't reserved) is an error on the page.

### 4\.2 Frontmatter values

Frontmatter is YAML. For type checking, processors parse it with the YAML 1.2 **core schema**: `true` and `false` are booleans (`yes`, `no`, `on`, and `off` are strings), and `3.10` is the number 3.1. So a `string` field whose value is `3.10` unquoted is a type error, and the message suggests quoting it.

### 4\.3 Fragments

```toml
[fragments]
patterns = ["includes/**", "**/*.partial.md"]

[fragments.frontmatter]
owner = "string?"
```

Tested with ascribe 0.4.4 on Linux.

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `patterns` | array of patterns | `[]` | Additional fragment patterns ([SPEC §2.2](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#22-pages-and-fragments)). A file is a fragment if any segment of its path begins with `_`, or its path matches one of these patterns. |
| `frontmatter` | table of fields | `{}` (no fields) | The fragment schema ([SPEC §2.2](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#22-pages-and-fragments)): the frontmatter every fragment is validated against. Fields as in §5. `title` is not required. |

Content types never apply to fragments: a fragment names no type, and `type` in a fragment's frontmatter is an error (`frontmatter-reserved-in-fragment`).

**Reserved keys on fragments.** `type`, `available`, `variant`, `permalink`, `listing`, `replaces`, and `nav-label` are a page's, so a fragment that has one in its frontmatter gets an error, and `[fragments.frontmatter]` can't declare them (`model-field-reserved`). To mark what a fragment contributes, put `@available` inside it. A fragment can use `intended`, which `[fragments.frontmatter]` can't declare either.

---

## 5\. Field and attribute types

Two related type languages:

- **Field types** describe frontmatter fields (`[types.<name>.frontmatter]`, `[fragments.frontmatter]`). They cover the [SPEC §7.2](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#72-declarations) set: string, number, boolean, date, enumeration, list, and object, each optional or with a default.
- **Attribute types** describe attributes on images and widgets (`[images.attributes]`, `[widgets.<name>.attributes]`). They cover [SPEC §3.3](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#33-attributes)'s set: string, enumeration, boolean, and number, plus set-valued keys.

Each entry is written either in **short form**, a string, or in **table form**, when it needs a default, a description, phrases, enumeration values that aren't simple words, or nested fields.

```toml
[types.guide.frontmatter]
title = "string"                                   # required string
description = "string?"                            # optional string
level = "enum(beginner, intermediate, advanced)"   # required, one of three
tags = "list(string)?"                             # optional list of strings
updated = "date?"                                  # optional date
status = { type = "enum(draft, published)", default = "published" }
author = { type = "object?", fields = { name = "string", url = "string?" } }
```

### 5\.1 Short form

A field is **required** unless its type ends in `?` or it has a default. The same holds for attributes: `lab = "string"` means every use of the widget must give `lab`.

| Short form | Field types | Attribute types | Accepts |
| --- | --- | --- | --- |
| `string` | yes | yes | Frontmatter: a YAML string. Attribute: a token or quoted string. |
| `number` | yes | yes | Frontmatter: a YAML integer or float. Attribute: a token matching `["-"] 1*DIGIT ["." 1*DIGIT]` (`600`, `-2`, `1.5`; not `600px`). |
| `boolean` | yes | yes | Frontmatter: YAML `true` or `false`. Attribute: the token `true` or `false` ([SPEC §3.3](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#33-attributes)). |
| `date` | yes | no | A calendar date written `YYYY-MM-DD` that exists (`2026-02-30` is an error), quoted or not. Times aren't accepted. |
| `enum(a, b, …)` | yes | yes | One of the listed values, compared exactly. Frontmatter: a YAML string. Attribute: a token or quoted string. |
| `list(T)` | yes | no | A YAML sequence whose items are all of type `T`, where `T` is `string`, `number`, `boolean`, `date`, `enum(…)`, or (table form only) `object`. An empty sequence is allowed. |
| `set(T)` | no | yes | A value set ([SPEC §3.3](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#33-attributes)), such as `platform=cloud\|on-prem`, or a single token. `T` is `string` or `enum(…)`. Members are tokens. Only keys typed `set(…)` accept `\|`. |
| `object` | table form only | no | A YAML mapping whose keys are the object's `fields` (nested field types). Unknown keys are errors. |
| any of the above + `?` | yes | yes | The same type, optional. |

Grammar (ABNF, with the rules of SPEC Appendix A):

```abnf
field-type      = field-base [ "?" ]
field-base      = scalar / enum / list-type / "object"    ; "object" in table form only
scalar          = "string" / "number" / "boolean" / "date"
list-type       = "list" "(" OWS ( scalar / enum / "object" ) OWS ")"
                                        ; "list(object)" in table form only

attribute-type  = attribute-base [ "?" ]
attribute-base  = "string" / "number" / "boolean" / enum / set-type
set-type        = "set" "(" OWS ( "string" / enum ) OWS ")"

enum            = "enum" [ "(" OWS enum-value *( OWS "," OWS enum-value ) OWS ")" ]
                                        ; bare "enum" in table form only, with "values"
enum-value      = 1*( ALPHA / DIGIT / "-" / "_" / "." )
```

Spaces are allowed only where `OWS` appears. The canonical spelling has no spaces except one after each comma: `enum(a, b)`.

### 5\.2 Table form

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | string | **required** | A field type or attribute type (§5.1). In table form it may also be `object`, `list(object)`, or bare `enum`, with the keys below. |
| `fields` | table of fields | required when `type` is `object`, `object?`, `list(object)`, or `list(object)?`; not allowed otherwise | The nested fields of an object, in the same syntax (short or table form). Field types only. |
| `values` | array of strings | required when `type` uses bare `enum`; not allowed otherwise | The enumeration's values, for values that aren't simple words, such as `["Getting started", "How-to"]`. Values must be distinct. Values of a `set(enum)` attribute must be tokens. |
| `default` | any TOML value | none | The value used when the field or attribute is absent. It must have the declared type (a TOML string for `string` and `enum`, integer or float for `number`, boolean for `boolean`, local date for `date`, array for `list`, inline table for `object`, a string or array of strings for `set`). A field with a default is optional; adding `?` as well is allowed and changes nothing. |
| `phrases` | boolean | `false` | Whether phrases ([SPEC §5.1](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#51-phrases)) are substituted in this field's value. Field types only; allowed only on `string` and `list(string)` fields (and their optional forms), including fields nested in objects. See §11. |
| `inline` | string | none | `"code"` reads the value's code spans. Allowed only on a page type's top-level `string` fields (and `string?`), its own or `[frontmatter]`'s. See §5.3. |
| `description` | string | none | Help text shown by the editor (hover and completion) and emitted into generated schemas as documentation. |
| `role` | string | none | What the field is for, which a check reads: `"description"`, `"recheck-date"` (or `"review-date"`, its old name, for a release), or `"owner"`. Allowed only on a page type's top-level fields, its own or `[frontmatter]`'s. A field named for a role has it without `role`, unless the type gives it to another field. See §5.4. |

Nesting beyond one level of `fields` is allowed but discouraged; keep frontmatter flat.

### 5\.3 Code in a field

A backtick in a `string` field is just a backtick. To show code in a page title, set `inline = "code"` on the field:

```toml
[types.reference.frontmatter]
title = { type = "string", inline = "code" }
```

```yaml
---
title: "`ascribe.toml` reference"
---
```

The value is then read as Markdown that knows only code spans. A run of backticks opens a code span, and the next run of the same length closes it. Outside code spans, a backslash escapes punctuation, so `` \` `` is a literal backtick. Emphasis, links, and HTML stay literal text, since a title also becomes the text of other links. With `phrases = true` as well, phrases are substituted outside code spans, never inside them.

Each output has the title both ways: as plain text, `ascribe.toml reference`, and formatted, with the code spans kept.

- **Empty link text** and the plain-markdown output's heading keep the code spans: ``[`ascribe.toml` reference](…)``.
- **The site output** writes the field as plain text, for a layout's `<title>`, search, and sorting, and its formatted form as HTML under `formatted`, for the page's heading and navigation. The generated schema types both, so a layout uses `entry.data.formatted.title` with `set:html`. See [Astro](https://ascribed-dev.com/docs/astro.md#the-layout).
- **The JSON output** has the plain text in `title` and `frontmatter`, and the formatted form in `formatted.title`, a list of text and code pieces.
- **Review** shows the formatted title where a page's title is a heading or a list entry: the heading of VS Code's [page preview](https://ascribed-dev.com/docs/glossary.md#page-preview), the [HTML preview](https://ascribed-dev.com/docs/glossary.md#html-preview)'s list of pages and its page headings, and the list of comments in the editor and the [site preview](https://ascribed-dev.com/docs/glossary.md#site-preview). **Ascribe: Changed Pages**, which can show only text, writes the code spans between backticks. Tooltips, buttons, and search keep the plain text.

Only a page type's top-level `string` fields can set it, its own or `[frontmatter]`'s, not fields in objects or the fragment schema, and `"code"` is its only value (`model-inline-field`).

### 5\.4 Field roles

A check that needs a page's description or the date it's next due to be checked, or a report that counts pages by owner, reads the field with that role.

A field named for a role has it, when its type is one the role takes:

```toml
[frontmatter]
description = "string?"
recheck-date = "date?"
owner = "enum(docs, platform)"
```

A field's name is yours to choose, so a field of another name takes a role with `role`, and the field named for it then has none:

```toml
[types.guide.frontmatter]
summary = { type = "string?", role = "description" }
review-by = { type = "date?", role = "recheck-date" }
team = { type = "enum(docs, platform)", role = "owner" }
```

| Role | The field's type | What reads it |
| --- | --- | --- |
| `description` | `string`, optional or not | [`page-description-missing`](https://ascribed-dev.com/docs/checks/diagnostics/content-checks.md#asc150-page-description-missing) reports a page of the type without one. A required field, or one with a default, always has one, so the check has nothing to report: a page that leaves a field with a default out, or empty, has the default as its description, in `llms.txt` and the page records too. [`description-link`](https://ascribed-dev.com/docs/checks/diagnostics/content-checks.md#asc198-description-link) reports a Markdown link in one: a description is plain text, so the link is shown as written. |
| `recheck-date` | `date`, optional or not | [`recheck-overdue`](https://ascribed-dev.com/docs/checks/diagnostics/content-checks.md#asc153-recheck-overdue) reports a page whose date has passed. `review-date`, the role's old name, still works, as a value of `role` and as a field's name, and so does the check's, `review-overdue`, in `[checks]` and in an acknowledgement, until the next release. |
| `owner` | `string` or `enum`, optional or not | [`ascribe report inventory`](https://ascribed-dev.com/docs/checks/report-command.md#ascribe-report-1) counts pages by owner. |

Without a field with the role, those checks don't look at the type's pages, and the report counts its pages as having no owner. A role changes nothing in the outputs.

In VS Code, the sidebar's [This page](https://ascribed-dev.com/docs/editor/sidebar.md#this-page) shows a page's description against its limit, and when it's due for a recheck, from the fields with these roles.

**Rules.** A role is one of the three (`model-field-role`), on a field of its type, in a page type's top-level fields, its own or `[frontmatter]`'s: not a field in an object, nor in the fragment schema. Two fields of one table can't have the same role. A role a type gives one of its own fields with `role` is that field's, and not a field's in `[frontmatter]`. A field named for a role has it only when no other field of the type is given it, and its type is one the role takes: a `description` that's a number is a field like any other.
