# Pennington > A content engine for .NET that transforms markdown into static documentation sites and blogs. Pennington is a library for building content-driven .NET websites. It provides a pipeline that discovers markdown files, parses YAML front matter, renders HTML with syntax highlighting, and generates static sites. Key features include hot reload during development, SPA-style navigation, pluggable code highlighting, and utility-first CSS via MonorailCSS. site: https://usepennington.net/ canonical: https://usepennington.net/llms.txt generated: 2026-06-16 04:19 UTC penningtonVersion: 0.1.2 ## Map - [Blog](https://usepennington.net/blog/llms.txt) (45 entries, ~10k tokens) — Posts and announcements from the site blog. - [Reference](https://usepennington.net/reference/llms.txt) (13 entries, ~30k tokens) — API surface, host extensions, front-matter keys, Markdig extensions, UI components, diagnostics codes. - [API reference](https://usepennington.net/reference/api/llms.txt) (364 entries, ~95k tokens) — Type and member reference for this library. - [Index](https://usepennington.net/_llms/index.md) ## Explanation ### Core - [The Pennington mental model](https://usepennington.net/_llms/explanation/core/mental-model.md): A short map of the host, content sources, rendering pipeline, response pipeline, and DocSite/BlogSite templates before you dive into the deeper architecture pages. - [The content pipeline and union types](https://usepennington.net/_llms/explanation/core/content-pipeline.md): Why Pennington models content as a four-case union that flows Discovered to Parsed to Rendered — with Failed as a peer case rather than an exception. - [Why ContentSource is a union](https://usepennington.net/_llms/explanation/core/content-source.md): Why the case-discriminated union — FileSource, RazorPageSource, RedirectSource, EndpointSource, LlmsOnlySource — beats the polymorphic alternatives, and why every consumer goes through `.Value`. - [Dev mode and build mode share one code path](https://usepennington.net/_llms/explanation/core/dev-vs-build.md): Why the static build is a crawler against the same ASP.NET pipeline as dev, not a second renderer — keeping dev fidelity and publish output in lockstep. - [The front-matter capability system](https://usepennington.net/_llms/explanation/core/front-matter-capabilities.md): How IFrontMatter distinguishes universal capabilities (as default members) from selective ones (as separate interfaces) — and why presence of a capability interface is a meaningful signal. - [The response-processing pipeline](https://usepennington.net/_llms/explanation/core/response-processing.md): Why Pennington splits response rewriting into generic body processors and HTML-DOM rewriters that share one AngleSharp pass. - [The head subsystem](https://usepennington.net/_llms/explanation/core/head-subsystem.md): Why everything that writes to the document head — title, canonical, JSON-LD, OpenGraph, alternates, Standard Site links — funnels through one typed model, one rewriter that finalizes it, and a shared `data-head` attribute. ### Rendering - [MonorailCSS integration](https://usepennington.net/_llms/explanation/rendering/monorail-css.md): Why Pennington discovers CSS classes by scanning compiled assemblies and watched source files instead of pre-building a static stylesheet. - [The syntax-highlighting cascade](https://usepennington.net/_llms/explanation/rendering/highlighting.md): Why Pennington dispatches code fences through a priority-ordered chain of highlighters with a guaranteed plain-text fallback instead of a single parser. ### Routing - [URL paths and content routes](https://usepennington.net/_llms/explanation/routing/url-paths.md): Why Pennington models URLs and filesystem paths as value-type records and why ContentRoute separates canonical identity from output location. - [Why the sidebar mirrors your folders](https://usepennington.net/_llms/explanation/routing/navigation-tree.md): Why Pennington derives the sidebar tree from folder structure and front-matter order instead of a hand-written nav file, and what that costs when sections reorder. - [Cross-reference resolution](https://usepennington.net/_llms/explanation/routing/cross-references.md): Why Pennington links pages by symbolic uid rather than filesystem path, and how the two-phase resolver turns those uids into canonical URLs without the authoring cost of hand-coded links. ### Spa - [SPA navigation through region swaps](https://usepennington.net/_llms/explanation/spa/islands.md): Why Pennington's SPA fetches the canonical HTML page and swaps marked regions — reusing the server render and its rewriters instead of a parallel JSON envelope. ### Localization - [Locale-aware URLs and content fallback](https://usepennington.net/_llms/explanation/localization/urls-and-fallback.md): Why Pennington treats the URL path prefix as the authoritative locale signal, how DocSiteContentResolver strips it and falls back to the default locale, and why the search index is split per locale. ### Dev Experience - [Hot reload and file watching](https://usepennington.net/_llms/explanation/dev-experience/hot-reload.md): Why Pennington ships its own file watcher and WebSocket reload channel, and how the dev-only script is kept out of published builds. ### Positioning - [What the DocSite and BlogSite templates wire for you](https://usepennington.net/_llms/explanation/positioning/docsite-positioning.md): DocSite and BlogSite are pre-assembled shortcuts on top of the AddPennington engine — what each template wires, and where you have to drop down to the engine itself. - [The SDK you need and the union shim](https://usepennington.net/_llms/explanation/positioning/sdk-and-the-union-shim.md): Why the published packages need only the stable .NET 10 SDK, when the .NET 11 preview SDK is worth opting into, and what the union shim does underneath. ### Discovery - [How the search index is built and queried](https://usepennington.net/_llms/explanation/discovery/search.md): Why Pennington ships a sharded, heading-level search index built at render time and queried entirely in the browser — and what that shape buys the reader. ## How To ### Pages - [Define custom front-matter keys](https://usepennington.net/_llms/how-to/pages/front-matter.md): Declare a record implementing IFrontMatter with extra YAML keys and register it through AddMarkdownContent so a markdown source deserializes into the custom type. - [Mark drafts, schedule posts, tag pages, and control sort order](https://usepennington.net/_llms/how-to/pages/drafts-tags-ordering.md): Hide unfinished pages, embargo posts until a release date, attach grouping keywords, and choose where a page lands in its sidebar section using front-matter keys. - [Add images and shared assets to a page](https://usepennington.net/_llms/how-to/pages/images-and-assets.md): Colocate page-specific images next to their markdown, and put shared images in wwwroot for one canonical URL. - [Forward visitors from a renamed page](https://usepennington.net/_llms/how-to/pages/redirects.md): Set redirectUrl in front matter to forward visitors from the old path to the new one — an HTTP 301 on the live server, plus a meta-refresh stub in the static build. - [Reuse one snippet across many pages](https://usepennington.net/_llms/how-to/pages/include-shared-content.md): Pull a shared Markdown partial into a page with the DocFX-style [!INCLUDE] directive — block or inline — instead of copy-pasting. - [Provide a 404 page](https://usepennington.net/_llms/how-to/pages/not-found-page.md): Author the not-found body with a content-root 404.md (or a NotFound.razor component); the static build writes it to output/404.html for your host to serve. ### Code Samples - [Annotate specific lines in a code block](https://usepennington.net/_llms/how-to/code-samples/code-annotations.md): Apply highlight, diff, focus, and error/warning line classes to fenced code with trailing `[!code ...]` comment directives. - [Group adjacent code fences into a tabbed sample](https://usepennington.net/_llms/how-to/code-samples/tabbed-code.md): Collapse adjacent fenced code blocks into one tabbed widget and customize the rendered CSS class names. - [Embed focused code samples](https://usepennington.net/_llms/how-to/code-samples/focused-code-samples.md): Scope :symbol fences to one member, strip declaration noise with bodyonly, and refactor long methods into named helpers so each section of a walkthrough shows one idea. ### Rich Content - [Add a colored callout for a note, tip, warning, or caution](https://usepennington.net/_llms/how-to/rich-content/alerts.md): GitHub-style alerts: open a blockquote whose first line is `[!KIND]` in uppercase. Pennington recognizes five kinds and paints each one differently. - [Embed a Mermaid diagram in a markdown page](https://usepennington.net/_llms/how-to/rich-content/diagrams.md): Author Mermaid diagrams in markdown with a fenced `mermaid` block and let the DocSite render them client-side with theme awareness. - [Drop a Razor component into a markdown page](https://usepennington.net/_llms/how-to/rich-content/ui-components-in-markdown.md): Embed Pennington.UI components (and your own Razor components) inside a `.md` file through Mdazor-backed rendering. - [Add a custom schema.org JSON-LD type](https://usepennington.net/_llms/how-to/rich-content/structured-data-custom-types.md): Define a record that subclasses JsonLdEntity, attribute its properties for System.Text.Json, and either let the front matter own it via IHasStructuredData or render it inline from a Razor page. - [Tab platform or language variants together](https://usepennington.net/_llms/how-to/rich-content/content-tabs.md): Wrap whole sections of prose, code, and lists in DocFX-style content tabs — synced page-wide, with dependent tabs for a second axis. - [Ship a custom client-side widget](https://usepennington.net/_llms/how-to/rich-content/client-side-widget.md): Add browser behavior to a static Pennington site by composing a server-rendered Mdazor component, your own script, and the head seam that loads a CDN library — built here as an image-gallery lightbox. ### Navigation - [Reorder, rename, or hide entries in the sidebar](https://usepennington.net/_llms/how-to/navigation/customize-sidebar.md): Use front-matter keys, a folder _meta.yml sidecar, and folder layout to control the auto-built sidebar — reorder pages and sections, promote a section landing, override the section header, and hide drafts. - [Link between pages without hardcoding URLs](https://usepennington.net/_llms/how-to/navigation/linking.md): Pick the right link form for sibling pages, cross-area targets, anchors, assets, and external sites — and let Pennington rewrite for sub-path deployments. ### Theming - [Recolor the site](https://usepennington.net/_llms/how-to/theming/monorail-css.md): Swap palettes, override syntax-highlight colors, append site-wide rules, and tweak prose through MonorailCSS without leaving DocSite or BlogSite. - [Switch the body and heading typeface](https://usepennington.net/_llms/how-to/theming/fonts.md): Drop self-hosted woff2 files into wwwroot, register @font-face rules, declare preload hints, and point DisplayFontFamily and BodyFontFamily at the new faces — or load the faces from an external provider instead. - [Populate the blog homepage](https://usepennington.net/_llms/how-to/theming/blogsite-homepage.md): Populate the BlogSite homepage — hero block, My Work card, social-icon row, and top-nav links — from the four init-only properties on BlogSiteOptions. - [Override component styles](https://usepennington.net/_llms/how-to/theming/component-styles.md): Replace individual utility classes on the sidebar navigation and outline rail through the style registry — Tailwind-aware merging keeps the classes you don't touch. ### Versioning - [Version a DocSite](https://usepennington.net/_llms/how-to/versioning/docsite.md): Ship /v1/ and /v2/ URL trees from one DocSite host, each with its own content area and its own reflection-based API reference. ### Discovery - [Serve docs and a blog from separate content roots](https://usepennington.net/_llms/how-to/discovery/multiple-sources.md): Register more than one markdown root — either as DocSite areas or as chained AddMarkdownContent calls on a bare Pennington host — and keep them from overlapping. - [Tune what the search box returns](https://usepennington.net/_llms/how-to/discovery/search.md): Exclude pages from the index, weight document priority, and scope the indexed HTML region without replacing the search backend. - [Add the search modal to a non-DocSite site](https://usepennington.net/_llms/how-to/discovery/search-on-a-bare-host.md): Light up the Pennington.UI search modal on a bare AddPennington host: reference the UI library, serve its scripts, and add a trigger element. - [Serve the site in multiple languages](https://usepennington.net/_llms/how-to/discovery/localization.md): Populate LocalizationOptions, lay out translated content in locale subdirectories, register UI translations, and wire the locale-routing middleware. - [Paginate archive and tag listings](https://usepennington.net/_llms/how-to/discovery/pagination.md): Split long blog archives and tag pages into numbered pages, and apply the same pattern to a custom IContentService. - [Flag missing and outdated translations in the build report and dev overlay](https://usepennington.net/_llms/how-to/discovery/audit-translations.md): Register AddTranslationAudit so missing and stale per-locale translations surface in the build report and the dev overlay, gated by git commit history. ### Feeds - [Make the site discoverable to LLM crawlers](https://usepennington.net/_llms/how-to/feeds/llms-txt.md): Expose a stripped-markdown /llms.txt index plus per-page sidecars so LLM crawlers and agents can ingest your site without scraping HTML. - [Publish an RSS feed](https://usepennington.net/_llms/how-to/feeds/rss.md): Confirm /rss.xml is on, give every post a date so it appears in the channel, and point CanonicalBaseUrl at your production origin so links resolve. - [Publish a custom feed from a content service](https://usepennington.net/_llms/how-to/feeds/custom-feed.md): Build the same RSS pattern BlogSite uses for /rss.xml — a content service that caches records, an XML builder method, and a MapGet endpoint — for podcast episodes, conference sessions, changelogs, or any non-blog content type. - [Publish a sitemap](https://usepennington.net/_llms/how-to/feeds/sitemap.md): Expose an auto-built /sitemap.xml that enumerates every canonical URL, skips drafts and redirects, and uses front-matter dates for lastmod. - [Generate social card images](https://usepennington.net/_llms/how-to/feeds/social-cards.md): Configure SocialCards so every page ships a generated OpenGraph/Twitter image: Pennington discovers, serves, bakes, and meta-tags one card per page; you supply the drawing code. ### Content Services - [Source content from outside the markdown pipeline](https://usepennington.net/_llms/how-to/content-services/custom-content-service.md): Implement IContentService to surface JSON files, a database table, or a remote API as routed pages, navigation entries, search documents, and xref targets. - [Source content from a remote API](https://usepennington.net/_llms/how-to/content-services/source-from-a-remote-api.md): Implement IContentService over a typed HttpClient to turn a remote JSON API — here the GitHub Releases API — into routed pages, navigation, search, and xref targets, with one cached fetch per build, rendered markdown bodies, and a fixture fallback when the API is down. - [Auto-generate an API reference tree for a class library](https://usepennington.net/_llms/how-to/content-services/auto-api-reference.md): Wire the reflection metadata backend (a compiled .dll + .xml pair), call AddApiReference, and get one /reference/api/{type}/ page per public type plus inline Mdazor components for member tables, summaries, and extension-method catalogs. - [Emit generated output artifacts](https://usepennington.net/_llms/how-to/content-services/emit-generated-artifacts.md): Implement an IArtifactContentService that owns a URL territory and produces byte artifacts — robots.txt, JSON sidecars, generated images — served live in dev and written into the static build. - [Use a YAML or JSON data file in pages](https://usepennington.net/_llms/how-to/content-services/data-files.md): Register a YAML or JSON file as a typed value any Razor page or component can read through IDataFiles. The file hot-reloads when you edit it. - [Build browse-by-{field} pages with AddTaxonomy](https://usepennington.net/_llms/how-to/content-services/taxonomy.md): Group your content by any front-matter field (cuisine, tag, audience, series) and render the resulting term pages from a Razor component. Hot-reloads when source files change. - [Add a custom content format](https://usepennington.net/_llms/how-to/content-services/custom-content-format.md): Register a non-markdown file format — here Cooklang .cook recipes — so its files are discovered, parsed, and rendered as pages through the same pipeline as markdown: supply a front-matter type, an IContentParser, an IContentRenderer, and one AddContentFormat call. ### Markdown Pipeline - [Add a Markdig extension or inline parser](https://usepennington.net/_llms/how-to/markdown-pipeline/markdig-extension.md): Register any Markdig extension, inline parser, or block parser through ConfigureMarkdownPipeline — wiki-links as the worked example — and check what the default pipeline already enables before you add. - [Add a custom fence syntax](https://usepennington.net/_llms/how-to/markdown-pipeline/code-block-preprocessor.md): Implement ICodeBlockPreprocessor to claim a fence language or :modifier suffix and return pre-rendered HTML before the default highlighter chain runs. - [Add a custom syntax highlighter](https://usepennington.net/_llms/how-to/markdown-pipeline/custom-highlighter.md): Implement ICodeHighlighter for a fence language TextMateSharp doesn't cover and register it with HighlightingOptions.AddHighlighter. - [Expand a directive before Markdig parses](https://usepennington.net/_llms/how-to/markdown-pipeline/shortcodes.md): Register an IShortcode handler so directives in markdown expand to text or HTML before the rest of the pipeline runs. - [Attach derived metadata to every page](https://usepennington.net/_llms/how-to/markdown-pipeline/metadata-enrichers.md): Implement IMetadataEnricher to merge derived values like reading time or git timestamps into ParsedItem.Derived, kept separate from authored front matter. ### Response Pipeline - [Rewrite HTML attributes after parsing](https://usepennington.net/_llms/how-to/response-pipeline/html-rewriter.md): Implement IHtmlResponseRewriter to mutate already-parsed HTML — lowercase anchors, normalize hrefs, stamp rel=noopener — sharing the document parse with every other rewriter. - [Transform the response body on every page](https://usepennington.net/_llms/how-to/response-pipeline/response-processor.md): Implement IResponseProcessor to rewrite the final response body as a string — inject HTML before
, log an outgoing payload, or append a non-HTML footer. - [Customize the DocSite chrome through DocSiteOptions](https://usepennington.net/_llms/how-to/response-pipeline/override-docsite-components.md): Use DocSiteOptions to inject head content, append CSS, replace the header/footer HTML, and route extra @page components without forking the template. - [Render a Razor component as a page on a bare host](https://usepennington.net/_llms/how-to/response-pipeline/razor-page-on-bare-host.md): Use HtmlRenderer.RenderComponentAsync inside a MapGet to make a Razor component the entire response body, no DocSite layout pipeline required. - [Add tags to the document head](https://usepennington.net/_llms/how-to/response-pipeline/head-contributor.md): Implement IHeadContributor to emit title, meta, link, or script tags into
with central deduplication, band-based ordering, and automatic SPA-navigation survival.
### Deployment
- [Build a static site](https://usepennington.net/_llms/how-to/deployment/static-build.md): Produce a deployable `output/` directory by running the same app in build mode and reading the `BuildReport` for failures.
- [Deploy to GitHub Pages](https://usepennington.net/_llms/how-to/deployment/github-pages.md): Ship a Pennington site to GitHub Pages with a ready-to-copy Actions workflow, base-URL injection, and the `.nojekyll` marker.
- [Adapt the deploy workflow for other hosts](https://usepennington.net/_llms/how-to/deployment/adapt-for-other-hosts.md): Port the GitHub Pages recipe to Azure Static Web Apps, Cloudflare Pages, or Netlify by swapping four shared values and dropping in one host-specific config file.
- [Self-host behind Nginx or IIS](https://usepennington.net/_llms/how-to/deployment/self-host.md): Serve the generated `output/` directory from Nginx or IIS with pretty-URL rewrites and the generated `404.html` as the fallback.
- [Host under a sub-path (base URL)](https://usepennington.net/_llms/how-to/deployment/base-url.md): Serve a Pennington site from a non-root URL by passing `[baseUrl]` to the build and letting `BaseUrlHtmlRewriter` prefix every internal href, src, and action.
## Tutorials
### Getting Started
- [Create your first Pennington site](https://usepennington.net/_llms/tutorials/getting-started/first-site.md): Stand up a minimal ASP.NET host that serves a single markdown page through the Pennington content pipeline.
- [Serve markdown through Blazor Pages](https://usepennington.net/_llms/tutorials/getting-started/first-page.md): Stand up a Pennington site whose markdown is served through a Blazor Server `@page` catch-all — the natural shape for a real app.
- [Style the site with MonorailCSS](https://usepennington.net/_llms/tutorials/getting-started/styling.md): Layer MonorailCSS onto the Blazor-pages site through a routed `MainLayout.razor` and watch the stylesheet regenerate as new utility classes appear in the source.
- [Add navigation across your pages](https://usepennington.net/_llms/tutorials/getting-started/navigation.md): Give the styled bare host a header menu that builds itself from the content pipeline and highlights the current page.
### Docsite
- [Scaffold a documentation site with DocSite](https://usepennington.net/_llms/tutorials/docsite/scaffold.md): Stand up the DocSite template on an empty ASP.NET project and let a folder of markdown light up the sidebar.
- [Add doc pages and link between them](https://usepennington.net/_llms/tutorials/docsite/first-doc-page.md): Build out a Guides area with two content pages, wire sibling navigation, hub-style absolute links, and rename-safe uid cross-references.
- [Organize content with sections and areas](https://usepennington.net/_llms/tutorials/docsite/sections-and-areas.md): 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.
- [Add a Razor landing page at the site root](https://usepennington.net/_llms/tutorials/docsite/landing-page.md): Route a Razor component at / so a DocSite opens on a hand-built landing page, and swap the doc-page chrome for the sidebar-free FullWidthLayout.
- [Add a blog to your documentation site](https://usepennington.net/_llms/tutorials/docsite/add-a-blog.md): Drop a Content/blog folder into a DocSite and watch the blog index, post pages, browse-by-tag pages, and RSS feed light up — no Program.cs changes.
### Blogsite
- [Scaffold a blog with BlogSite](https://usepennington.net/_llms/tutorials/blogsite/scaffold.md): Swap the bare Pennington host for the BlogSite template and configure the core options that drive the home, archive, post, tag, and RSS routes.
- [Publish your first post and light up the RSS feed](https://usepennington.net/_llms/tutorials/blogsite/first-post.md): Replace the scaffold's placeholder post with a fully-populated BlogSiteFrontMatter block and watch it flow into the blog index, per-tag pages, and the built-in RSS feed.
- [Add a hero, projects, and social links](https://usepennington.net/_llms/tutorials/blogsite/hero-projects-socials.md): Populate the four BlogSite homepage surfaces — hero block, My Work card, social-icon row, and top-nav links — on BlogSiteOptions.
### Beyond Basics
- [Add a second locale to your site](https://usepennington.net/_llms/tutorials/beyond-basics/add-a-locale.md): Turn a single-language DocSite into a bilingual one by registering a second locale, translating three pages, and letting the built-in LanguageSwitcher appear in the header.
- [Author a custom Razor component for markdown](https://usepennington.net/_llms/tutorials/beyond-basics/custom-razor-component.md): Author a PricingCard Razor component inside a DocSite, register it with AddMdazorComponent