Work with front matter
This guide covers declaring YAML front matter in a markdown file, selecting the built-in record that matches the host, and creating a custom record when the built-in types don't expose the keys needed. For the full key catalog, see Front matter key reference; for the design rationale, see The front-matter capability system.
Assumptions
- An existing Pennington site with markdown content under a
Content/folder (see the Getting Started tutorial if not). - The host template in use —
AddDocSite,AddBlogSite, or bareAddPenningtonwithAddMarkdownContent<T>. - A markdown file open and ready to fill in (or extend) its YAML block.
To copy a working setup, see examples/DocSiteKitchenSinkExample — Content/main/front-matter.md is the page this how-to is fenced from, and ApiFrontMatter.cs is the custom-record demo.
Steps
- 1
Declare the YAML block at the top of the file
Place the YAML between two
---fences as the very first content in the markdown file — before any heading.--- title: Front matter description: The YAML block at the top of every markdown page. tags: [authoring, front-matter] sectionLabel: authoring order: 20 uid: kitchen-sink.main.front-matter --- # Working with front matter Every page in this site opens with a YAML block between `---` markers. Those keys drive the sidebar title, description, tags, ordering, draft state, and cross-reference `uid`. Each built-in front-matter record maps the same keys onto a strongly-typed record. ## The built-in DocSite record The DocSite template uses `DocSiteFrontMatter` under the hood. Its fields cover the full capability surface — `Title`, `Description`, `IsDraft`, `Tags`, `Order`, `RedirectUrl`, `Section`, `Uid`, `Search`, and `Llms`. ## A custom front-matter record When you need extra fields, declare a record implementing `IFrontMatter` (plus any capability interfaces you want). This site ships an `ApiFrontMatter` record used by the API area to add `Namespace` and `Stability` fields: ```yaml --- title: Symbol reference namespace: Pennington.Search stability: preview order: 30 --- ``` Declare the record alongside your host project; Pennington discovers it by type when you call `AddMarkdownContent<ApiFrontMatter>(...)`. - 2
Pick the built-in record that matches your host
The record is determined by the host:
AddDocSitebindsDocSiteFrontMatter,AddBlogSitebindsBlogSiteFrontMatter, and bareAddPenningtonaccepts whichever type you pass toAddMarkdownContent<T>. The base doc-shaped record isDocFrontMatter:/// <summary> /// Core-library front matter for documentation pages on bare /// <see cref="Infrastructure.PenningtonExtensions.AddPennington"/> hosts. /// Implements <see cref="IFrontMatter"/>, <see cref="ITaggable"/>, <see cref="ISectionable"/>, /// and <see cref="IOrderable"/> — the default capability shape for doc content without the /// DocSite template. Hosts using <c>AddDocSite</c> bind /// <c>DocSiteFrontMatter</c> from the <c>Pennington.DocSite</c> package instead. /// </summary> public record DocFrontMatter : IFrontMatter, ITaggable, ISectionable, IOrderable { /// <inheritdoc/> public string Title { get; init; } = ""; /// <inheritdoc/> public string? Description { get; init; } /// <inheritdoc/> public bool IsDraft { get; init; } /// <inheritdoc/> public string[] Tags { get; init; } = []; /// <inheritdoc/> public string? SectionLabel { get; init; } /// <inheritdoc/> public string? Uid { get; init; } /// <inheritdoc/> public int Order { get; init; } = int.MaxValue; /// <inheritdoc/> public bool Search { get; init; } = true; /// <inheritdoc/> public bool Llms { get; init; } = true; }The blog-shaped counterpart for posts:
/// <summary> /// Core-library front matter for blog posts on bare /// <see cref="Infrastructure.PenningtonExtensions.AddPennington"/> hosts. /// Carries <see cref="Date"/>, <see cref="Author"/>, and <see cref="Series"/> alongside the /// <see cref="IFrontMatter"/> defaults, and implements <see cref="IFrontMatter"/> and /// <see cref="ITaggable"/>. Not the record bound by <c>AddBlogSite</c> — see /// <c>BlogSiteFrontMatter</c> in the <c>Pennington.BlogSite</c> package for that. /// </summary> public record BlogFrontMatter : IFrontMatter, ITaggable { /// <inheritdoc/> public string Title { get; init; } = ""; /// <inheritdoc/> public string? Description { get; init; } /// <inheritdoc/> public bool IsDraft { get; init; } /// <inheritdoc/> public string[] Tags { get; init; } = []; /// <inheritdoc/> public DateTime? Date { get; init; } /// <summary>Optional author name rendered in post bylines and feeds.</summary> public string? Author { get; init; } /// <summary>Optional series identifier used to group related posts together.</summary> public string? Series { get; init; } /// <inheritdoc/> public string? Uid { get; init; } /// <inheritdoc/> public bool Search { get; init; } = true; /// <inheritdoc/> public bool Llms { get; init; } = true; } - 3
Fill in only the keys needed
Every key on the built-in records has a default, so the YAML block can be as small as
title:plus whatever the page needs — tags, order, description, uid. The DocSite template exposes the full superset viaDocSiteFrontMatter:/// <summary> /// Front matter bound by <see cref="DocSiteServiceExtensions.AddDocSite"/>. Extends the /// <see cref="FrontMatter.DocFrontMatter"/> shape with <see cref="RedirectUrl"/> via /// <see cref="IRedirectable"/>. Implements <see cref="IFrontMatter"/>, <see cref="ITaggable"/>, /// <see cref="ISectionable"/>, <see cref="IOrderable"/>, and <see cref="IRedirectable"/>. /// </summary> public record DocSiteFrontMatter : IFrontMatter, ITaggable, ISectionable, IOrderable, IRedirectable { /// <summary>Page title rendered in the browser tab and page heading.</summary> public string Title { get; init; } = ""; /// <summary>Short description used for the meta description and social cards.</summary> public string? Description { get; init; } /// <summary>When true, the page is skipped during production builds.</summary> public bool IsDraft { get; init; } /// <summary>Tags applied to this page for filtering and the tag index.</summary> public string[] Tags { get; init; } = []; /// <summary>Sort order within the containing section. Lower values appear first.</summary> public int Order { get; init; } = int.MaxValue; /// <summary>When set, the page emits a client-side redirect to this URL instead of normal content.</summary> public string? RedirectUrl { get; init; } /// <summary>Section heading this page belongs under in navigation.</summary> public string? SectionLabel { get; init; } /// <summary>Stable identifier used for cross-references (<c>[text](xref:uid)</c>).</summary> public string? Uid { get; init; } /// <summary>When false, the page is excluded from the search index.</summary> public bool Search { get; init; } = true; /// <summary>When false, the page is excluded from the generated llms.txt output.</summary> public bool Llms { get; init; } = true; } - 4
Define a custom record for extra keys
Declare a
public recordimplementingIFrontMatterand any relevant capability interfaces —ITaggable,IOrderable,ISectionable,IRedirectable. See Pennington.FrontMatter.IFrontMatter for the full list of optional interfaces./// <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"; } - 5
Register the custom record with a markdown source
Pass the record type to
AddMarkdownContent<T>so the pipeline deserializes the YAML into that type.AddDocSiteandAddBlogSiteeach already register one source — chaining a second record requires bareAddPennington(see Use multiple content sources)./// <summary>Register a markdown content source with a specific front matter type.</summary> public MarkdownContentOptions AddMarkdownContent<TFrontMatter>(Action<MarkdownContentOptions> configure) where TFrontMatter : FrontMatter.IFrontMatter { var options = new MarkdownContentOptions { FrontMatterType = typeof(TFrontMatter) }; configure(options); _markdownSources.Add(options); return options; }
Verify
- Run
dotnet runand visit the page — the rendered<h1>matches thetitle:value. - The sidebar entry appears with the label from
title:at the position set byorder:. - When a custom record is in use, pages under its content source build without
FrontMatterParseErrordiagnostics in the build report.
Related
- Reference: Front matter key reference — every built-in key, type, and default
- Reference: Built-in front-matter types —
DocFrontMatter,BlogFrontMatter,DocSiteFrontMatter,BlogSiteFrontMatter - Reference:
IFrontMatterand capability defaults — the capability interfaces you can add to a custom record - Background: The front-matter capability system — why the design collapsed ten interfaces into default members