This documentation is also published as Markdown for efficient machine reading: the whole site is indexed at /llms.txt, and every page has a clean Markdown copy under /_llms/. These are generated from the same source and cost far fewer tokens to read than this rendered HTML.

Skip to main content Skip to navigation
Getting Started

Style the site with MonorailCSS

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.

By the end of this tutorial the Blazor-pages site from Serve markdown through a Blazor catch-all is styled with MonorailCSS — a Tailwind-compatible JIT compiler in pure .NET. Every routed @page renders through a MainLayout.razor that carries the utility classes. The MonorailCSS Discovery pipeline turns those classes into real CSS rules, served at /styles.css. The stylesheet regenerates whenever a new class appears in the source.

Prerequisites

The finished code for this tutorial lives in examples/GettingStartedStylingExample. For a documentation site, the DocSite template ships this MonorailCSS-plus-MainLayout stack with a sidebar, search, and theme toggle already assembled — Scaffold a documentation site with DocSite covers it.


1. Wrap pages in a styled MainLayout.razor

Before MonorailCSS can do anything, the layout needs to carry the utility classes that will turn into CSS rules. The document shell moves out of App.razor and into a MainLayout.razor that holds those classes; App.razor shrinks to a bare router that wraps every routed page in the new layout.

1

Create Components/Layout/MainLayout.razor

Drop this file at Components/Layout/MainLayout.razor. Inheriting LayoutComponentBase makes it a Blazor layout — every routed page renders into the @Body placeholder. This component now owns the whole document shell — <!DOCTYPE>, <html>, <head> (with <HeadOutlet>), and <body> — moved here from App.razor. The <link rel="stylesheet" href="/styles.css"> tag points at an endpoint section 2 will mount.

razor
@* Styled shell. Lives once and wraps every routed @page via App.razor's
   DefaultLayout. The class strings (text-primary-700, bg-base-50, …) become
   literal IL strings after Razor compiles, and Discovery's startup IL scan
   picks them up to populate the class registry behind /styles.css. *@
  
@inherits LayoutComponentBase
  
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="/styles.css" />
    <HeadOutlet />
</head>
<body class="bg-base-50 text-base-900 min-h-screen">
    <div class="max-w-3xl mx-auto px-6 py-10">
        <header class="mb-8 border-b border-base-200 pb-4">
            <a class="text-lg font-bold text-primary-700" href="/">My Styled Pennington Site</a>
        </header>
        <article class="prose">
            @Body
        </article>
        <footer class="mt-12 pt-4 border-t border-base-200 text-xs text-base-500">
            Styled with MonorailCSS.
        </footer>
    </div>
</body>
</html>

The classes — bg-base-50, text-primary-700, border-base-200, and so on — come from the named color palette configured in the next section.

2

Reference the layout namespace from _Imports.razor

App.razor refers to MainLayout by its bare name, so the project's _Imports.razor needs an @using for the layout's namespace — without it typeof(MainLayout) fails to compile. Add the Components.Layout line to the _Imports.razor at the project root:

razor
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Pennington.Content
@using Pennington.Pipeline
@using Pennington.Routing
@using GettingStartedStylingExample.Components
@using GettingStartedStylingExample.Components.Layout

Note

The embeds use the finished example's root namespace, GettingStartedStylingExample (in _Imports.razor and the section 2 Program.cs snippet). Swap in your own.

3

Replace Components/App.razor

With the shell now in MainLayout.razor, App.razor is replaced wholesale: it drops the <!DOCTYPE>, <html>, <head>, and <HeadOutlet> it used to own and becomes just the <Router>. RouteView names MainLayout as the DefaultLayout for every matched page, and the LayoutView for the not-found case lets the same shell wrap the 404 message.

razor
@* Root component. The Router scans this assembly for [@page] components and
   wraps each match in MainLayout (the styled shell). <PageTitle> from each
   routed page flows into <head> via <HeadOutlet>. *@
  
<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Not found.</p>
        </LayoutView>
    </NotFound>
</Router>

2. Register MonorailCSS and mount /styles.css

MonorailCSS ships in its own package, separate from the core Pennington package the previous tutorial added. Pull it in:

bash
dotnet add package Pennington.MonorailCss

