# [FrontMatter] — a document's header as typed data

> `[FrontMatter]` binds a member to a key of the entity's markdown document's front-matter — the `---` header at the top of the file. Assigning the document fills those members in, so a document's header becomes ordinary, queryable app data rather than an opaque block, and a required member makes its key mandatory.

<!-- id: entity-front-matter · area: entity · stability: preview · html: https://osysharp.com/reference/entity/front-matter/ -->

## Summary        {#summary}
Markdown documents often begin with a `---` header:

```text
---
title: Getting started
summary: How to create your first project.
stability: stable
---

## Overview
…
```

For a document that header is rarely "metadata". It is usually most of the **application's data** — the title is the
navigation, the summary is the search snippet, the tags are the facets. `[FrontMatter]` says so:

```osy syntax
entity DocPage {
  [FrontMatter] string Title;
  [FrontMatter] string? Summary;
  Markdown Body;
}
```

Assign `Body` and those members fill in. They are ordinary members, so everything else about them is ordinary too:
you can query them, index them, put them in a grid, secure them.

## Signature      {#signature}
```osy syntax
[FrontMatter] <type> <Member>;              // key = the member name, camelCased
[FrontMatter("<key>")] <type> <Member>;     // key named exactly
```

## Description    {#description}

### Which key a member takes   {#key}

A bare `[FrontMatter]` derives the key from the member name by **camelCasing** it. Osy# members are PascalCase and
front-matter keys conventionally are not, so this is usually all you need:

| Member | Key |
|---|---|
| `Title` | `title` |
| `FormatVersion` | `formatVersion` |
| `DeprecatedBy` | `deprecatedBy` |

When a key does not follow that convention — or when the member has to be called something else — name it exactly:

```osy title="naming a key that the camelCase rule would miss" syntax
[FrontMatter("format-version")] int? Version;
```

That form is also the answer for a key called `id`. Every entity already has an `Id`, so a member cannot be named
that; call it something else and name the key:

```osy title="the key called id, which no member may be named" syntax
[FrontMatter("id")] string Slug;
```

### Which document it reads   {#document}

The entity's `Markdown` member. You do not name it, because an entity has one document in the ordinary case and
repeating its name on every key would be ceremony.

An entity with **no** `Markdown` member is a compile error — there is nothing to read. An entity with **more than
one** is also a compile error, naming both, because guessing which document was meant would be a coin flip you never
see.

### What binding does, and does not do   {#binding}

**It fills in declared members, on assignment.** Every write of the whole document — `page.Body = text` — re-reads the
header and sets each bound member.

**A key the document omits CLEARS its member.** A member says what the document says *now*, not what an earlier
document said. This is why a key that may be absent should be declared optional:

```osy title="a key the document may omit must be optional" syntax
[FrontMatter] string? Stability;    // the document may not carry it
```

**A required member makes its key mandatory.** This is the useful consequence of the same rule: declare
`[FrontMatter] string Title;` without the `?` and a document that omits `title` is refused at commit, with `Title` named.
The declaration is the schema — front-matter that is checked rather than merely stored.

**A key you did NOT declare is preserved, untouched.** The header is kept whole; declaring three of its ten keys binds
three and leaves the other seven exactly as written. A reader that quietly dropped what it did not understand would
corrupt the document it read.

### Types   {#types}

The header is YAML, so its ordinary shapes work: quoted and unquoted strings, folded blocks (`summary: >`), flow
sequences (`tags: [a, b]`), block sequences, numbers, booleans, and an explicit `null`.

| Declared as | Takes |
|---|---|
| `string` | a scalar (a folded block arrives as one line) |
| `int` / `decimal` / `bool` / `DateTime` / `Guid` | a scalar that parses as one; anything else leaves the member unset |
| a child collection | a list-valued key, one **row** per item — see below |
| `Json` | a list-valued key, keeping its items as a JSON array |

An explicit `null` (`deprecatedBy: null`) is *nothing*, not the text "null".

### A list-valued key   {#list-key}

