---
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:a3b25577e7b5b0c65f60aae73d693d35f16975ca8fe930f0e61a67258e7a31ec
tokens: 2174
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 references `examples/DocSiteKitchenSinkExample/ApiFrontMatter.cs`, which adds `namespace` and `stability` keys on top of the built-in front-matter records.

 
## 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 component

 
The lede promised a `stability` badge — here is what consumes it. 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 component renders the `stability` value by casting the page's resolved front matter to the custom record. The ambient `MdazorContext` carries the parsed front matter under the `Metadata` key as an `IFrontMatter`; casting to `ApiFrontMatter` exposes the typed `Stability` property:

 
```razor:symbol
@* StabilityBadge — reads the custom `stability` key off the page's front matter
   and renders it as a Badge. The page's parsed front matter cascades in through
   MdazorContext["Metadata"] as an IFrontMatter; casting to the custom
   ApiFrontMatter record exposes the typed Stability property. This is how a
   component consumes a key a custom front-matter record adds. Registered in
   Program.cs via AddMdazorComponent<StabilityBadge>(). *@
@using DocSiteKitchenSinkExample
@using Mdazor
@using Pennington.FrontMatter
@using Pennington.UI.Components
  
@if (Stability is not null)
{
    <Badge Text="@Stability" Variant="@Variant" />
}
  
@code {
    [CascadingParameter] public MdazorContext? Context { get; set; }
  
    // The "Metadata" key carries the page's parsed front matter. On pages served
    // by the ApiFrontMatter source it is an ApiFrontMatter, so the cast succeeds
    // and the typed Stability value round-trips from the YAML.
    private string? Stability => (Context?["Metadata"] as ApiFrontMatter)?.Stability;
  
    private string Variant => Stability switch
    {
        "stable" => "success",
        "preview" => "tip",
        "experimental" => "caution",
        _ => "note",
    };
}
```

 
Drop `<StabilityBadge />` into a markdown page served by the source and the badge renders the value the YAML supplied — the round-trip from key to component.

 
## Verify

 
 - Run `dotnet run` and visit `/symbols/highlighting-service/`. The `<StabilityBadge />` renders the literal `preview` from that page's `stability:` key — proof the YAML deserialized into the typed `ApiFrontMatter.Stability` property.
 - 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)