---
title: Create your first Pennington site
description: Stand up a minimal ASP.NET host that serves a single markdown page through the Pennington content pipeline.
canonical_url: https://usepennington.net/tutorials/getting-started/first-site/
sidecar_url: https://usepennington.net/tutorials/getting-started/first-site.md
content_hash: sha256:c9b9381c2217086363776eb611028bab5200c4ee63cf34f6e0ab210c8f35cb7d
tokens: 2996
uid: tutorials.getting-started.first-site
reading_time_minutes: 7
---

Getting Started
# Create your first Pennington site

Stand up a minimal ASP.NET host that serves a single markdown page through the Pennington content pipeline.

 
By the end of this tutorial a runnable ASP.NET project — `MyFirstPenningtonSite` — serves `Content/index.md` as HTML at `http://localhost:5000/`, with the front-matter `title` appearing in both the `<title>` tag and the page's `<h1>`. The next tutorial swaps the bare `MapGet` for a Blazor Server catch-all.

 
## Prerequisites

 
Pennington's published packages target .NET 10, so the stable .NET 10 SDK is all you need to build a site — no preview language flag. The .NET 11 beta SDK is an opt-in that only matters when you extend the pipeline yourself; [the SDK and the union shim](https://usepennington.net/explanation/positioning/sdk-and-the-union-shim.md) explains when that pays off.

 
 - .NET 10 SDK installed
 - A terminal and a text editor or IDE
 
 
