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.
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.
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 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 for the recipe.
Further reading
- Reference: IFrontMatter and capability defaults
- Reference: Front matter key reference
- How-to: Work with front matter