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
Getting Started

Organize content with sections and areas

Split a DocSite's Content/ folder into two top-level areas with subfolder-driven sections, and use staggered order: values so the sidebar groups in the order you expect.

By the end of this tutorial the DocSite runs at http://localhost:5000 with an area selector showing Guides and Reference. Each area renders its own grouped sidebar: Getting Started and Advanced under Guides, Core API and Extensions under Reference, with pages sorted by order: inside each group. For the algorithm behind the sidebar, see Why the sidebar mirrors your folders.

Prerequisites

The finished code for this tutorial lives in examples/DocSiteSectionsExample.


1. Register two areas and start from a flat page

So far Content/ is area-free — every page shares one sidebar tree. This tutorial splits that into two switchable tabs, Guides and Reference, then fills each with grouped sections. To watch the grouping build up from nothing, reset the Guides area to a single flat page first: delete the configure.md and hub index.md you added in Add doc pages and link between them, leaving just install.md. Then register the areas and strip install.md back to minimal front matter, so the sidebar starts as a single ungrouped entry before any sections appear.

1

Register two content areas in Program.cs

Add two ContentArea entries — Guides bound to guides/ and Reference bound to reference/. Each binds a top-level folder under Content/ to its own sidebar tab, and the selector appears once more than one area is configured. Every other change in this tutorial is a filesystem change under Content/.

csharp
using Pennington.DocSite;
  
var builder = WebApplication.CreateBuilder(args);
  
// Same DocSite host shape as apps #4 and #5 — the focus here is on the
// *structure* of `Content/`. Two areas, each broken into two subfolder-backed
// sections. `NavigationBuilder` (inside `MainLayout`) turns the discovered
// flat TOC list into a grouped sidebar: each subfolder under an area becomes
// a non-navigable section header, and the pages inside sort by their front
// matter `order:` (tiebreaker: title).
builder.Services.AddDocSite(() => new DocSiteOptions
{
    SiteTitle = "Sections Docs",
    SiteDescription = "Structure Content/ into areas and sections using subfolders, section, and order front matter.",
    GitHubUrl = "https://github.com/usepennington/pennington",
    HeaderContent = """<a href="/">Sections Docs</a>""",
    FooterContent = """<footer class="mt-16 py-8 text-center text-sm text-base-500">Built with Pennington DocSite.</footer>""",
  
    // Two areas bound to two top-level content folders. The sidebar renders
    // an area selector above the per-area TOC; each area's TOC is grouped
    // by subfolder (sections "Getting Started" / "Advanced" under guides,
    // "Core Api" / "Extensions" under reference).
    Areas =
    [
        new ContentArea("Guides", "guides"),
        new ContentArea("Reference", "reference"),
    ],
});
  
var app = builder.Build();
  
app.UseDocSite();
  
await app.RunDocSiteAsync(args);
2

Strip Content/guides/install.md back to minimal front matter

Leave Content/guides/install.md with just a title: and a description: — drop the order:, uid:, and the Next footer from the earlier tutorial so the page starts as a bare entry.

markdown
---
title: Install Pennington
description: Add the Pennington package to a new or existing ASP.NET project.
---
  
The first thing every new Pennington site needs is the package itself.

With no subfolder, the page is a single ungrouped entry directly under the Guides area — there is no section header because there is no folder to title-case into one. A missing order: defaults to int.MaxValue, so an un-ordered page sorts after any page with an explicit order: — but here it is the only page, so it just appears on its own.

Checkpoint

The sidebar shows the page directly, with no section header above it.

  • Run dotnet run from your project and visit http://localhost:5000/guides/install
  • The Guides sidebar shows the Install Pennington link directly under the area with no section header above it

2. Move the page into a subfolder to create a section

Now let's move the same page under a getting-started/ subfolder and add sectionLabel: plus order: to the front matter. The sidebar gains its first grouped section header.

1

Move install.md under Content/guides/getting-started/

