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

Customize the DocSite chrome through DocSiteOptions

Use DocSiteOptions to inject head content, append CSS, replace the header/footer HTML, and route extra @page components without forking the template.

To replace the bundled DocSite header or footer (or inject head tags, append CSS, route additional @page components) without forking the template, populate the four extension points — the slot seams — on DocSiteOptions. The bundled layout, content pipeline, SPA navigation, and MonorailCSS wiring keep working. To rearrange the layout shell fundamentally, read What the DocSite and BlogSite templates wire for you before deciding whether AddDocSite is still the right starting point.

Before you begin

  • An existing Pennington site wired through AddDocSite(...) (see Scaffold a documentation site with DocSite if not).
  • All edits go in the DocSiteOptions factory passed to AddDocSite, not the DocSite source.
  • These extension points are set at host-build time — changes take effect on the next dotnet run, whose source watch reloads them.

For a working setup, see examples/DocSiteChromeOverridesExample. SiteChromeOverrides.cs returns a populated DocSiteOptions exercising all four extension points, Components/ExtraHeadFragment.razor backs the head-slot fragment, and Components/ExtraPage.razor is the routed @page component showing that AdditionalRoutingAssemblies widened the router. Program.cs runs the DocSite end-to-end against those overrides.

Build the populated options

All the code for this recipe lives in one factory method, so the four extension points sit together on a single record initializer. The example sets SiteTitle and SiteDescription alongside the override properties, matching the options produced by AddDocSite(() => SiteChromeOverrides.BuildDocSiteOptions()).

csharp
public static DocSiteOptions BuildDocSiteOptions() => new()
{
    SiteTitle = "DocSite Chrome Overrides",
    SiteDescription = "Running DocSite that exercises every override seam on DocSiteOptions.",
    HeaderContent = """<span class="chrome-header" data-chrome-overrides="docsite-header">Chrome Overrides</span>""",
    FooterContent = """<span class="chrome-footer" data-chrome-overrides="docsite-footer">(c) 2026 Pennington</span>""",
    AdditionalHtmlHeadContent = BuildHtmlHeadContent(),
    ExtraStyles = BuildExtraStyles(),
    AdditionalRoutingAssemblies = BuildAdditionalRoutingAssemblies(),
    Areas =
    [
        new ContentArea("Guides", "guides"),
    ],
};

Inject tags into <head> via AdditionalHtmlHeadContent

AdditionalHtmlHeadContent is a raw HTML string rendered inside every page's <head>, making it the right place for meta tags, preconnect hints, analytics snippets, and font <link> elements that MonorailCSS does not know about. To author the fragment as a Razor component instead, render it with ToHtmlString() once at startup and pass the resulting string — the example pairs SiteChromeOverrides.BuildHtmlHeadContent with Components/ExtraHeadFragment.razor so both approaches sit side by side.

Use this string for static site-wide markup you do not want to write a class for; reach for an IHeadContributor instead when the tag must deduplicate against another writer, order against site or page defaults, or be computed per-page. Both routes flow through the same head reconciler, so either way the tags get a data-head stamp and survive SPA navigation.

csharp
=> """
<meta name="x-chrome-overrides-head" content="extra-head-fragment">
<link rel="preconnect" href="https://example.com">
"""

Prepend rules to the generated stylesheet via ExtraStyles

ExtraStyles is a CSS string emitted above the MonorailCSS-generated rules inside /styles.css, making it the right home for @font-face declarations, custom-property overrides, and any selector the utility-class scanner will not discover on its own. Keep this string small — anything expressible as MonorailCSS utilities in Razor markup gets picked up automatically by the MonorailCss.Discovery pipeline.

csharp
=> """
.chrome-header { font-weight: 600; color: var(--color-primary-700); }
.chrome-footer { font-size: 0.875rem; color: var(--color-base-500); }
"""

HeaderContent owns the entire header brand area: the default document icon and the <a href="/">SiteTitle</a> link both step aside, so you control that region outright while the rest of the header chrome (search button, theme toggle, repo link) keeps rendering around it. FooterContent is what the layout drops into the footer region. Both accept either a raw HTML string or a RenderFragment — assign a string for inline markup, or point them at a RenderFragment (for example a static fragment defined in a .razor) for a component-authored header, no AdditionalRoutingAssemblies entry required.

csharp
var options = new DocSiteOptions
{
    HeaderContent = """<span class="chrome-header" data-chrome-overrides="docsite-header">Chrome Overrides</span>""",
    FooterContent = """<span class="chrome-footer" data-chrome-overrides="docsite-footer">(c) 2026 Pennington</span>""",
    // ...
};

The data-chrome-overrides attributes are not required by DocSiteOptions — they are markers that make the swapped-in chrome easy to spot in page source, matching what the example renders and what the Result describes below.

Route your own @page components via AdditionalRoutingAssemblies

The DocSite shell only discovers @page directives in its own assembly by default; adding the host assembly to AdditionalRoutingAssemblies makes any @page "/route" component in that assembly routable alongside the bundled pages. The example returns [typeof(SiteChromeOverrides).Assembly] so a Razor component like ExtraPage.razor sitting next to Program.cs gets picked up without any additional DI wiring.

csharp
=>
[typeof(SiteChromeOverrides).Assembly]

Register the implementation

AddDocSite takes a Func<DocSiteOptions> factory, so the most direct wiring is to pass the helper as a method reference and keep the host file short. The example's Program.cs runs this exact shape end-to-end.

csharp
using DocSiteChromeOverridesExample;
using Pennington.DocSite;
  
var builder = WebApplication.CreateBuilder(args);
  
// Live wiring referenced from step 6 of
// how-to/extensibility/override-docsite-components. The factory is a
// method reference, so the helper in SiteChromeOverrides.cs owns the
// full DocSiteOptions shape and Program.cs stays short.
builder.Services.AddDocSite(SiteChromeOverrides.BuildDocSiteOptions);
  
var app = builder.Build();
  
app.UseDocSite();
  
await app.RunDocSiteAsync(args);

Result

The chrome on every page is replaced by the configured fragments, one outcome per extension point:

  • Header and footer. The header brand area reads "Chrome Overrides" on the left, rendered as <span class="chrome-header" data-chrome-overrides="docsite-header"> in place of the default icon and <a href="/">…</a> link, with the rest of the header chrome (search, theme toggle, repo link) intact; the footer carries the matching data-chrome-overrides="docsite-footer" copyright span.
  • Head content. Every <head> gains the <meta name="x-chrome-overrides-head"> tag and the https://example.com preconnect.
  • Styles. /styles.css begins with the prepended .chrome-header / .chrome-footer rules, above the generated MonorailCSS utilities.
  • Routing. Any @page "/route" component in the host assembly (for example /extra) routes alongside the bundled DocSite pages.

Verify

  • Run dotnet run and view page source on / — expect the <meta name="x-chrome-overrides-head"> tag inside <head>, your HeaderContent and FooterContent markup in the layout, and the .chrome-header rule inside /styles.css.
  • Navigate to a route defined by a Razor component in your app assembly (for example /extra) and confirm it renders. A 404 here means AdditionalRoutingAssemblies is not including the right assembly.
  • Run dotnet run -- build output and search output/index.html for your head fragment and output/styles.css for your ExtraStyles rules to confirm the overrides survive publish.