Blazor MudBlazor Starter
Production-ready Blazor Server starter template with MudBlazor Material Design components. Built on .NET 9 with Docker multi-arch support, GHCR publishing, Azure App Service deployment, CodeQL, and GitHub Pages documentation.
Quick Links
| Page | Description |
|---|---|
| Getting Started | Prerequisites, local run, Docker run |
| Project Structure | Directory layout and file descriptions |
| Components | Blazor components reference |
| Configuration | App settings, build flags, environment variables |
| Deployment | Docker, CI/CD, and Azure deployment |
| Documentation Site | Astro Pages authoring, Sätteri, and validation workflow |
Key Features
- Pre-configured MudBlazor layout with purple app bar, navigation drawer, compact breadcrumbs, project links, and dark mode toggle
- Productized demo pages: Overview, Counter demo, and DataGrid demo with Add/Edit/Remove dialogs
- DataGrid showcase with 69,420 virtualized rows, shortened record IDs, row selection, paging, and right-click clipboard copy
- Multi-architecture Docker image (AMD64 + ARM64) with ASP.NET Core health endpoint at
/healthz - Production-optimized builds with optional AOT plus ReadyToRun, trimming, and extra optimization support
- CI/CD pipeline: PR build checks with container health verification, main branch release to GHCR and Azure
- Renovate dependency updates through the shared
github>jonathanperis/.githubpreset - GitHub Pages documentation site with project overview, reference docs, and deployment notes
Components
Layout Components
MainLayout.razor
The root layout component that provides the MudBlazor application shell. Inherits from LayoutComponentBase and implements IBrowserViewportObserver for responsive design.
Features:
MudThemeProviderwith bindable dark mode toggle, persisted to localStorage- Purple
MudAppBarbranded as MudBlazor Starter, with drawer toggle, dark mode control, and project overflow menu MudDrawerwithMudNavMenulinks labeled Overview, Counter demo, and DataGrid demo- Project overflow links for GitHub, documentation, and health check
MudPopoverProvider,MudDialogProvider, andMudSnackbarProviderfor MudBlazor services- Responsive breakpoint detection: displays a
MudToggleIconButtonon small screens and aMudSwitchon larger screens - Semantic
<main>wrapper and accessible labels for shell controls - UI state (dark mode, drawer open, screen size) persisted to localStorage and restored on first render
Key behavior:
- Subscribes to
IBrowserViewportServicefor breakpoint change notifications - Implements
IAsyncDisposableto unsubscribe from viewport events - Delays rendering until localStorage state is loaded to prevent flash of unstyled content
Breadcrumb.razor
A reusable compact breadcrumb navigation component that wraps MudBreadcrumbs in a MudContainer instead of a heavy raised card.
Parameters:
| Parameter | Type | Description |
|---|---|---|
Items |
List<BreadcrumbItem> |
List of breadcrumb items to display |
Uses a custom separator template with MudIcon (arrow forward icon) and an aria-label="Breadcrumb" navigation label. Each page defines its own breadcrumb items and passes them to this component.
Page Components
Home.razor
Route: /
Production-oriented starter overview page. It presents a concise hero, proof chips, GitHub/docs/DataGrid CTAs, a clone/run command block, and feature cards that explain the included deployment, documentation, and MudBlazor UI patterns.
Counter.razor
Route: /counter
Interactive Counter demo page. Shows a prominent current count value, a primary Increment count button, and a Reset button that is disabled while the count is zero. Demonstrates Blazor component state and event handling without reading like untouched scaffold filler.
Weather.razor
Route: /weather
Full-featured DataGrid demo page demonstrating CRUD operations, virtualization, row selection, paging, and clipboard integration.
Features:
MudDataGridwith 69,420 generated weather forecast entries- Header framed as MudDataGrid showcase with capability chips for virtualization, CRUD dialogs, and right-click copy
- Action row with Add record, Remove selected, and a selected-count chip
Remove selectedstays disabled until at least one row is selected- Shortened record IDs via
ShortId(Guid)to avoid full GUID visual noise - Date, Temperature (C/F), and Summary columns without duplicated stress-test columns
- Multi-selection support with
SelectColumn - Quick filter search across displayed columns
- Sortable and filterable columns with
SortMode.Multiple - Virtualized rendering for performance with large datasets
- Fixed header with configurable page sizes (10, 25, 50, 100, 500, 1000, 5000)
- Loading state with simulated 2-second delay
CRUD Operations:
- Add: opens
AddWeatherdialog viaIDialogService, appends a new entry, and showsWeather record added. - Edit: opens
EditWeatherdialog with the selected item and replaces the entry in-place - Remove: opens
RemoveWeatherconfirmation dialog and removes all selected items
Context Menu:
- Right-click on a row to copy a single line or all selected lines to the clipboard
- Clipboard data is formatted as semicolon-separated values
- Empty-selection snackbar messages explicitly tell the user to select rows first
Data model (WeatherForecast): Defined as a nested class with Id (Guid), Date (DateTime), TemperatureC (int), Summary (string?), and computed TemperatureF.
Error.razor
Route: /Error
Error page that displays when an unhandled exception occurs. Shows the request ID from Activity.Current or HttpContext.TraceIdentifier when available. Includes guidance about the Development environment.
Weather Dialog Components
AddWeather.razor
A MudDialog wrapped in an EditForm with DataAnnotationsValidator. Provides text fields for Weather ID (Guid), Date, Temperature (C), and Summary. On valid submission, returns the new WeatherForecast via DialogResult.Ok and shows a success snackbar notification.
EditWeather.razor
A MudDialog wrapped in an EditForm for editing an existing weather entry.
Parameters:
| Parameter | Type | Description |
|---|---|---|
Item |
Weather.WeatherForecast |
The weather entry to edit |
The Weather ID field is read-only. On valid submission, returns the edited item via DialogResult.Ok and shows a success snackbar notification.
RemoveWeather.razor
A simple MudDialog confirmation prompt. Displays a warning message asking the user to confirm deletion. On confirmation, returns DialogResult.Ok(true) and shows a success snackbar notification. Does not use EditForm since no data input is required.
MudBlazor Components Used
| Component | Usage |
|---|---|
MudLayout, MudAppBar, MudDrawer, MudMainContent |
Application shell structure |
MudThemeProvider |
Material Design theming with dark mode |
MudNavMenu, MudNavLink |
Side navigation |
MudBreadcrumbs |
Compact page navigation breadcrumbs |
MudDataGrid, PropertyColumn, SelectColumn, TemplateColumn |
DataGrid demo table |
MudDataGridPager |
Data grid pagination |
MudDialog, MudDialogProvider |
Modal dialogs for CRUD operations |
MudSnackbar, MudSnackbarProvider |
Toast notifications |
MudButton, MudIconButton, MudToggleIconButton |
Action buttons |
MudTextField |
Form inputs and search |
MudCard, MudCardHeader, MudCardContent, MudCardActions |
Content cards |
MudMenu, MudMenuItem |
Context menu and overflow menu |
MudSwitch |
Dark mode toggle (large screens) |
MudText, MudLink, MudSpacer, MudDivider, MudIcon, MudChip |
Typography and layout utilities |
MudPopoverProvider |
Popover rendering |
Configuration
Application Settings
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
| Key | Default | Description |
|---|---|---|
Logging:LogLevel:Default |
Information |
Minimum log level for all categories |
Logging:LogLevel:Microsoft.AspNetCore |
Warning |
Log level for ASP.NET Core framework logs |
AllowedHosts |
* |
Allowed host headers (all hosts by default) |
appsettings.Development.json
Overrides for local development. Currently mirrors the base logging configuration.
Environment Variables
| Variable | Default | Description |
|---|---|---|
ASPNETCORE_ENVIRONMENT |
Production |
Set to Development for local dev (auto-set by launch profiles) |
ASPNETCORE_URLS |
http://+:5000 |
Listening URL (set in Dockerfile for container builds) |
APPLICATIONINSIGHTS_CONNECTION_STRING |
unset locally; set by Azure Bicep | Enables Application Insights telemetry when present |
APPINSIGHTS_INSTRUMENTATIONKEY |
unset locally; set by Azure Bicep | Legacy instrumentation key exposed for Azure App Service/Application Insights compatibility |
ApplicationInsightsAgent_EXTENSION_VERSION |
unset locally; ~3 in Azure |
Enables the Azure App Service Application Insights site extension |
Application Insights is inactive for local development unless you provide APPLICATIONINSIGHTS_CONNECTION_STRING. In Azure, the Bicep deployment wires the connection string and instrumentation key into Web App application settings.
.NET SDK Version
Pinned in global.json:
{
"sdk": {
"version": "9.0.202",
"rollForward": "minor"
}
}
The rollForward: minor policy allows using any 9.0.x SDK version at or above 9.0.202.
MudBlazor Service Registration
In Program.cs, MudBlazor is registered with:
builder.Services.AddMudServices();
builder.Services.AddMudTranslations();
MudGlobal.UnhandledExceptionHandler = Console.WriteLine;
AddMudServices()registers all MudBlazor services (dialogs, snackbar, scroll, resize, etc.)AddMudTranslations()registers localization support (MudBlazor.Translations 3.3.0)MudGlobal.UnhandledExceptionHandlerroutes unhandled exceptions to the console
Docker Build Arguments
The Dockerfile accepts four build arguments that control the compilation output:
| Argument | Default | Values | Description |
|---|---|---|---|
AOT |
false |
true, false |
Enable Ahead-of-Time compilation for faster startup |
TRIM |
false |
true, false |
Enable ReadyToRun, single-file publish, and self-contained deployment |
EXTRA_OPTIMIZE |
false |
true, false |
Strip symbols, disable debugger support, invariant globalization |
BUILD_CONFIGURATION |
Debug |
Debug, Release |
.NET build configuration |
Build Flag Details
AOT (AOT=true): Enables PublishAot with OptimizationPreference set to Speed. Produces a native binary with faster cold-start performance.
TRIM (Trim=true): Enables PublishReadyToRun, PublishReadyToRunComposite, PublishSingleFile, and SelfContained. Produces an optimized single-file deployment.
EXTRA_OPTIMIZE (ExtraOptimize=true): Applies aggressive optimizations for minimal binary size:
TrimmerRemoveSymbols– removes debug symbolsDebuggerSupport– disabledInvariantGlobalization– uses invariant cultureEventSourceSupport– disabledHttpActivityPropagationSupport– disabledMetadataUpdaterSupport– disabledStackTraceSupport– disabledUseSystemResourceKeys– uses system resource keys instead of embedded strings
Production Defaults
The main-release.yml pipeline uses these defaults:
AOT=false
TRIM=true
EXTRA_OPTIMIZE=true
BUILD_CONFIGURATION=Release
Getting Started
Prerequisites
- .NET 9 SDK (9.0.202 or later)
- Docker (optional, for container builds)
Run Locally
git clone https://github.com/jonathanperis/blazor-mudblazor-starter.git
cd blazor-mudblazor-starter
dotnet restore
dotnet run --project src/WebClient
Open http://localhost:5000 in your browser.
The https launch profile is also available at https://localhost:5001.
Run with Docker
Build the image from the src/ context using the Dockerfile inside src/WebClient/:
docker build -t blazor-mudblazor -f src/WebClient/Dockerfile src/
docker run -p 5000:5000 blazor-mudblazor
Open http://localhost:5000 in your browser.
Docker Build Arguments
You can pass build arguments to control optimization:
docker build \
--build-arg AOT=false \
--build-arg TRIM=true \
--build-arg EXTRA_OPTIMIZE=true \
--build-arg BUILD_CONFIGURATION=Release \
-t blazor-mudblazor -f src/WebClient/Dockerfile src/
See Configuration for details on each build argument.
Access URLs
| Context | URL |
|---|---|
| Local (HTTP) | http://localhost:5000 |
| Local (HTTPS) | https://localhost:5001 |
| Docker container | http://localhost:5000 |
| Live demo | blazor-mudblazor-starter |
Deployment
Docker
Build the Image
The Dockerfile uses a multi-stage build with the .NET 9 SDK and ASP.NET runtime images. The build context is the src/ directory.
docker build -t blazor-mudblazor -f src/WebClient/Dockerfile src/
With production optimizations:
docker build \
--build-arg AOT=false \
--build-arg TRIM=true \
--build-arg EXTRA_OPTIMIZE=true \
--build-arg BUILD_CONFIGURATION=Release \
-t blazor-mudblazor -f src/WebClient/Dockerfile src/
Run the Container
docker run -p 5000:5000 blazor-mudblazor
The container listens on port 5000 (ASPNETCORE_URLS=http://+:5000). The entry point is the compiled ./WebClient binary.
Multi-Architecture Support
The release pipeline builds both linux/amd64 and linux/arm64/v8 images. It uses Docker Buildx for both builds, QEMU for the arm64 job, and then merges both digests into the multi-arch :latest manifest. The Dockerfile installs clang and zlib1g-dev in the SDK stage so optional AOT compilation has the native toolchain it needs.
Pre-built Image
The latest release image is available from GitHub Container Registry:
docker pull ghcr.io/jonathanperis/blazor-mudblazor-starter:latest
docker run -p 5000:5000 ghcr.io/jonathanperis/blazor-mudblazor-starter:latest
CI/CD Pipelines
build-check.yml (Pull Requests)
Triggered on pull requests to main. Runs two jobs:
-
setup-build-test: Sets up the .NET SDK from
global.json, restores dependencies, and builds the project with debug settings (AOT=false,TRIM=false,BUILD_CONFIGURATION=Debug). -
container-test: Builds a Docker image, runs the container on port 5030, and polls the
/healthzendpoint up to 20 times (5-second intervals) to verify the application starts correctly. Fails the pipeline if the health check does not return HTTP 200.
main-release.yml (Main Branch)
Triggered on push to main or manual dispatch. The current release flow is split into six jobs:
-
setup-build-test: Restores and builds with production settings (
AOT=false,TRIM=true,EXTRA_OPTIMIZE=true,BUILD_CONFIGURATION=Release). -
build-push-amd64: Sets up Docker Buildx, authenticates to GitHub Container Registry, builds the
linux/amd64image, and pushes it asghcr.io/jonathanperis/blazor-mudblazor-starter:latest. -
deploy-infra: Logs in to Azure with OIDC (
AZURE_CLIENT_ID,AZURE_TENANT_ID,AZURE_SUBSCRIPTION_ID) and deploysinfra/main.bicep/infra/main.bicepparamwithazure/arm-deploy. -
deploy-image: Deploys the GHCR
:latestimage to Azure App Service withazure/webapps-deployand theAZURE_WEBAPP_PUBLISH_PROFILEsecret. -
build-push-arm64: Sets up QEMU and Docker Buildx, builds
linux/arm64/v8, and pushes it as:latest-arm64. -
merge-manifest: Combines the amd64 and arm64 digests into the final multi-arch
:latestmanifest.
codeql.yml
Runs CodeQL security analysis on the codebase.
deploy.yml (GitHub Pages)
Triggered on push to main or manual dispatch. Delegates to the reusable jonathanperis/.github/.github/workflows/pages-docs-deploy.yml@main workflow with inherited secrets; that shared workflow builds the Astro docs from docs/ and publishes the generated Pages artifact.
Azure Web App
The application is deployed to Azure App Service in the Brazil South region. The workflow first keeps the Azure resources current with Bicep over OIDC, then deploys the GHCR container image to the Web App with the Azure publish profile.
The Bicep entry point creates or updates the App Service Plan, Web App, Log Analytics Workspace, and Application Insights instance. The App Service Plan is provisioned from infra/modules/appServicePlan.bicep before the Web App module consumes its resource ID.
Live demo: blazor-mudblazor-starter
Azure Deployment Requirements
- Resource group:
github-jonathanperis - Region:
brazilsouth - App Service Plan:
github-jonathanperis(B1, Linux)- The plan is created or updated by
infra/modules/appServicePlan.bicep. infra/main.bicepparamcontrols the plan name andappServicePlanSku.
- The plan is created or updated by
- Web App name:
blazor-mudblazor-starter - OIDC secrets for infrastructure deployment:
AZURE_CLIENT_ID,AZURE_TENANT_ID, andAZURE_SUBSCRIPTION_ID - The
AZURE_WEBAPP_PUBLISH_PROFILEsecret set in the GitHub repository settings (download from Azure Portal > Web App > Deployment Center > Manage publish profile) - GHCR image access configured on the Azure Web App (the image is public via GitHub Packages)
Observability
infra/main.bicep provisions Log Analytics and Application Insights, then passes telemetry settings into the Web App module. The app only registers AddApplicationInsightsTelemetry() when APPLICATIONINSIGHTS_CONNECTION_STRING is present, so local runs stay telemetry-free by default while Azure deployments emit telemetry automatically.
Azure app settings managed by Bicep:
| Setting | Purpose |
|---|---|
APPLICATIONINSIGHTS_CONNECTION_STRING |
Enables Application Insights telemetry in Program.cs |
APPINSIGHTS_INSTRUMENTATIONKEY |
Compatibility setting for App Service/Application Insights integration |
ApplicationInsightsAgent_EXTENSION_VERSION |
Enables the App Service Application Insights extension (~3) |
Documentation Site
The public documentation is an Astro static site in docs/ and is published to GitHub Pages by .github/workflows/deploy.yml.
Authoring Model
| Area | Purpose |
|---|---|
docs/wiki/*.md |
Markdown source pages rendered under /docs/ |
docs/src/lib/sidebar.config.ts |
Navigation order and section grouping for Markdown pages |
docs/src/components/home/ |
Custom landing page sections for the GitHub Pages root |
docs/astro.config.mjs |
Astro configuration, static output, base path, sitemap, Tailwind, and Markdown processor |
docs/out/ |
Generated static output from bun run build |
Local Commands
Use Bun from the docs/ directory:
cd docs
bun install
bun run build
bun run check:rendered
Run the source-backed drift check from the repository root:
python3 scripts/check-docs-drift.py
Adding or Renaming a Page
- Add or rename the Markdown file in
docs/wiki/. - Add the page slug to
SECTION_CATEGORIESindocs/src/lib/sidebar.config.ts. - Link to the page with root-relative docs routes such as
../configuration/from another docs page, or/blazor-mudblazor-starter/docs/configuration/when authoring absolute public links. - Run
bun run build,bun run check:rendered, andpython3 scripts/check-docs-drift.py.
Markdown Processor
The site uses Astro 6.4 with the Rust-based Sätteri Markdown processor:
import { satteri } from '@astrojs/markdown-satteri';
export default defineConfig({
markdown: {
processor: satteri(),
},
});
Sätteri is used for faster Markdown builds. The rendered HTML smoke test protects the important Markdown features this site depends on: routes, headings and anchors, tables, fenced code blocks, and internal links.
Deployment
.github/workflows/deploy.yml delegates to the reusable jonathanperis/.github/.github/workflows/pages-docs-deploy.yml@main workflow with inherited secrets. The reusable workflow installs the docs dependencies, builds the Astro site, and publishes the generated Pages artifact.
Project Structure
blazor-mudblazor-starter/
├── .github/
│ └── workflows/
│ ├── build-check.yml # PR validation: .NET build + Docker build + health check
│ ├── codeql.yml # CodeQL security analysis
│ ├── deploy.yml # GitHub Pages deployment
│ └── main-release.yml # Release: optimized build + GHCR push + Azure deploy
├── src/
│ └── WebClient/
│ ├── Components/
│ │ ├── App.razor # Root HTML document with MudBlazor CSS/JS imports
│ │ ├── Routes.razor # Blazor router, defaults to MainLayout
│ │ ├── _Imports.razor # Global using directives for all components
│ │ ├── Layout/
│ │ │ ├── MainLayout.razor # App shell: app bar, drawer, dark mode, responsive breakpoints
│ │ │ └── Breadcrumb.razor # Reusable breadcrumb navigation component
│ │ ├── Pages/
│ │ │ ├── Home.razor # Production starter overview (route: /)
│ │ │ ├── Counter.razor # Counter demo with increment/reset state (route: /counter)
│ │ │ ├── Weather.razor # DataGrid demo with virtualization and CRUD (route: /weather)
│ │ │ └── Error.razor # Error page with request ID display (route: /Error)
│ │ └── Weather/
│ │ ├── AddWeather.razor # MudDialog for adding weather entries
│ │ ├── EditWeather.razor # MudDialog for editing weather entries
│ │ └── RemoveWeather.razor # MudDialog for delete confirmation
│ ├── Properties/
│ │ └── launchSettings.json # Local dev profiles (HTTP on 5000, HTTPS on 5001)
│ ├── wwwroot/ # Static assets (favicon, CSS)
│ ├── appsettings.json # Base configuration (logging, allowed hosts)
│ ├── appsettings.Development.json # Development logging overrides
│ ├── Dockerfile # Multi-stage .NET 9 build (AMD64 + ARM64)
│ ├── Program.cs # App entry point, MudBlazor service registration
│ └── WebClient.csproj # Project file: .NET 9, MudBlazor 9.3.0, AOT/Trim flags
├── .editorconfig # Code style settings
├── .gitignore # Git ignore rules
├── global.json # .NET SDK version pin (9.0.202, roll-forward: minor)
├── renovate.json # Shared Renovate dependency update preset
├── LICENSE # MIT license
├── README.md # Project overview and quick start
└── WebClient.sln # Solution file
Key Directories
src/WebClient/Components/Layout/
Contains the application shell. MainLayout.razor provides the MudBlazor layout with app bar, navigation drawer, dark mode toggle (persisted via localStorage), and responsive breakpoint handling. Breadcrumb.razor is a reusable component that accepts a list of BreadcrumbItem parameters.
src/WebClient/Components/Pages/
Contains routable page components. Each page uses the compact Breadcrumb component for navigation context. Home presents the production-ready starter overview, Counter demonstrates component state, and the Weather route is framed as a DataGrid demo with MudDataGrid, dialog services, snackbar notifications, row selection, and clipboard integration.
src/WebClient/Components/Weather/
Contains MudDialog components used by the Weather page for Add, Edit, and Remove operations. Each dialog uses EditForm with DataAnnotationsValidator for form validation (except RemoveWeather, which is a simple confirmation).
.github/workflows/
Contains four GitHub Actions workflows: build-check.yml for PR validation (includes container health check against /healthz), main-release.yml for production releases to GHCR and Azure, codeql.yml for security analysis, and deploy.yml for GitHub Pages deployment. Dependency updates are configured separately in renovate.json, which inherits the shared github>jonathanperis/.github preset.