Author a custom Razor component for markdown
Author a PricingCard Razor component inside a DocSite, register it with Add
By the end of this tutorial you'll have a running DocSite at http://localhost:5000/pricing that renders two styled <PricingCard /> cards — a standard "Basic" tier and a highlighted "Pro" tier — both driven by tag attributes inside a plain markdown file.
Along the way, the tutorial covers authoring a Razor component with [Parameter]-decorated properties, wiring it into Mdazor's component registry with one AddMdazorComponent<T>() line, and consuming it from markdown with self-closing tag syntax whose attribute values bind case-insensitively to the component's parameters.
Prerequisites
- .NET 10 SDK installed
- Completed Scaffold a documentation site with DocSite (provides the
AddDocSite/UseDocSite/RunDocSiteAsynchost shape this tutorial extends) - Basic Razor familiarity — a
.razorfile with@code {}and[Parameter]properties should feel routine
The finished code for this tutorial lives in examples/BeyondCustomRazorComponentExample.
1. Author the PricingCard component
Before Mdazor can render a custom tag from markdown, a real Razor component has to exist in your project. This unit adds Components/PricingCard.razor and a top-level _Imports.razor so [Parameter] is in scope without per-file @using lines.
Add a project-wide _Imports.razor
Drop an _Imports.razor file at your project root so every .razor file in the project gets the Blazor component namespaces. This is the same file a Blazor template ships with — the @using lines are what make [Parameter] resolve inside the component file in the next step, and the last line brings your Components/ folder into scope so markdown can reference PricingCard by name.
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Web
@using <your root namespace>.Components
Use your own root namespace. Replace
<your root namespace>with your project's default namespace (the.csprojname).
Create Components/PricingCard.razor
Create a Components/ folder and add PricingCard.razor with four [Parameter] properties — Tier, Price, Features, and Highlighted — and markup that renders a pricing card, switching to a thicker accent border when Highlighted is set. The Features parameter is a pipe-delimited string because Mdazor binds only primitive parameter types from markdown attributes; lists arrive as strings and are split inside the component.
<div class="not-prose my-6">
<div class="@CardClasses">
<h3 class="text-xl font-bold">@Tier</h3>
<div class="mt-2 flex items-baseline gap-1">
<span class="text-4xl font-extrabold">$@Price</span>
<span class="text-sm">/ month</span>
</div>
<ul class="mt-4 space-y-2 text-sm">
@foreach (var feature in ParsedFeatures)
{
<li>@feature</li>
}
</ul>
</div>
</div>
@code {
[Parameter] public string Tier { get; set; } = "Basic";
[Parameter] public string Price { get; set; } = "0";
[Parameter] public string Features { get; set; } = "";
[Parameter] public bool Highlighted { get; set; }
private IEnumerable<string> ParsedFeatures =>
(Features ?? "").Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
private string CardClasses => Highlighted
? "rounded-xl border-2 border-primary-500 p-6"
: "rounded-xl border border-base-200 p-6";
}
The file is a regular Blazor component — nothing Pennington-specific yet.
Checkpoint
Run dotnet build from your project root. The build succeeds. The PricingCard type now exists at <your root namespace>.Components.PricingCard, but it is not yet wired to Mdazor, so a <PricingCard /> tag in markdown would still render as a literal custom element.
2. Register the component with Mdazor
DocSite already calls AddMdazor() and registers the built-in Pennington.UI components. The only remaining step is one AddMdazorComponent<PricingCard>() line so Mdazor's registry knows about the new type.
Add AddMdazorComponent<PricingCard>() to Program.cs
Open Program.cs and add a single builder.Services.AddMdazorComponent<PricingCard>() line after the AddDocSite block. The extension lives in the Mdazor namespace and ships from the Mdazor NuGet package, already transitively referenced through Pennington.DocSite — no package add required.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDocSite(() => new DocSiteOptions
{
SiteTitle = "Beyond Custom Razor Component",
SiteDescription = "Authoring a Razor component and rendering it inline from markdown.",
GitHubUrl = "https://github.com/usepennington/pennington",
HeaderContent = """<a href="/">Beyond Custom Razor Component</a>""",
FooterContent = """<footer class="mt-16 py-8 text-center text-sm text-base-500">Built with Pennington DocSite.</footer>""",
});
// The one new line vs stage 1: tell Mdazor about the PricingCard type.
// AddMdazorComponent<T>() is an IServiceCollection extension in the
// Mdazor namespace (from the Mdazor NuGet package, transitively
// referenced through Pennington.DocSite). It returns the same
// IServiceCollection so it chains with further registrations.
builder.Services.AddMdazorComponent<PricingCard>();
var app = builder.Build();
app.UseDocSite();
await app.RunDocSiteAsync(args);
Checkpoint
Run dotnet build from your project root again. The build still succeeds, and Mdazor's registry now contains PricingCard. Nothing renders differently yet — the next section adds a markdown page that uses the tag.
3. Consume the component from markdown
Now let's add a markdown page that uses <PricingCard /> twice with different attribute values, exercising both the default and highlighted visual states of the component.
Create Content/pricing.md
Add a new markdown page under Content/ with front matter (title: Pricing, description:, order: 20) and two <PricingCard ... /> tags between headings. The first card uses Tier="Basic" Price="9"; the second adds Highlighted="true" and richer feature text.
---
title: Pricing
description: Two PricingCard components rendered from markdown with distinct parameter values.
order: 20
---
Pick a plan that fits your team. Both tiers below are rendered from a single
Razor component, `PricingCard`, authored in this example's `Components/`
folder and registered via `AddMdazorComponent<PricingCard>()` in
`Program.cs`. The markdown below consumes the component by name — Mdazor
intercepts tags that look like registered components, binds their
attributes as parameters, and hands the resulting HTML back to the Markdig
pipeline.
## Plans
<PricingCard Tier="Basic" Price="9" Features="1 project|5 GB storage|Community support" />
<PricingCard Tier="Pro" Price="49" Features="Unlimited projects|100 GB storage|Priority email support|Team seats included" Highlighted="true" />
## Why two cards?
Rendering the component twice with different attribute values proves that
Mdazor resolves `<PricingCard />` tags on every occurrence, not just the
first. The second card passes `Highlighted="true"`, which flips the
component into its emphasised visual state — a thicker accent border.
## How the wiring works
1. The component is a regular Razor component with `[Parameter]`-decorated
properties for `Tier`, `Price`, `Features`, and `Highlighted`.
2. `services.AddMdazorComponent<PricingCard>()` adds the type to Mdazor's
component registry.
3. When the markdown renderer encounters `<PricingCard ... />`, it looks up
the registered type, instantiates it, assigns parameters via
case-insensitive reflection, renders the component through Blazor's
server-side `HtmlRenderer`, and inlines the resulting HTML into the page.
Self-closing (`<PricingCard ... />`) and open/close (`<PricingCard ...></PricingCard>`)
forms are both supported; the open/close form lets the component receive
`ChildContent` populated by any markdown between the tags.
Mdazor matches tag names case-sensitively on the leading character — <PricingCard> must start with a capital letter — and binds attribute values to parameters case-insensitively. See Content components for the full binding rules.
Run the site
Start the host with dotnet run from your project root, then open http://localhost:5000/pricing. Mdazor intercepts each <PricingCard ... /> tag, looks up the registered type, instantiates it, binds the attributes to its parameters, and inlines the rendered HTML in place of the tag.
Checkpoint
Visit http://localhost:5000/pricing. Two pricing cards appear: a Basic card at $9 / month with a thin border, and a Pro card at $49 / month with a thicker accent border (its Highlighted="true" attribute). View the page source — <PricingCard> has been replaced by real HTML (a <div> tree with the card classes), not left as a literal custom element.
4. Pass more parameters and verify binding
Now let's confirm the markdown-to-parameter binding is real by editing attribute values in the markdown and watching the rendered output change — this is the whole authoring loop.
Edit the Pro card to change Price and Features
In Content/pricing.md, change Price="49" to Price="99" and extend the Features="" string with an extra pipe-separated entry (for example, "...|24/7 chat support"). Save the file.
Flip Highlighted on the Basic card
Add Highlighted="true" to the first <PricingCard Tier="Basic" ... /> tag. Boolean attribute values from markdown bind with case-insensitive true / false — Highlighted="True" and Highlighted="true" both flip the card into its emphasized state.
Checkpoint
Reload http://localhost:5000/pricing. The dev host picks up markdown changes as you save, so no rebuild is required.
- The Pro card now reads $99 / month and lists the extra feature bullet
- The Basic card now renders with the thicker accent border instead of its plain one
- Open the browser's dev tools — the generated HTML under each
<PricingCard>has changed to match
Summary
- A Razor component lives under
Components/with[Parameter]-decorated properties and is consumed from markdown by name. - Any component type registers with Mdazor in one line:
services.AddMdazorComponent<T>()afterAddDocSite(or afterAddPenningtonon a custom host). - Two binding rules govern markdown-driven consumption: tag names start with a capital letter, and attribute values bind case-insensitively to parameter properties of primitive types (
string,bool, numbers). - Built-in Pennington.UI components and custom components mix freely in the same markdown page — both go through the same Mdazor registry.