This documentation is also published as Markdown for efficient machine reading: the whole site is indexed at /llms.txt, and every page has a clean Markdown copy under /_llms/. These are generated from the same source and cost far fewer tokens to read than this rendered HTML.

Skip to main content Skip to navigation
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; for the design rationale behind the capability interfaces, see The front-matter capability system.

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 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).

Declare the record

Implement Pennington.FrontMatter.IFrontMatter 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
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 CamelCaseNamingConventionNamespace 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
// 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
@* 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.