This site provides a machine-readable index at /llms.txt.

Skip to main content Skip to navigation

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 bare AddPennington with AddMarkdownContent<T>.
  • A markdown file open and ready to fill in (or extend) its YAML block.

To copy a working setup, see examples/DocSiteKitchenSinkExampleContent/main/front-matter.md is the page this how-to is fenced from, and ApiFrontMatter.cs is the custom-record demo.


Steps

  1. 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. 2

    Pick the built-in record that matches your host

    The record is determined by the host: AddDocSite binds DocSiteFrontMatter, AddBlogSite binds BlogSiteFrontMatter, and bare AddPennington accepts whichever type you pass to AddMarkdownContent<T>. The base doc-shaped record is DocFrontMatter:

    /// <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. 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 via DocSiteFrontMatter:

    /// <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. 4

    Define a custom record for extra keys

    Declare a public record implementing IFrontMatter and 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. 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. AddDocSite and AddBlogSite each already register one source — chaining a second record requires bare AddPennington (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 run and visit the page — the rendered <h1> matches the title: value.
  • The sidebar entry appears with the label from title: at the position set by order:.
  • When a custom record is in use, pages under its content source build without FrontMatterParseError diagnostics in the build report.