Author a custom Razor component for markdown
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 11 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 the project. This unit adds Components/PricingCard.razor and a top-level _Imports.razor so [Parameter] is in scope without per-file @using lines.
- 1
Add a project-wide
_Imports.razorDrop an
_Imports.razorfile at the project root so every.razorfile in the project gets the Blazor component namespaces. This is the same file a Blazor template ships with — the two@usinglines are what make[Parameter]resolve inside the component file in the next step.@using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components.Web @using BeyondCustomRazorComponentExample.Components - 2
Create
Components/PricingCard.razorCreate a
Components/folder and addPricingCard.razorwith four[Parameter]properties —Tier,Price,Features, andHighlighted— and markup that renders a pricing card with a "Most Popular" badge when highlighted. TheFeaturesparameter 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 — there is nothing Pennington-specific about it yet. Mdazor discovers it in the next unit.
Checkpoint — The component compiles but markdown cannot see it
Run dotnet build from examples/BeyondCustomRazorComponentExample. The build succeeds and produces BeyondCustomRazorComponentExample.dll. The PricingCard type exists at BeyondCustomRazorComponentExample.Components.PricingCard but is not yet wired to Mdazor, so a <PricingCard /> tag in markdown renders 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.
- 1
Add
AddMdazorComponent<PricingCard>()toProgram.csOpen
Program.csand add a singlebuilder.Services.AddMdazorComponent<PricingCard>()line after theAddDocSiteblock. The extension lives in theMdazornamespace and ships from theMdazorNuGet package, already transitively referenced throughPennington.DocSite— no package add required.var builder = WebApplication.CreateBuilder(args); builder.Services.AddDocSite(() => new DocSiteOptions { SiteTitle = "Beyond Custom Razor Component", Description = "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);AddMdazorComponent<T>()returnsIServiceCollection, so additional component registrations can chain off the same call. That becomes handy when registering several custom components at once. - 2
Confirm the host still boots
Run the DocSite host to verify the extra DI line did not break startup. No markdown change has been made yet, so the site renders exactly as it did before — the new wiring stays invisible until a page consumes the tag.
Checkpoint — Mdazor knows about PricingCard
Run dotnet run from examples/BeyondCustomRazorComponentExample and visit http://localhost:5000/. The landing page renders without errors and the log shows the site serving on port 5000. PricingCard is now a registered Mdazor component; rendering is proved in the next unit.
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.
- 1
Create
Content/pricing.mdAdd a new markdown page under
Content/with front matter (title: Pricing,description:,order: 20) and two<PricingCard ... />tags between headings. The first card usesTier="Basic" Price="9"; the second addsHighlighted="true"and richer feature text.--- title: Pricing description: Two PricingCard components rendered from markdown with distinct parameter values. order: 20 --- # Pricing 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 different border, a pale accent background, and a "Most Popular" pill at the top. ## 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.Two rules govern how the page works. Tag-name matching is case-sensitive on the leading character —
<PricingCard>must start with a capital letter to be treated as a component candidate. Attribute-to-parameter binding is case-insensitive via reflection, soTier="Pro"binds to[Parameter] public string Tierregardless of casing. - 2
Refresh the pricing page in the browser
With
dotnet runstill active, openhttp://localhost:5000/pricing. Mdazor intercepts each<PricingCard ... />tag, looks up the registered type, instantiates it, assigns parameters from the attributes, renders the component through Blazor's server-sideHtmlRenderer, and inlines the resulting HTML back into the Markdig pipeline's output.
Checkpoint — Two cards render on the pricing page
Visit http://localhost:5000/pricing. Two pricing cards appear: a plain Basic card at $9 / month and a Pro card at $49 / month with a "Most Popular" pill and a thicker accent border. 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.
- 1
Edit the Pro card to change
PriceandFeaturesIn
Content/pricing.md, changePrice="49"toPrice="99"and extend theFeatures=""string with an extra pipe-separated entry (for example,"...|24/7 chat support"). Save the file. - 2
Flip
Highlightedon the Basic cardAdd
Highlighted="true"to the first<PricingCard Tier="Basic" ... />tag. Boolean attribute values from markdown bind with case-insensitivetrue/false—Highlighted="True"andHighlighted="true"both flip the card into its emphasised state.
Checkpoint — The rendered cards reflect the edits
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 has the "Most Popular" pill and the highlighted border
- 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.