With the package referenced, wire MonorailCSS into the service container, pick a color scheme, and mount the JIT stylesheet endpoint. AddMonorailCss registers the services; each of PrimaryColorName, AccentColorName, and BaseColorName takes a ColorName constant (indigo/pink/slate here — any combination works). app.UseMonorailCss() mounts /styles.css as a real endpoint that regenerates on every request, matching the <link> tag in MainLayout.razor. Both highlighted blocks are new in this section; the using Pennington.MonorailCss; at the top is what makes AddMonorailCss, MonorailCssOptions, NamedColorScheme, and ColorName resolve.

csharp
using GettingStartedStylingExample.Components;
using Pennington.FrontMatter;
using Pennington.Infrastructure;
using Pennington.MonorailCss;
  
var builder = WebApplication.CreateBuilder(args);
  
builder.Services.AddPennington(penn =>
{
    penn.SiteTitle = "My Styled Pennington Site";
    penn.ContentRootPath = "Content";
  
    penn.AddMarkdownContent<DocFrontMatter>(md =>
    {
        md.ContentPath = "Content";
        md.BasePageUrl = "/";
    });
});
  
// New in this stage: register MonorailCSS. Pick which named palettes
// back the `primary`, `accent`, and `base` utility prefixes. Any
// ColorName constant works — swap freely.
builder.Services.AddMonorailCss(_ => new MonorailCssOptions 
{ 
    ColorScheme = new NamedColorScheme 
    { 
        PrimaryColorName = ColorName.Indigo, 
        AccentColorName = ColorName.Pink, 
        BaseColorName = ColorName.Slate, 
    }, 
}); 
  
builder.Services.AddRazorComponents();
  
var app = builder.Build();
  
app.UsePennington();
  
// New in this stage: mount /styles.css. The default path matches the
// <link> tag in MainLayout.razor.
app.UseMonorailCss(); 
  
app.UseAntiforgery();
app.MapRazorComponents<App>();
  
await app.RunOrBuildAsync(args);

Checkpoint

  • Run dotnet run --urls http://localhost:5000 and visit http://localhost:5000/ — the header, article, and footer now render with indigo accents, slate neutrals, and the layout spacing the utility classes describe
  • Visit http://localhost:5000/styles.css directly and a populated stylesheet appears, containing rules for every utility class the layout emits

3. Watch the stylesheet regenerate

Under dotnet run, MonorailCSS rescans your project for new utility classes on the next /styles.css request, so classes you add in source (Razor components and other compiled C#) appear without a restart. Markdown bodies are out of scope: a utility token added to a .md file will not produce a CSS rule. The MonorailCSS integration explanation covers why.

1

Add a new utility class to MainLayout.razor

Open Components/Layout/MainLayout.razor and wrap the footer's "MonorailCSS" word in an accented span:

razor
<footer class="mt-12 pt-4 border-t border-base-200 text-xs text-base-500">
    Styled with <span class="text-accent-600 italic">MonorailCSS</span>.
</footer>

The class text-accent-600 wasn't in the layout, so it doesn't yet exist in the stylesheet.

2

Reload and confirm the new rule

Reload any page in the browser. The footer's "MonorailCSS" word renders in pink italic because the .razor edit refreshed the class set, and the next /styles.css request picked up the new token. Reload /styles.css directly and the text-accent-600 rule is present.

Checkpoint

  • The footer's "MonorailCSS" word renders in pink italic on every page
  • http://localhost:5000/styles.css now contains a rule for text-accent-600 that wasn't there before the edit
  • No server restart was required — the MonorailCSS file watcher refreshed the stylesheet under the running dotnet run

Summary

  • MainLayout.razor (a Blazor LayoutComponentBase) holds the utility-class scaffold every routed @page renders into via App.razor's DefaultLayout.
  • AddMonorailCss(...) registers the service container; UseMonorailCss() mounts the /styles.css endpoint that regenerates on every request.
  • A NamedColorScheme of three ColorName constants drives every primary-*, accent-*, and base-* utility prefix.
  • Under dotnet run, adding a new utility class to a .razor or .cs file regenerates the stylesheet on the next request without a restart — markdown edits do not participate.

The site is styled, but every page is an island with no way to reach the next. The final getting-started tutorial adds a navigation menu that links them.