---
title: The front-matter capability system
description: How IFrontMatter distinguishes universal capabilities (as default members) from selective ones (as separate interfaces) — and why presence of a capability interface is a meaningful signal.
canonical_url: https://usepennington.net/explanation/core/front-matter-capabilities/
sidecar_url: https://usepennington.net/explanation/core/front-matter-capabilities.md
content_hash: sha256:7dffd8730c90d669744d7e1ef05fa391ff68de731434a228c898cd0ba18d9f0c
tokens: 3239
uid: explanation.core.front-matter-capabilities
reading_time_minutes: 5
---

Under the Hood
# The front-matter capability system

How IFrontMatter distinguishes universal capabilities (as default members) from selective ones (as separate interfaces) — and why presence of a capability interface is a meaningful signal.

 
Where does the line fall between "every page needs this" and "only some pages need this" when those two categories share a base interface?

 
## Context

 
A content engine asks a lot of questions about each page. Is it a draft? Does it belong in search? In the LLM index? Does it carry a cross-reference uid, a description, or a date? These questions apply to every page, even when the answer is trivially "no." A smaller set of questions — does it have tags? does it participate in ordered navigation? does it carry a section label? is it a redirect? — applies only to some content types.

 
Pennington models this split directly on the type system. The universal questions live on `IFrontMatter` itself, with sensible defaults, so every record answers them without having to opt in. The selective questions live on separate capability interfaces, so a content type that does not implement `ITaggable` is not tagged — and the engine can tell at compile time.

 
## How it works

 
### `IFrontMatter`: universal capabilities with defaults

 
`IFrontMatter` has one abstract member (`Title`) and seven default-implemented ones. Every front-matter record inherits `IsDraft => false`, `Search => true`, `Llms => true`, `SearchOnly => false`, `Uid => null`, `Description => null`, and `Date => null` without declaring them.

 
```csharp:symbol
public interface IFrontMatter
{
    /// <summary>Page title rendered in the browser tab, navigation, and OpenGraph tags.</summary>
    string Title { get; }
  
    /// <summary>True when the page is a draft and should be excluded from builds.</summary>
    bool IsDraft => false;
  
    /// <summary>True when the page should be included in the search index.</summary>
    bool Search => true;
  
    /// <summary>True when the page should be included in llms.txt output.</summary>
    bool Llms => true;
  
    /// <summary>
    /// When true, the page is included in indexing channels (search, llms.txt) but excluded
    /// from the rendered navigation tree. Useful for FAQ entries, glossary terms, or other
    /// content that should be discoverable by search but should not clutter the sidebar.
    /// </summary>
    bool SearchOnly => false;
  
    /// <summary>Stable cross-reference identifier used by xref links.</summary>
    string? Uid => null;
  
    /// <summary>Short summary used in meta descriptions, OpenGraph tags, and listings.</summary>
    string? Description => null;
  
    /// <summary>
    /// Publication date surfaced in feeds and sitemaps. Also drives scheduled publishing:
    /// when this is set to a moment after the build clock, the page is excluded from build
    /// output (same dev-vs-build behavior as <see cref="IsDraft"/>) until the clock catches up.
    /// </summary>
    DateTime? Date => null;
}
```

 
The contract gives every record common defaults it can override. A minimal record exposes a single required `Title` property and the engine handles drafts, search indexing, LLM indexing, cross-references, descriptions, and dates gracefully. Engine code uses the members directly — `if (page.IsDraft)` works on every `IFrontMatter` without checking for each interface first.

 
### The capability interfaces

 
Tags, order, section labels, redirects, and Standard Site document keys live on separate interfaces because the interface's *presence* is itself a signal. Seeing `IOrderable` on a record says the content type consciously participates in ordered navigation; folding it into `IFrontMatter` would erase that distinction, since every record would then carry the member whether it meant anything or not. The selectivity is real, too: a blog post has tags but no meaningful order among siblings; a doc page has an order but no redirect target; a redirect stub carries a destination URL and little else. Folding these into `IFrontMatter` would force every record to carry empty tag arrays and meaningless sort keys.

 
```csharp:symbol
public interface IOrderable
{
    /// <summary>Sort order for this page within its section (lower sorts first).</summary>
    int Order { get; }
}
```

 
`NavigationBuilder` keys off the `IOrderable` interface itself, not a sentinel value in the `Order` property. A content type either implements the interface and participates in ordered navigation, or it does not; there is no "this page has no meaningful order" case to handle. The same applies to `ITaggable` (tag cloud participation), `ISectionable` (section-label breadcrumbs), `IRedirectable` (redirect-stub semantics), and `IStandardSiteDocument` (the AT Protocol record key for Standard Site syndication).

 
The whole matrix fits in one view. The two shipped records implement different subsets — a doc page orders itself within its section, a blog post syndicates to Standard Site instead — and that difference is visible in the type declarations, not in runtime flags:

 
.b-b1c63163{--beck-surface:var(--color-base-50, #ffffff);--beck-node-bg:var(--color-base-50, #ffffff);--beck-node-border:var(--color-base-200, #e2e8f0);--beck-node-shadow:0 1px 3px rgb(0 0 0 / 0.05), 0 4px 12px rgb(0 0 0 / 0.06);--beck-text:var(--color-base-800, #1e293b);--beck-text-muted:var(--color-base-500, #64748b);--beck-text-faint:var(--color-base-400, #94a3b8);--beck-primary:var(--color-primary-600, #175ddc);--beck-success:var(--color-emerald-500, #10b981);--beck-warn:var(--color-amber-500, #f59e0b);--beck-danger:var(--color-red-500, #ef4444);--beck-info:var(--color-violet-500, #8b5cf6);--beck-neutral:var(--color-base-400, #94a3b8);--beck-group-border:color-mix(in srgb, var(--beck-neutral) 45%, transparent);--beck-group-label:var(--beck-text-muted);--beck-edge:var(--color-base-300, #cbd5e1);--beck-packet:var(--beck-primary);--beck-icon-bg:var(--color-base-100, #f1f5f9);--beck-accent:var(--beck-primary);--beck-font:'Inter', system-ui, -apple-system, sans-serif;--beck-font-mono:'IBM Plex Mono', ui-monospace, monospace;}[data-theme='dark'] .b-b1c63163{--beck-surface:var(--color-base-950, #0d1117);--beck-node-bg:var(--color-base-900, #161b22);--beck-node-border:var(--color-base-700, #30363d);--beck-node-shadow:0 1px 3px rgb(0 0 0 / 0.3), 0 4px 14px rgb(0 0 0 / 0.4);--beck-text:var(--color-base-50, #f0f6fc);--beck-text-muted:var(--color-base-400, #8b949e);--beck-text-faint:var(--color-base-500, #6e7681);--beck-edge:var(--color-base-700, #30363d);--beck-icon-bg:var(--color-base-800, #21262d);--beck-font:'Inter', system-ui, -apple-system, sans-serif;--beck-font-mono:'IBM Plex Mono', ui-monospace, monospace;}@media (prefers-color-scheme: dark){:root:not([data-theme='light']) .b-b1c63163{--beck-surface:var(--color-base-950, #0d1117);--beck-node-bg:var(--color-base-900, #161b22);--beck-node-border:var(--color-base-700, #30363d);--beck-node-shadow:0 1px 3px rgb(0 0 0 / 0.3), 0 4px 14px rgb(0 0 0 / 0.4);--beck-text:var(--color-base-50, #f0f6fc);--beck-text-muted:var(--color-base-400, #8b949e);--beck-text-faint:var(--color-base-500, #6e7681);--beck-edge:var(--color-base-700, #30363d);--beck-icon-bg:var(--color-base-800, #21262d);--beck-font:'Inter', system-ui, -apple-system, sans-serif;--beck-font-mono:'IBM Plex Mono', ui-monospace, monospace;}}.b-b1c63163{font-family:var(--beck-font);}.b-b1c63163 .beck-fx-node{transform-box:fill-box;transform-origin:center;}.b-b1c63163 .beck-packet-label{font-family:var(--beck-font-mono);font-size:11px;font-weight:600;}.b-b1c63163 .beck-node{fill:var(--beck-node-bg);stroke:color-mix(in srgb, var(--beck-accent) 32%, var(--beck-node-border));stroke-width:1.5;filter:drop-shadow(0 1px 3px rgb(0 0 0/.05)) drop-shadow(0 4px 12px rgb(0 0 0/.06));}[data-theme='dark'] .b-b1c63163 .beck-node{filter:drop-shadow(0 1px 3px rgb(0 0 0/.3)) drop-shadow(0 4px 14px rgb(0 0 0/.4));}@media (prefers-color-scheme: dark){:root:not([data-theme='light']) .b-b1c63163 .beck-node{filter:drop-shadow(0 1px 3px rgb(0 0 0/.3)) drop-shadow(0 4px 14px rgb(0 0 0/.4));}}.b-b1c63163 .beck-node--external{stroke-dasharray:5 4;}.b-b1c63163 .beck-node--subtle{opacity:.72;}.b-b1c63163 .beck-node--ghost{fill:transparent;stroke-dasharray:5 4;filter:none;}.b-b1c63163 .beck-icon-chip{fill:color-mix(in srgb, var(--beck-accent) 15%, var(--beck-icon-bg));}.b-b1c63163 .beck-node--ghost .beck-icon-chip{fill:transparent;}.b-b1c63163 .beck-icon{color:var(--beck-accent);}.b-b1c63163 .beck-node-title{fill:var(--beck-text);}.b-b1c63163 .beck-node-subtitle,.b-b1c63163 .beck-ghost-label{fill:var(--beck-text-muted);}.b-b1c63163 .beck-status-inline{fill:var(--beck-accent);}.b-b1c63163 .beck-status-bg{fill:color-mix(in srgb, var(--beck-accent) 14%, transparent);}.b-b1c63163 .beck-status-text{fill:var(--beck-accent);}.b-b1c63163 .beck-group{fill:none;stroke:var(--beck-group-border);stroke-width:1.5;stroke-dasharray:6 6;}.b-b1c63163 .beck-group-label-bg{fill:var(--beck-surface);}.b-b1c63163 .beck-group-label{fill:var(--beck-group-label);}.b-b1c63163 .beck-node--start{fill:var(--beck-text-muted);}.b-b1c63163 .beck-node--end{fill:none;stroke:var(--beck-text-muted);stroke-width:2;}.b-b1c63163 .beck-end-dot{fill:var(--beck-text-muted);}.b-b1c63163 .beck-class-head{fill:color-mix(in srgb, var(--beck-accent) 10%, transparent);}.b-b1c63163 .beck-class-head-border{stroke:color-mix(in srgb, var(--beck-accent) 28%, var(--beck-node-border));stroke-width:1;}.b-b1c63163 .beck-class-divider{stroke:var(--beck-node-border);stroke-width:1;}.b-b1c63163 .beck-class-stereo{fill:var(--beck-text-muted);}.b-b1c63163 .beck-class-title{fill:var(--beck-text);}.b-b1c63163 .beck-class-field{fill:var(--beck-text-muted);}.b-b1c63163 .beck-class-method{fill:var(--beck-text);}.b-b1c63163 .beck-lifeline{stroke-width:2;stroke-dasharray:6 7;}.b-b1c63163 .beck-activation{filter:drop-shadow(0 0 5px color-mix(in srgb, var(--beck-accent) 45%, transparent));}.b-b1c63163 .beck-msg-chip{fill:var(--beck-node-bg);stroke:color-mix(in srgb, var(--beck-accent) 40%, transparent);stroke-width:1;}.b-b1c63163 .beck-msg-text{fill:color-mix(in srgb, var(--beck-accent) 34%, var(--beck-text));}.b-b1c63163 .beck-msg--reply .beck-msg-chip{stroke:none;}.b-b1c63163 .beck-msg--reply .beck-msg-text,.b-b1c63163 .beck-msg-text--bare{fill:var(--beck-text-muted);}.b-b1c63163 .beck-band-box{fill:color-mix(in srgb, var(--beck-accent) 5%, transparent);stroke:color-mix(in srgb, var(--beck-accent) 30%, transparent);stroke-width:1.5;stroke-dasharray:6 6;}.b-b1c63163 .beck-band-chip{fill:var(--beck-surface);stroke:color-mix(in srgb, var(--beck-accent) 40%, transparent);stroke-width:1;}.b-b1c63163 .beck-band-label{fill:color-mix(in srgb, var(--beck-accent) 70%, var(--beck-text));}.b-b1c63163 .beck-edge{fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;}.b-b1c63163 .beck-edge-label{fill:var(--beck-text-muted);paint-order:stroke;stroke:var(--beck-surface);stroke-width:3px;stroke-linejoin:round;}.b-b1c63163 .beck-title{fill:var(--beck-text);}.b-b1c63163 .beck-subtitle{fill:var(--beck-text-muted);}«interface»IFrontMatterTitle: stringIsDraft = falseSearch = trueLlms = trueSearchOnly = falseUid = nullDescription = nullDate = null«interface»IOrderableOrder: int«interface»ITaggableTags: string[]«interface»ISectionableSectionLabel: string?«interface»IRedirectableRedirectUrl: string?«interface»IStandardSiteDocumentAtprotoRkey = null«record»DocSiteFrontMatter«record»BlogSiteFrontMatter
 
Both records also implement `IHasStructuredData`, which belongs to the JSON-LD subsystem rather than this capability system and is omitted here.

 
The rule of thumb is simple: if adoption is universal, the member lives on `IFrontMatter` with a sensible default. If adoption is selective, it lives on a capability interface so that pattern-matching on the interface remains meaningful.

 
### Custom front-matter records

 
A custom record buys typed access to extra keys (an `apiVersion` or `gitHubUrl` field becomes a strongly-typed property) plus the same set of capability interfaces to opt into. The defaults give what the shipped records would give; the custom record only declares what it adds. See [Define custom front-matter keys](https://usepennington.net/how-to/pages/front-matter.md) for the recipe.

 
## Further reading

 
 - Reference: [IFrontMatter and capability defaults](https://usepennington.net/reference/api/i-front-matter.md)
 - Reference: [Front matter key reference](https://usepennington.net/reference/front-matter/keys.md)
 - How-to: [Work with front matter](https://usepennington.net/how-to/pages/front-matter.md)
 
 
[Previous
                
                Dev mode and build mode share one code path](https://usepennington.net/explanation/core/dev-vs-build.md)[Next
                    
                The response-processing pipeline](https://usepennington.net/explanation/core/response-processing.md)