This site provides a machine-readable index at /llms.txt.

Skip to main content Skip to navigation

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

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. 1

    Confirm the two-area host from the scaffolding tutorial

    The Program.cs file wires up two ContentArea entries: Guides bound to the guides folder and Reference bound to the reference folder.

    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 through MainLayout whenever DocSiteOptions.Areas contains more than one entry — no extra code required.

    The two ContentArea constructors take a label shown in the area selector, followed by the folder name under Content/. AddDocSite discovers both folders through a single markdown pipeline.

  2. 2

    Drop a single page into Content/guides/ with no section or order

    Create Content/guides/install.md with minimal front matter — a title: and a description:, 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 no order:, the page sorts to the top of the Guides area as a flat entry. The order key defaults to int.MaxValue and 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 run from examples/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. 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 load-bearing rule: the subfolder name is what creates the sidebar section, not the sectionLabel: key. NavigationBuilder title-cases the folder name (getting-started becomes Getting Started) and renders it as a non-navigable header above the page links.

  2. 2

    Add sectionLabel: Getting Started and order: 10 to 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 on NavigationInfo.SectionName and 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. 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.

    ---
    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 is 10, which matters in the next step.

  2. 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.

    ---
    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 inside getting-started/ and 40/50 inside advanced/ — so the two section headers sort in the intended order. When both sections start at 10, the navigation builder falls back to alphabetical ordering of the folder names, and advanced/ 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. 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.

    ---
    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. 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. Using 30/40 rather than restarting at 10 applies the same staggering rule from unit 3 — the Core API minimum is 10 and the Extensions minimum is 30, 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. 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 — 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 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.