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

Serve docs and a blog from separate content roots

Register more than one markdown root — either as DocSite areas or as chained AddMarkdownContent calls on a bare Pennington host — and keep them from overlapping.

When one markdown tree needs more than one content root — a /docs/ section alongside a separate /blog/ section, or a catch-all root paired with a specialized subtree — registering multiple content sources is the answer. The right recipe depends on the host: AddDocSite supports multiple folder-scoped sub-trees through ContentArea entries on a single DocSiteFrontMatter pipeline; bare AddPennington allows any number of chained AddMarkdownContent<T> calls with independent front-matter types. For a first site, start with Serve markdown through Blazor Pages.

Before you begin

For a working DocSite multi-area setup, see examples/DocSiteKitchenSinkExample. For the bare AddPennington chained-sources recipe, see examples/MultipleSourcesExample.

Split a DocSite into areas

AddDocSite owns exactly one markdown pipeline keyed on DocSiteFrontMatter. To split that pipeline into folder-scoped sub-trees, populate DocSiteOptions.Areas with one ContentArea per slug — each slug becomes both the URL prefix and the top-level folder under ContentRootPath.

Declare the areas

Build the ContentArea list — one entry per slug, where the slug is both the URL prefix and the top-level content folder.

csharp
=>
[
new ContentArea("Main", "main"),
new ContentArea("API", "api"),
]

Wire the areas onto DocSiteOptions

Assign that list to DocSiteOptions.Areas so the single DocSiteFrontMatter pipeline discovers each folder as its own sub-tree.

csharp
=> new()
{
SiteTitle = "Kitchen Sink Docs",
SiteDescription = "A wide-surface DocSite example that backs eighteen how-to pages.",
GitHubUrl = "https://github.com/usepennington/pennington",
CanonicalBaseUrl = "https://example.com/",
HeaderContent = """<a href="/" class="font-bold">Kitchen Sink Docs</a>""",
FooterContent = BuildFooter(),
ColorScheme = BuildColorScheme(),
DisplayFontFamily = "'DocSiteKitchenSinkDisplay', system-ui, sans-serif",
BodyFontFamily = "'DocSiteKitchenSinkBody', system-ui, sans-serif",
FontPreloads = BuildFontPreloads(),
ExtraStyles = BuildExtraStyles(),
ConfigureLocalization = ConfigureLocalization,
ConfigurePennington = RegisterApiSource,
Areas = BuildAreas(),
}

Chain AddMarkdownContent on a bare host

On bare AddPennington, call AddMarkdownContent<TFrontMatter> once per source. Each call accepts its own ContentPath, BasePageUrl, and optional SectionLabel. Front-matter types can differ between sources.

Register the first source

csharp
md.ContentPath = "Content/docs";
md.BasePageUrl = "/docs";
md.SectionLabel = "Documentation";

Register a second source with a different front-matter type

csharp
md.ContentPath = "Content/blog";
md.BasePageUrl = "/blog";
md.SectionLabel = "Blog";

Carve out an overlapping subtree with ExcludePaths

When one source's ContentPath is a parent of another's, Pennington emits an overlap warning at startup because both pipelines would discover the inner tree and produce conflicting outputs. Adding ExcludePaths on the broader source gives the specialized source exclusive ownership of that subtree.

csharp
md.ContentPath = "Content";
md.BasePageUrl = "/";
md.ExcludePaths = ["blog"];

Verify

  • Run dotnet run and visit each source's BasePageUrl. Pages render under both prefixes.
  • Startup logs contain no Markdown content source rooted at '…' overlaps… warnings, or — when an overlap is intentional — the warning text names the subtree set aside for exclusion.
  • Each source's pages appear under the correct SectionLabel / ContentArea.Title in the generated navigation.