---
title: Define custom front-matter keys
description: Declare a record implementing IFrontMatter with extra YAML keys and register it through AddMarkdownContent so a markdown source deserializes into the custom type.
canonical_url: https://usepennington.net/how-to/pages/front-matter/
sidecar_url: https://usepennington.net/how-to/pages/front-matter.md
content_hash: sha256:0e7706925656a772e3f3df624f37647734ef3e2eedaf705fa02a693d3d272b4d
tokens: 2388
uid: how-to.pages.front-matter
reading_time_minutes: 4
---

Guides
# Define custom front-matter keys

Declare a record implementing IFrontMatter with extra YAML keys and register it through AddMarkdownContent so a markdown source deserializes into the custom type.

 
To parse YAML keys the shipped front-matter records do not expose — a `namespace`, a `stability` badge, a `productName` — declare a custom `record` implementing `IFrontMatter` and the capability interfaces relevant to the keys, then register it as a markdown source with `AddMarkdownContent<T>`. For the full catalog of built-in keys, see [Front matter key reference](https://usepennington.net/reference/front-matter/keys.md); for the design rationale behind the capability interfaces, see [The front-matter capability system](https://usepennington.net/explanation/core/front-matter-capabilities.md).

 
The recipe declares and registers the record in `examples/DocSiteKitchenSinkExample`, which adds `namespace` and `stability` keys on top of the built-in front-matter records, then reads those keys from a regular Razor page in `examples/CustomFrontMatterRazorPageExample`.

 
## Before you begin

 
 - An existing Pennington site with markdown content under a `Content/` folder (see [Create your first Pennington site](https://usepennington.net/tutorials/getting-started/first-site.md) if not).
 - A bare `AddPennington` host, or an existing `AddDocSite`/`AddBlogSite` host with room for an additional markdown source. `AddBlogSite` registers one source against `BlogSiteFrontMatter`; `AddDocSite` registers two sources (`DocSiteFrontMatter` and `BlogPostFrontMatter`). Adding a third custom-record source on top of the template is done by chaining another `AddMarkdownContent<T>()` call after `AddDocSite`/`AddBlogSite`, or by falling back to bare `AddPennington` (see [Serve docs and a blog from separate content roots](https://usepennington.net/how-to/discovery/multiple-sources.md)).
 
 
## Declare the record

 
Implement [Pennington.FrontMatter.IFrontMatter](https://usepennington.net/reference/api/i-front-matter.md) as a `record` and add only the capability interfaces the new keys need — `ITaggable`, `IOrderable`, `ISectionable`, `IRedirectable`. Unimplemented capabilities pick up their default-member values, so a minimal record is short.

 
```csharp:symbol
namespace DocSiteKitchenSinkExample;
  
using Pennington.FrontMatter;
  
/// <summary>
/// Custom front-matter record used by the "multiple content sources" how-to.
/// Implements the same capability interfaces as <c>DocSiteFrontMatter</c>
/// plus an API-specific <see cref="Namespace"/> and <see cref="Stability"/>
/// pair so reference pages can expose a per-API namespace and stability
/// badge.
/// </summary>
/// <remarks>
/// Kept as a standalone record so tutorials can target it with
/// <c>T:DocSiteKitchenSinkExample.ApiFrontMatter</c>. Declaring a record
/// that implements <see cref="IFrontMatter"/> with a small handful of
/// capability interfaces is the canonical "write your own front matter"
/// pattern referenced by the front-matter how-to.
/// </remarks>
public record ApiFrontMatter : IFrontMatter, ITaggable, ISectionable, IOrderable, IRedirectable
{
    public string Title { get; init; } = "";
    public string? Description { get; init; }
    public bool IsDraft { get; init; }
    public string[] Tags { get; init; } = [];
    public int Order { get; init; } = int.MaxValue;
    public string? RedirectUrl { get; init; }
    public string? SectionLabel { get; init; }
    public string? Uid { get; init; }
    public bool Search { get; init; } = true;
    public bool Llms { get; init; } = true;
  
    /// <summary>API namespace (e.g. <c>Pennington.Highlighting</c>).</summary>
    public string? Namespace { get; init; }
  
    /// <summary>Stability classification — <c>stable</c>, <c>preview</c>, or <c>experimental</c>.</summary>
    public string Stability { get; init; } = "stable";
}
```

 
Property names map to YAML keys under `CamelCaseNamingConvention` — `Namespace` reads `namespace:`; `Stability` reads `stability:`. Unknown keys are dropped with a warning in lenient mode (dev) and rejected in strict mode (the build default), so a typo on a custom key is flagged — as a dev warning or a build failure — rather than silently taking effect as a default.

 
## Register the record

 
Pass the record type to `AddMarkdownContent<T>` so the pipeline deserializes the YAML into that type. The configure delegate selects the content root the source reads from and the URL prefix its pages serve under. On a bare host this call goes inside the `AddPennington` lambda; on a DocSite or BlogSite host, chain it through the `ConfigurePennington` escape hatch so the extra source sits alongside the template's own. The kitchen-sink example registers its `ApiFrontMatter` source this way:

 
```csharp:symbol,bodyonly
// DocSite's default source serves all of Content/ at /. Carve out the
// symbols subtree so the custom-typed source below owns it without an
// overlap warning.
penn.MarkdownSources[0].ExcludePaths = ["symbols"];
  
penn.AddMarkdownContent<ApiFrontMatter>(o =>
{
    o.ContentPath = "Content/symbols";
    o.BasePageUrl = "/symbols";
    o.SectionLabel = "Symbols";
});
```

 
`ExcludePaths` on the template's own doc source carves the subtree out so exactly one source owns those pages — drop that line on a bare `AddPennington` host where no template source claims the folder.

 
## Read the key in a Razor page

 
The lede promised a `stability` value — here is what consumes it. The `ApiFrontMatter` record is portable across hosts; only its registration differs, so this section uses a bare `AddPennington` host where a Razor page owns rendering. A page under the registered source authors the custom keys at the top of its YAML block:

 
```yaml
---
title: "Highlighting service"
namespace: "Pennington.Highlighting"
stability: "preview"
---
```

 
A regular Razor `@page` reads those keys by injecting `IPageResolver` and calling the generic `ResolveAsync<ApiFrontMatter>`. That overload resolves the requested URL and hands back the front matter already typed as the custom record, so `Stability` and `Namespace` are plain property reads — no cast. It returns `null` when nothing matches or the page is served by a source registered against a different front-matter type:

 
```razor:symbol
@* Catch-all that resolves a URL to a rendered page and reads the custom front
   matter directly. ResolveAsync<ApiFrontMatter> hands back the front matter
   already typed as the custom record, so Stability and Namespace are plain
   property reads — no cast, no MdazorContext dictionary. *@
  
@page "/{*Path}"
@inject IPageResolver Resolver
  
@if (_html is not null)
{
    <PageTitle>@_title</PageTitle>
    <article>
        <h1>@_title</h1>
        @if (_stability is not null)
        {
            <p>Stability: <strong>@_stability</strong> · Namespace: <code>@_namespace</code></p>
        }
        @((MarkupString)_html)
    </article>
}
else
{
    <PageTitle>Not found</PageTitle>
    <p>No content matches @Path.</p>
}
  
@code {
    [Parameter] public string? Path { get; set; }
  
    private string? _title;
    private string? _html;
    private string? _stability;
    private string? _namespace;
  
    protected override async Task OnInitializedAsync()
    {
        var requested = new UrlPath(Path ?? string.Empty).EnsureLeadingSlash();
  
        // The generic overload resolves the page and narrows its front matter to
        // ApiFrontMatter in one step. It returns null when nothing matches or the
        // page is served by a source registered against a different type.
        if (await Resolver.ResolveAsync<ApiFrontMatter>(requested) is { } page)
        {
            _title = page.Metadata.Title;
            _stability = page.Metadata.Stability;
            _namespace = page.Metadata.Namespace;
            _html = page.Content.Html;
        }
    }
}
```

 
Any page served by the `ApiFrontMatter` source now surfaces its typed keys — the round-trip from YAML to a strongly-typed Razor page.

 
## Verify

 
 - In `examples/CustomFrontMatterRazorPageExample`, run `dotnet run` and visit `/symbols/highlighting-service/`. The page renders `Stability: preview` from that page's `stability:` key — proof the YAML deserialized into the typed `ApiFrontMatter.Stability` property and `ResolveAsync<ApiFrontMatter>` returned the typed page.
 - The build report contains no `FrontMatterParseError` diagnostics for pages under the new source.
 
 
## Related

 
 - Reference: [Front matter key reference](https://usepennington.net/reference/front-matter/keys.md) — every built-in key, type, and default
 - Reference: [Built-in front-matter types](https://usepennington.net/reference/api/doc-front-matter.md) — `DocFrontMatter`, `BlogFrontMatter`, `DocSiteFrontMatter`, `BlogSiteFrontMatter`
 - Reference: [IFrontMatter and capability defaults](https://usepennington.net/reference/api/i-front-matter.md) — the capability interfaces available to a custom record
 - Background: [The front-matter capability system](https://usepennington.net/explanation/core/front-matter-capabilities.md) — why the design collapsed ten interfaces into default members
 - How-to: [Use multiple content sources](https://usepennington.net/how-to/discovery/multiple-sources.md) — chain a second `AddMarkdownContent<T>` against a custom record
 
 
[Next
                    
                Mark drafts, schedule posts, tag pages, and control sort order](https://usepennington.net/how-to/pages/drafts-tags-ordering.md)