Delete Content/guides/install.md and create Content/guides/getting-started/installation.md in its place. The subfolder name is what creates the sidebar section header — Pennington title-cases the folder (getting-startedGetting Started) and renders it as a non-navigable group label.

Moving the file changes its URL. Routes preserve subfolders, so the page no longer serves at /guides/install — it now serves at /guides/getting-started/installation/, mirroring its path under Content/. The folder you added for grouping also became a URL segment.

2

Add sectionLabel: Getting Started and order: 10 to the front matter

markdown
---
title: Install Pennington
description: Add the Pennington package to a new or existing ASP.NET project.
sectionLabel: Getting Started 
order: 10 
---
  
The first thing every new Pennington site needs is the package itself.

order: sorts pages within the section (smaller first). sectionLabel: surfaces in breadcrumbs and prev/next chrome.

Checkpoint

  • The old http://localhost:5000/guides/install URL now 404s — the page moved. Visit http://localhost:5000/guides/getting-started/installation/ instead
  • The Guides sidebar shows a non-navigable Getting Started header with the Install Pennington link indented under it
  • The breadcrumb at the top of the article reads Guides › Getting Started › Install Pennington

3. Fill in the rest of the Guides area

Let's add the remaining pages to getting-started/ and advanced/ so Guides has two sibling sections with staggered order: values.

1

Add two more pages to getting-started/ with order: 20 and order: 30

Add the Guides landing page and two more pages to the getting-started/ subfolder. Give first-project.md an order: of 20 and configuration.md an order: of 30. Each page also carries sectionLabel: Getting Started.

markdown
---
title: Guides
description: Walk-throughs for getting productive with Pennington, grouped by skill level.
sectionLabel: Guides
order: 0
---
  
Welcome to the **Guides** area. The sidebar groups every page in this area by
the subfolder it lives in — *Getting Started* collects the first three pages
you want to read, *Advanced* holds the deeper material. Within each group,
pages are ordered by the `order:` front-matter key.
  
Pick a page from the sidebar to jump in.
markdown
---
title: Create your first project
description: Wire AddPennington and UsePennington into Program.cs and drop in a markdown page.
sectionLabel: Getting Started
order: 20
---
  
With the package installed, the smallest useful site is two lines of
registration and one markdown file on disk.
  
## Program.cs
  
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddPennington();
  
