Organize content with sections and areas
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.
The tutorial shows how a top-level folder under Content/ becomes a ContentArea, how each subfolder under an area becomes a sidebar section node driven by the folder name rather than by sectionLabel:, and how staggering order: numbers across sibling sections controls which section header appears first.
Prerequisites
- .NET 11 SDK installed
- Completed Scaffold a documentation site with DocSite (provides the single-area host shape this tutorial extends)
- Completed Author a documentation page with DocFrontMatter (so
DocSiteFrontMatterkeys likesectionLabel:andorder:are already familiar)
The finished code for this tutorial lives in examples/DocSiteSectionsExample.
1. Start from a flat area and see the limit
Let's begin with a single page parked directly under an area folder — no subfolder, no sectionLabel:, no order: — so the sidebar has nothing to group. This establishes the baseline the rest of the tutorial builds on.
- 1
Confirm the two-area host from the scaffolding tutorial
The
Program.csfile wires up twoContentAreaentries:Guidesbound to theguidesfolder andReferencebound to thereferencefolder.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", Description = "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);This file stays untouched for the rest of the tutorial. Every change from here on is a filesystem change under
Content/. The area selector at the top-left of the sidebar renders automatically throughMainLayoutwheneverDocSiteOptions.Areascontains more than one entry — no extra code required.The two
ContentAreaconstructors take a label shown in the area selector, followed by the folder name underContent/.AddDocSitediscovers both folders through a single markdown pipeline. - 2
Drop a single page into
Content/guides/with no section or orderCreate
Content/guides/install.mdwith minimal front matter — atitle:and adescription:, nothing else.""" --- title: Install Pennington description: Add the Pennington package to a new or existing ASP.NET project. --- # Install Pennington The first thing every new Pennington site needs is the package itself. """Paste the YAML-plus-markdown content above into
Content/guides/install.md. With no subfolder and noorder:, the page sorts to the top of the Guides area as a flat entry. Theorderkey defaults toint.MaxValueand there is no sibling subfolder for the navigation builder to fold the page under. The next unit fixes that.
Checkpoint — A single ungrouped entry under Guides
The sidebar shows the page directly, with no section header above it.
- Run
dotnet runfromexamples/DocSiteSectionsExample - Visit
http://localhost:5000/guides/install - The Guides sidebar shows the Install Pennington link at the top of 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.mdunderContent/guides/getting-started/Delete
Content/guides/install.mdand createContent/guides/getting-started/installation.mdin its place.The load-bearing rule: the subfolder name is what creates the sidebar section, not the
sectionLabel:key.NavigationBuildertitle-cases the folder name (getting-startedbecomes Getting Started) and renders it as a non-navigable header above the page links. - 2
Add
sectionLabel: Getting Startedandorder: 10to the front matter""" --- title: Install Pennington description: Add the Pennington package to a new or existing ASP.NET project. sectionLabel: Getting Started order: 10 --- # Install Pennington The first thing every new Pennington site needs is the package itself. """The two keys serve different purposes.
order:is an integer that sorts pages inside a section — smaller numbers appear first, with ties broken alphabetically on title.sectionLabel:is metadata carried onNavigationInfo.SectionNameand shown in breadcrumbs and prev/next chrome. When a file lives outside a subfolder,sectionLabel:has no grouping effect — it is a label, not a grouper.One section, one subfolder.
sectionLabel:names it in breadcrumbs.
Checkpoint — One grouped section under Guides
- Reload
http://localhost:5000/guides/installation - 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 — the pattern that prevents the tie-break surprise.
- 1
Add two more pages to
getting-started/withorder: 20andorder: 30Add the Guides landing page and two more pages to the
getting-started/subfolder. Givefirst-project.mdanorder:of20andconfiguration.mdanorder:of30. Each page also carriessectionLabel: Getting Started.--- title: Guides description: Walk-throughs for getting productive with Pennington, grouped by skill level. sectionLabel: Guides order: 0 --- # Guides 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.--- title: Create your first project description: Wire AddPennington and UsePennington into Program.cs and drop in a markdown page. sectionLabel: Getting Started order: 20 --- # Create your first project 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.--- title: Configure your site description: Tour the PenningtonOptions knobs you will reach for on day one. sectionLabel: Getting Started order: 30 --- # Configure your site `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 sequence is deliberate — it leaves room to insert pages later without renumbering everything. The minimum
order:value in this section is10, which matters in the next step. - 2
Add the
advanced/section withorder: 40andorder: 50Create
Content/guides/advanced/and add two pages withsectionLabel: Advancedandorder:values of40and50.--- title: Swap in a custom layout description: Override the default DocSite layout with your own Razor component. sectionLabel: Advanced order: 40 --- # Swap in a custom layout 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.--- title: Hook into the response pipeline description: Intercept rendered HTML before it reaches the browser with IResponseProcessor. sectionLabel: Advanced order: 50 --- # Hook into the response pipeline 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 page; islands let you hand a region of the DOM off to a Razor component that rehydrates on the client. Reach for a processor when the change is purely HTML; reach for an island when you need interactive state.Stagger
order:values across sibling sections — 10/20/30 insidegetting-started/and 40/50 insideadvanced/— so the two section headers sort in the intended order. When both sections start at10, the navigation builder falls back to alphabetical ordering of the folder names, andadvanced/appears above Getting Started.
Checkpoint — Two sections under Guides, in the intended order
- Revisit
http://localhost:5000/guides/installation - The Guides sidebar shows, top to bottom: Getting Started (with Install Pennington, Create your first project, Configure Pennington) 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/withorder: 10andorder: 20Create the
core-api/subfolder underContent/reference/and add two pages, each withsectionLabel: Core APIandorder:values of10and20. The folder creates the section, the key labels it, and the staggered numbers keep sibling sections predictable.--- title: Reference description: API surface for the Pennington library, split into core and extension packages. sectionLabel: Reference order: 0 --- # Reference 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.--- title: PenningtonOptions description: Core configuration surface handed to AddPennington. sectionLabel: Core API order: 10 --- # PenningtonOptions `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.--- title: ContentPipeline description: The discovery/parse/render pipeline every content source flows through. sectionLabel: Core API order: 20 --- # ContentPipeline `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/withorder: 30andorder: 40Create
extensions/and drop two pages in it withsectionLabel: Extensionsandorder:values of30and40. Using30/40rather than restarting at10applies the same staggering rule from unit 3 — the Core API minimum is10and the Extensions minimum is30, so the sections sort Core API → Extensions without relying on the alphabetical tie-break.--- title: Markdown extensions description: The Markdig extensions Pennington ships with — alerts, tabbed code, highlighting. sectionLabel: Extensions order: 30 --- # Markdown extensions 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, ANSI shell output, and optional Roslyn-backed highlighting. -** Code annotations** — `//` inline markers. Registering your own extension is covered in the *Hook into the response pipeline* guide's companion how-to.--- title: Custom content services description: Teach Pennington a new content source by implementing IContentService. sectionLabel: Extensions order: 40 --- # Custom content services `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
ContentAreabindings fromProgram.csplus the subfolder layout are what make this work, with no extra code.
Checkpoint — Both areas render correctly, independently
- 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 multipleContentAreaentries, and each one gets its own sidebar tree. - The subfolder name creates the sidebar section —
sectionLabel: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.