The finished code for this tutorial lives in [examples/GettingStartedMinimalSiteExample](https://github.com/usepennington/pennington/tree/main/examples/GettingStartedMinimalSiteExample).

 
---

 
## 1. Scaffold a bare ASP.NET host

 
First, let's create the project shell Pennington will plug into — no Pennington code yet, a plain web app that returns a string, so the changes in step 2 stand out.

 
**Create the web project**

 
Run these two commands in a working folder. The `web` template produces a minimal top-level-statement `Program.cs` — no MVC, no Razor Pages — which is the starting shape we'll edit in the steps ahead.

 
```bash
dotnet new web -n MyFirstPenningtonSite
cd MyFirstPenningtonSite
```

 
**Add the Pennington package reference**

 
Add the Pennington package so the `AddPennington` extension method resolves. The backing example in this repo uses a `ProjectReference`, but for a new project this one command is enough.

 
```bash
dotnet add package Pennington
```

 
> [!IMPORTANT]
> Pennington is in alpha — check NuGet for the current prerelease and pin every `Pennington.*` package to that same version.

 
**Run the bare host**

 
The `dotnet new web` template produces a `Program.cs` with a single `MapGet` returning `"Hello World!"`. Change that string to `"Hello from ASP.NET."` — a value the template never writes, so seeing it in the browser proves you're running your own edited code and not a cached default — then run the host to confirm the shell works before Pennington takes over.

 
```bash
dotnet run --urls http://localhost:5000
```

 
> [!CHECKPOINT]
>  - `http://localhost:5000/` returns the literal text `Hello from ASP.NET.`
>  - Stop the process with `Ctrl+C` before continuing.

---

 
## 2. Register Pennington and point it at markdown

 
Now let's swap the pass-through string endpoint for the Pennington content pipeline: `AddPennington` registers the core services, `AddMarkdownContent<DocFrontMatter>` names the markdown folder (see [DocFrontMatter](https://usepennington.net/reference/api/doc-front-matter.md)), and the host gains a `ContentRootPath` it will watch for changes.

 
**Create the Content folder and an index page**

 
Create a `Content/` folder beside `Program.cs`, then add `index.md` with the contents below. Two things are required: a YAML front-matter block with a `title:` key, and a markdown body.

 
```markdown:symbol
---
title: Welcome to your first Pennington site
description: The smallest Pennington host that renders a markdown page with front matter.
---
  
This page is a single markdown file in `Content/index.md`. Its `title` in the
front matter above is what the host reads out when it renders the page.
  
## What just happened
  
1. `AddPennington` registered the content pipeline.
2. `AddMarkdownContent<DocFrontMatter>` pointed Pennington at this folder.
3. `UsePennington` wired the middleware into the request pipeline.
4. A tiny `MapGet` endpoint walks the content service, renders this file, and
   returns the HTML.
  
Everything else you see in later tutorials builds on top of these four moves.
```

 
**Wire `AddPennington` in `Program.cs`**

 
Replace the body of `Program.cs` with the service-registration block below, which walks through `WebApplication.CreateBuilder` → `AddPennington` → `AddMarkdownContent<DocFrontMatter>` → `app.Build()`. The two `using` directives at the top bring in `DocFrontMatter` and the `AddPennington` extension — keep them, or the file won't compile.

 
```csharp:symbol,bodyonly,imports
using Pennington.FrontMatter;
using Pennington.Infrastructure;
  
var builder = WebApplication.CreateBuilder(args);
  
builder.Services.AddPennington(penn => 
{ 
    penn.SiteTitle = "My First Pennington Site"; 
    penn.ContentRootPath = "Content"; 
  
    penn.AddMarkdownContent<DocFrontMatter>(md => 
    { 
        md.ContentPath = "Content"; 
        md.BasePageUrl = "/"; 
    }); 
}); 
  
var app = builder.Build();
  
await app.RunAsync();
```

 
`ContentRootPath` sets the host's base for static files; the `ContentPath` passed to `AddMarkdownContent` is where this particular markdown source reads from — both point at `"Content"` here.

 
Don't run it yet — the services are registered but nothing serves them. The middleware and the rendering endpoint go in next, and you'll run the finished host then.

 
## 3. Wire the middleware and render the page

 
Now we mount the middleware chain with `app.UsePennington()`, add a `MapGet` that hands each request to `IPageResolver`, and hand control to [RunOrBuildAsync](https://usepennington.net/reference/host/cli.md) — which uses the same app for development and static output with no code change. `IPageResolver` is the one service you need to turn a URL into a rendered page: it walks the registered content sources, parses the markdown that matches, and renders it. A Razor page would normally call it, but a `MapGet` keeps the wiring visible in one place for this tutorial.

 
**Add `UsePennington`, `RunOrBuildAsync`, and the rendering endpoint**

 
Update `Program.cs` to match the complete file below. `UsePennington` installs static files, the response-processing middleware, live reload, and auto-registered endpoints like `/sitemap.xml`; `RunOrBuildAsync` serves live when called with no args and generates static HTML when passed `-- build`; the `MapGet` asks `IPageResolver` to resolve the request to a rendered page, then returns its HTML (or a 404 when nothing matches).

 
```csharp:symbol
using Pennington.Content;
using Pennington.FrontMatter;
using Pennington.Infrastructure;
using Pennington.Routing;
  
var builder = WebApplication.CreateBuilder(args);
  
// 1. Register the Pennington content pipeline. Point ContentRootPath at the
//    folder of markdown files and declare one markdown source.
builder.Services.AddPennington(penn =>
{
    penn.SiteTitle = "My First Pennington Site";
    penn.ContentRootPath = "Content";
  
    penn.AddMarkdownContent<DocFrontMatter>(md =>
    {
        md.ContentPath = "Content";
        md.BasePageUrl = "/";
    });
});
  
var app = builder.Build();
  
// 2. Wire the Pennington middleware (static files for Content/, live-reload,
//    response processing, and auto-registered endpoints like /sitemap.xml).
app.UsePennington();
  
// 3. Serve any URL by asking IPageResolver to find the matching markdown file,
//    parse it, and render it through the pipeline. The resolver collapses the
//    discover->parse->render loop into one call; this host only decides what to
//    do with the result. In later tutorials the DocSite template provides its
//    own Razor layout and routing.
app.MapGet("/{*path}", async (string? path, IPageResolver resolver) =>
{
    var requested = new UrlPath(path ?? string.Empty).EnsureLeadingSlash();
  
    if (await resolver.ResolveAsync(requested) is not { } page)
    {
        return Results.NotFound();
    }
  
    var html = $"""
        <!DOCTYPE html>
        <html lang="en">
        <head>
          <meta charset="utf-8" />
          <title>{page.Metadata.Title}</title>
        </head>
        <body>
          <article>
            <h1>{page.Metadata.Title}</h1>
            {page.Content.Html}
          </article>
        </body>
        </html>
        """;
    return Results.Content(html, "text/html");
});
  
// 4. Dev mode (`dotnet run`) serves live; build mode
//    (`dotnet run -- build <baseUrl> <outputDir>`) crawls the running app and
//    writes static HTML. Both args are optional; defaults are `/` and `output`.
await app.RunOrBuildAsync(args);
```

 
This `MapGet` is deliberately minimal. `IPageResolver` collapses the discover → parse → render flow into one call; if you want to see the four-stage union pipeline it runs underneath, read [The content pipeline and union types](https://usepennington.net/explanation/core/content-pipeline.md). The next tutorial replaces this `MapGet` with a Blazor Server `@page` catch-all, the form a real Pennington app stays in.

 
> [!CHECKPOINT]
> That's the working site. `dotnet run --urls http://localhost:5000` serves live, and `http://localhost:5000/` returns HTML whose `<title>` element and top-level `<h1>` both read `Welcome to your first Pennington site`, pulled straight from `Content/index.md`'s front matter.
> 
>  - Run `dotnet run --urls http://localhost:5000` from the project folder.
>  - Open `http://localhost:5000/` and confirm the page title in the browser tab reads `Welcome to your first Pennington site`.
>  - View source and confirm the same string appears inside the `<title>` tag and the article's `<h1>`.

 
The rendered page is plain unstyled HTML — Times-New-Roman serif, default browser margins, blue underlined links. That is on purpose: this host wires only the content pipeline, not the CSS layer. Replacing the bare `MapGet` with a Blazor Server `@page` catch-all is the next tutorial: [Serve markdown through Blazor Pages](https://usepennington.net/tutorials/getting-started/first-page.md).

 
---

 
## 4. Verify dev-mode hot reload

 
Let's confirm that `UsePennington`'s file-watcher and live-reload WebSocket are working: with `dotnet run --urls http://localhost:5000` still serving, edit the markdown file and watch the browser reload without touching the terminal.

 
**Edit the front-matter title**

 
Leave `dotnet run --urls http://localhost:5000` serving and keep `http://localhost:5000/` open in the browser. Open `Content/index.md` and change the `title:` value to something recognizable — for example `title: Hello, Pennington` — then save. The browser tab updates on its own within a second. If it doesn't, hard-refresh once; stale HTML may be cached from before the edit.

 
> [!CHECKPOINT]
> Without any terminal input, the browser tab updates to show the new title in both the `<h1>` and the tab title. The running console logs a file-change line naming `Content/index.md`.
> 
>  - Edit `Content/index.md`'s `title:` field and save.
>  - The browser tab title and page heading update to match — no manual refresh needed.
>  - The terminal logs the change.

---

 
> [!NOTE]
> Wiring the host yourself is the normal path, and the next tutorials build straight on it. If your site is a plain documentation site, the DocSite template pre-wires this same shape — sidebar, search, Razor catch-all — in one `AddDocSite` call; [Scaffold a documentation site with DocSite](https://usepennington.net/tutorials/docsite/scaffold.md) picks up there.

 
## Summary

 
 - An ASP.NET host now serves a markdown page end-to-end through `AddPennington` and `UsePennington`.
 - The content pipeline reads from a folder of markdown through `ContentRootPath` plus `AddMarkdownContent<DocFrontMatter>`.
 - `RunOrBuildAsync` means the same host generates a static site on `dotnet run -- build` with no code change.
 - A front-matter `title:` flows from YAML into the rendered `<h1>`, and dev-mode hot reload re-renders on save.
 
 
[Next
                    
                Serve markdown through Blazor Pages](https://usepennington.net/tutorials/getting-started/first-page.md)