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
DocSiteOptionsfactory passed toAddDocSite, 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()).
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.
=> """
<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.
=> """
.chrome-header { font-weight: 600; color: var(--color-primary-700); }
.chrome-footer { font-size: 0.875rem; color: var(--color-base-500); }
"""
Replace the site-title link and footer with the content slots
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.
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.
=>
[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.
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 matchingdata-chrome-overrides="docsite-footer"copyright span. - Head content. Every
<head>gains the<meta name="x-chrome-overrides-head">tag and thehttps://example.compreconnect. - Styles.
/styles.cssbegins with the prepended.chrome-header/.chrome-footerrules, 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 runand view page source on/— expect the<meta name="x-chrome-overrides-head">tag inside<head>, yourHeaderContentandFooterContentmarkup in the layout, and the.chrome-headerrule 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 meansAdditionalRoutingAssembliesis not including the right assembly. - Run
dotnet run -- build outputand searchoutput/index.htmlfor your head fragment andoutput/styles.cssfor yourExtraStylesrules to confirm the overrides survive publish.
Related
- Reference: Pennington.DocSite.DocSiteOptions — the full set of properties, including
ConfigurePennington,CustomCssFrameworkSettings, and every other override point beyond the four covered here. - How-to: Add tags to the document head — the typed alternative to
AdditionalHtmlHeadContentwhen a head tag must dedup, order, or compute per-page. - How-to: Serve docs and a blog from separate content roots — register extra markdown sources through
DocSiteOptions.ConfigurePennington. - How-to: Source content from outside the markdown pipeline — register a custom
IContentServicealongside DocSite's own. - Background: What the DocSite and BlogSite templates wire for you — when forking DocSite or dropping to bare
AddPenningtonbecomes the right move. - Background: SPA navigation through region swaps —
data-spa-regionsemantics for SPA-aware layout components.