`tags: [ui, authoring]` is a list, and a list of what you want to *query* is a set of rows. Bind it to a child
collection and each item becomes one:

```osy title="a list-valued key, bound to child rows" syntax
entity DocPage {
  [FrontMatter] [ForeignKey(Page)] DocTag[] Tags;
  Markdown Body;
}

entity DocTag {
  DocPage Page;
  [MaxLength(80)] string Name;
}
```

The rows are ordinary child rows, so the things you wanted them for work with nothing extra:

```osy title="querying those rows like any other child" syntax
DocPage.Where(p => p.Tags.Any(t => t.Name == "ui")).ToList()
```

The element entity must have **exactly one** scalar member — the one each item becomes. None, or more than one, is a
compile error naming them, because there would be nothing to put the item in or no way to tell which member was meant.
The collection must be a real `[ForeignKey]` child collection: a bare `DocTag[]` is an in-memory projection with no
rows behind it, so binding one is a compile error too rather than a write that quietly goes nowhere.

Re-assigning the document **reconciles**: an item that is still there keeps its row (and its id, so anything pointing
at it survives), an item that arrived is created, one that is gone is deleted, and a key the new document omits empties
the collection. A repeated item is one row — a set of tags is a set. A scalar is a one-item list, so `tags: ui` works.

Use `Json` instead when the items are only ever read together and never queried one by one.

A malformed header binds nothing rather than failing the write. Front-matter is content — hand-written, and sometimes
agent-written — and a mistyped header should still store its document.

### Reading back   {#reading-back}

Nothing changes about how you read the document itself: `Body` still returns the markdown, and [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/)
renders it.

### Importing documents   {#importing}

Assigning the member is what runs all of this, so anything that assigns it works — including a data file. A document
can be given inline, or named as a file beside the data file, which is how a real corpus arrives:

```json
{ "entity": "DocPage", "key": "Slug",
  "files": { "Body": "" },
  "rows": [ { "Body": "pages/controls.md" }, { "Body": "pages/slots.md" } ] }
```

Two things about that are worth knowing:

- **A document's file reference brings its TEXT**, not a stored path. Unlike an image column — which stores the bytes
  and records where they were put — a document has no path; it becomes sections. So it needs no `public/` prefix.
- **The rows carry no `Slug`.** A page's identity is its own `id:` key, and the import reads the key from the
  document's header when the row does not give it. That is what makes re-importing a corpus converge instead of
  doubling it, and it keeps one fact in one place — the document is the thing that is true.

If neither the row nor the header carries the key, the import says so and names both places it looked.

## Examples       {#examples}

A page whose header is its data — the title drives the nav, the summary the listing, and both are queryable:

```osy title="a docs page" test app=entity-front-matter
entity DocPage {
  [FrontMatter("id")] [MaxLength(200)] string Slug;
  [FrontMatter] [MaxLength(200)] string Title;
  [FrontMatter] string? Summary;
  [FrontMatter] [MaxLength(50)] string? Stability;
  [FrontMatter] [ForeignKey(Page)] DocTag[] Tags;
  [MaxLength(50)] string Area;
  Markdown Body;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

entity DocTag {
  DocPage Page;
  [MaxLength(80)] string Name;
  security { allow create, read, update, delete when IsAuthenticated || IsAnonymous; }
}

// `Title` and `Slug` are required, so every document must carry `title:` and `id:`.
void Publish(string area, string markdown) {
  new DocPage { Area = area, Body = markdown };
}

DocPage[] Preview() => DocPage.Where(p => p.Stability == "preview").ToList();

// `tags: [ui, …]` became rows, so a facet is an ordinary query.
DocPage[] Tagged(string tag) => DocPage.Where(p => p.Tags.Any(t => t.Name == tag)).ToList();
```

## See also       {#see-also}
- [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) — rendering the document the header belongs to.
- [entity members](https://osysharp.com/reference/entity/properties/) — declaring members, and what optional means.
- [entity](https://osysharp.com/reference/entity/declaration/) — the entity the members live on.
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — querying by a bound member, like any other.
