Scaffold a blog with BlogSite
Swap the bare Pennington host for the BlogSite template and configure the core options that drive the home, archive, post, tag, and RSS routes.
By the end of this tutorial, a running BlogSite host titled "Scaffold Blog" serves a home listing, /archive, /blog/<slug>/, /tags/, /tags/<name>/, and /rss.xml — all from a single placeholder post under Content/Blog/.
AddBlogSite folds the host, layout, navigation, and styling into one call, configured for a site where the blog is the site; for what the template wires, where the wiring stops, and why DocSite and BlogSite can't share an app, read what the templates wire for you first.
Prerequisites
- .NET 10 SDK installed
- Completed Create your first Pennington site
- Completed Serve markdown through a Blazor catch-all (so
Content/already has at least one markdown file)
The stable .NET 10 SDK is all BlogSite needs — its package targets .NET 10, and you never write the union keyword that would call for a preview SDK. See the SDK and the union shim for when the .NET 11 beta is worth opting into.
The finished code for this tutorial lives in examples/BlogSiteScaffoldExample.
1. Start from the bare Pennington host
The host you built in the getting-started tutorials calls AddPennington, registers content with AddMarkdownContent<DocFrontMatter>, mounts UsePennington, and routes through a Blazor @page "/{*Path}" catch-all (MarkdownPage.razor) that resolves each URL with IPageResolver to serve individual pages.
That host serves individual pages but nothing else: no home listing, no /archive, no /blog/<slug> pages, no /tags listings, no /rss.xml feed, and no MonorailCSS chrome. The next section brings all of that in with a single AddBlogSite call.
Add the first post
Posts live under {ContentRootPath}/{BlogContentPath} — Content/Blog/ with the defaults. Add one placeholder post so the host has something to serve. It uses four front-matter keys — title, description, date, and author — the minimum the home listing and RSS feed will need once the template is wired:
---
title: Hello world
description: A placeholder post so the scaffold has something to render. Tutorial 1.3.20 teaches the real BlogSiteFrontMatter fields.
date: 2026-04-13
author: Author Name
tags:
- scaffold
---
This post exists so the bare BlogSite scaffold has at least one entry on the
home page and in the RSS feed. The next tutorial, **Author your first post
with `BlogSiteFrontMatter`**, walks through the full set of post front-matter
fields (tags, series, repository, section, redirectUrl, and more).
Checkpoint
- Run
dotnet runand visit/blog/hello-worldat the URL the console prints — the page shows unstyled HTML for the markdown. None of the BlogSite chrome (home listing, archive, tag pages, RSS) exists yet.
2. Wire AddBlogSite, UseBlogSite, and RunBlogSiteAsync
AddBlogSite is the BlogSite template's single registration call; it stands in for AddPennington plus the AddMarkdownContent<DocFrontMatter> line, wiring Pennington core, MonorailCSS, the Razor chrome, and the blog content services in one call. UseBlogSite mounts the middleware stack and Razor component routes; RunBlogSiteAsync dispatches between dev-serve and static-build.
Replace Program.cs with the BlogSite calls
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddBlogSite(() => new BlogSiteOptions
{
SiteTitle = "Scaffold Blog",
SiteDescription = "A minimal BlogSite scaffold showing AddBlogSite, UseBlogSite, and RunBlogSiteAsync.",
CanonicalBaseUrl = "https://example.com",
ContentRootPath = "Content",
BlogContentPath = "Blog",
BlogBaseUrl = "/blog",
AuthorName = "Author Name",
AuthorBio = "Writing about software, tools, and the occasional side project.",
});
var app = builder.Build();
app.UseBlogSite();
await app.RunBlogSiteAsync(args);
The options populated here cover site identity (SiteTitle, SiteDescription, CanonicalBaseUrl), content paths shown at their defaults (ContentRootPath, BlogContentPath, BlogBaseUrl), and author fallbacks (AuthorName, AuthorBio). The full surface lives in Pennington.BlogSite.BlogSiteOptions.
Checkpoint
- Run
dotnet runand visit/at the URL the console prints - The BlogSite home layout appears: site title "Scaffold Blog", a recent-posts list with one entry, header chrome, and MonorailCSS styling
3. Verify every built-in route
With the post from section 1 in place and AddBlogSite wired, every route the template ships now responds. The placeholder post carries tags: [scaffold], so the tag routes have one entry to list.
Walk the page routes
With the host running, visit each of these at the URL the console prints. Every one returns 200:
/— home listing withhello-worldas the only recent post./archive— full archive, same single post in reverse-chronological order./blog/hello-world— the post itself, now rendered with BlogSite chrome./tags— the tag index, showingscaffoldwith a count of one./tags/scaffold— the per-tag listing with the one post.
Check the RSS feed
Visit /rss.xml. It returns application/rss+xml with one <item> carrying the post title, link, description, pub date, and author.
The full route surface, including the paginated /archive/page/{n} and per-tag pages, is cataloged in Built-in BlogSite routes. The next tutorial expands the post to the full BlogSiteFrontMatter surface, adding tags, series, repository, sectionLabel, and redirectUrl.
Checkpoint
- Each page route above returns 200 and renders the placeholder post's metadata
/rss.xmlreturnsapplication/rss+xmlcontent with one item whose<guid>matches the canonical post URL
Summary
- The bare
AddPenningtonhost gave way toAddBlogSite+UseBlogSite+RunBlogSiteAsync, and the full BlogSite chrome now renders. - The core
BlogSiteOptionssurface —SiteTitle,SiteDescription,CanonicalBaseUrl,ContentRootPath,BlogContentPath,BlogBaseUrl,AuthorName,AuthorBio— is populated, and each field flows through to the rendered output. - BlogSite binds posts through
AddMarkdownContent<BlogSiteFrontMatter>(introduced in the next tutorial) and defaults content paths toContent/Blogserved at/blog, which distinguishes it from theDocSitetemplate's area-driven layout. - Every built-in route the template ships responds:
/,/archive,/blog/<slug>,/tags,/tags/<name>, and/rss.xml.