var app = builder.Build();
app.UsePennington();
await app.RunOrBuildAsync(args);
```
  
## Drop in a page
  
Create `Content/index.md` with a `title:` front-matter key and a body. Run
`dotnet run` and the page is served from `/` with hot reload on file change.
  
The next guide covers the handful of options you will reach for first.
markdown
---
title: Configure your site
description: Tour the PenningtonOptions knobs you will reach for on day one.
sectionLabel: Getting Started
order: 30
---
  
`AddPennington` accepts a configuration callback that lets you tune the
content root, URL style, and a handful of feature toggles without leaving
`Program.cs`.
  
## Point at a content folder
  
By default Pennington reads from `Content/` next to your project. Override
the path when your content lives elsewhere in the repo or ships from an
embedded resource.
  
## Change the URL style
  
`LowercaseUrls` and `AppendTrailingSlash` control the shape of every
generated link. Pick a style early — changing it later invalidates existing
inbound links unless you pair the change with redirect front matter.
  
The next area — *Advanced* — covers layout overrides and the response
pipeline.

The 10/20/30 spacing leaves room to drop pages in later without renumbering. The minimum order: value in the section is 10 — that matters in the next step.

2

Add the advanced/ section with order: 40 and order: 50

Create Content/guides/advanced/ and add two pages with sectionLabel: Advanced and order: values of 40 and 50.

markdown
---
title: Swap in a custom layout
description: Override the default DocSite layout with your own Razor component.
sectionLabel: Advanced
order: 40
---
  
DocSite ships with a sensible default layout, but every surface is a plain
Razor component. When you need a different header, footer, or body grid,
register your layout after `AddDocSite` and it takes precedence.
  
## Replace the main layout
  
Pass `AdditionalRoutingAssemblies` a reference to the assembly that holds
your `MyLayout.razor`, then mark it with the same `@layout`/`@inherits`
shape DocSite's `MainLayout` uses.
  
## Keep the sidebar or write your own
  
The easiest path is to keep `AreaNavigation` and `TableOfContentsNavigation`
inside your custom layout so sidebar grouping still works exactly as this
tutorial describes. The harder path — writing a bespoke nav — is covered
in a later how-to.
markdown
---
title: Hook into the response pipeline
description: Intercept rendered HTML before it reaches the browser with IResponseProcessor.
sectionLabel: Advanced
order: 50
---
  
Every rendered page flows through a response pipeline before hitting the
wire. Register an `IResponseProcessor` to mutate the HTML — add a feedback
widget, rewrite anchor IDs, or inject analytics — without touching the
markdown.
  
## Register a processor
  
```csharp
builder.Services.AddSingleton<IResponseProcessor, MyProcessor>();
```
  
Processors run in `Order` ascending. Override `ShouldProcess` to scope the
work to particular routes, content types, or request metadata.
  
## Where this fits vs islands
  
Response processors mutate the already-rendered HTML server-side, before it
reaches the browser. Islands — the SPA engine's `data-spa-region` blocks —
mark which server-rendered regions swap in on in-site navigation; Pennington
renders them on the server with no client hydration. Reach for a processor to
change the HTML every page ships; for interactive client behavior, ship your
own client-side script that enhances the rendered HTML.

Section headers inherit the minimum order: of their pages. Leaving gaps between section order ranges — getting-started/ at 10/20/30, advanced/ at 40/50 — keeps Getting Started above Advanced without relying on alphabetical tie-breaks.

Checkpoint

  • Revisit http://localhost:5000/guides/getting-started/installation/
  • The Guides sidebar shows, top to bottom: Getting Started (with Install Pennington, Create your first project, Configure your site) then Advanced (with Custom layouts, The response pipeline)
  • Click around — breadcrumbs and prev/next labels reflect the sectionLabel: on each page

4. Populate the Reference area to confirm it repeats the pattern

The same subfolder-plus-staggered-order pattern applies to the Reference area. Switching between both areas through the sidebar's area selector confirms each gets its own independent sidebar tree.

1

Fill in Content/reference/core-api/ with order: 10 and order: 20

Create the core-api/ subfolder under Content/reference/ and add two pages, each with sectionLabel: Core API and order: values of 10 and 20. The folder creates the section, the key labels it, and the staggered numbers keep sibling sections predictable.

markdown
---
title: Reference
description: API surface for the Pennington library, split into core and extension packages.
sectionLabel: Reference
order: 0
---
  
The **Reference** area mirrors the package structure: *Core API* covers the
types in `Pennington` itself, *Extensions* covers the optional surface
(Markdig extensions, content services). Page order inside each section
matches the order you are likely to discover the types while building a
site.
  
Pick a page from the sidebar to inspect a specific surface.
markdown
---
title: PenningtonOptions
description: Core configuration surface handed to AddPennington.
sectionLabel: Core API
order: 10
---
  
`PenningtonOptions` is the record the configuration callback on
`AddPennington` mutates. It holds a handful of knobs that apply to every
page in the site regardless of content source.
  
## Keys worth knowing
  
- `ContentRootPath` — folder (or embedded-resource root) content is
  discovered from.
- `LowercaseUrls` — whether to force every generated URL to lowercase.
- `AppendTrailingSlash` — pick slash vs no-slash for clean URLs.
- `UrlStyle``Clean` (folder/index.html) or `Extension` (filename.html).
  
Every option has a sensible default; populate only the ones you need to
deviate from.
markdown
---
title: ContentPipeline
description: The discovery/parse/render pipeline every content source flows through.
sectionLabel: Core API
order: 20
---
  
`ContentPipeline` is the assembly line `IContentService` implementations
feed into. Each source yields `DiscoveredItem`s, the pipeline parses them
into `ParsedItem`s via `IContentParser`, and finally renders them into
`RenderedItem`s via `IContentRenderer`.
  
## The three stages
  
1. **Discover** — each registered `IContentService` walks its source (disk,
   Razor pages, a JSON feed, whatever) and emits `DiscoveredItem` unions.
2. **Parse** — the matching `IContentParser` reads the item, separates
   front matter from body, and produces a `ParsedItem`.
3. **Render**`IContentRenderer` turns the parsed body into HTML (plus
   an outline of headings and any diagnostics).
  
Custom parsers and renderers plug into the same pipeline — see the
*Extensions* section in the sidebar for how to write one.
2

Add Content/reference/extensions/ with order: 30 and order: 40

Create extensions/ and drop two pages in it with sectionLabel: Extensions and order: values of 30 and 40. Continuing the count rather than restarting at 10 keeps the section order ranges separated — Core API at 10/20, Extensions at 30/40 — so Core API sorts above Extensions by the same minimum-order: rule from unit 3.

markdown
---
title: Markdown extensions
description: The Markdig extensions Pennington ships with — alerts, tabbed code, highlighting.
sectionLabel: Extensions
order: 30
---
  
Pennington configures Markdig with a curated set of extensions that light
up the authoring syntax the tutorials lean on.
  
## What ships in the box
  
- **Alerts** — GitHub-flavoured block quotes (`> [!NOTE]`, `TIP`,
  `IMPORTANT`, `WARNING`, `CAUTION`).
- **Tabbed code groups** — two or more adjacent fenced blocks with
  `tabs=true title="…"`.
- **Syntax highlighting** — TextMate grammars and ANSI shell output.
- **Code annotations** — trailing-comment `[!code highlight]` markers.
  
Registering your own extension is covered in the *Hook into the response
pipeline* guide's companion how-to.
markdown
---
title: Custom content services
description: Teach Pennington a new content source by implementing IContentService.
sectionLabel: Extensions
order: 40
---
  
`IContentService` is the extension point for loading content from anything
that isn't plain markdown — a JSON feed, a database, a remote API, an
embedded resource. Register an implementation and the pipeline treats its
items exactly like every other source.
  
## The four methods
  
- `DiscoverAsync()` — yield a `DiscoveredItem` per logical page.
- `GetContentTocEntriesAsync()` — flat list of TOC entries with title,
  order, and hierarchy parts.
- `GetCrossReferencesAsync()` — any `uid`-to-route mappings you want the
  xref resolver to see.
- `GetContentToCopyAsync()` — assets the static-build step should copy
  alongside the rendered HTML.
  
Implementations live next to `MarkdownContentService` in the DI container
and are iterated in registration order.
3

Switch areas with the sidebar's area selector

Click the area selector pill at the top of the sidebar — the control that toggles between Guides and Reference. Each area has its own independent sidebar tree. The ContentArea bindings from Program.cs plus the subfolder layout are what make this work, with no extra code.

Checkpoint

  • With the host running, visit http://localhost:5000/reference/core-api/pennington-options
  • The sidebar shows Core API above Extensions, with two pages under each in order: sequence
  • Click the area selector to Guides — the sidebar replaces itself with the Getting Started / Advanced groups from unit 3
  • The area selector tracks the current area as navigation moves between pages

Summary

  • A DocSite's Content/ folder splits into multiple ContentArea entries, and each one gets its own sidebar tree.
  • The subfolder name creates the sidebar sectionsectionLabel: is metadata for breadcrumbs and prev/next labels, not a grouper.
  • Staggered order: values across sibling sections (10/20/30 for one, 40/50 for the next) sort section headers in the intended order, without relying on alphabetical tie-breaks between folder names.
  • The shape of the generated sidebar is predictable from the shape of the Content/ folder before running the site.