---
title: Author a custom Razor component for markdown
description: "Author a PricingCard Razor component inside a DocSite, register it with AddMdazorComponent<T>(), and render it from a markdown page with two parameter sets."
canonical_url: https://usepennington.net/tutorials/beyond-basics/custom-razor-component/
sidecar_url: https://usepennington.net/tutorials/beyond-basics/custom-razor-component.md
content_hash: sha256:13ef62b80fb868b1519230848fb8abc7eebbe6162aeca0435466dc9372a28dd9
tokens: 2966
uid: tutorials.beyond-basics.custom-razor-component
reading_time_minutes: 6
---

Getting Started
# Author a custom Razor component for markdown

Author a PricingCard Razor component inside a DocSite, register it with AddMdazorComponent<T>(), and render it from a markdown page with two parameter sets.

 
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](https://usepennington.net/reference/ui/content.md)'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](https://usepennington.net/tutorials/docsite/scaffold.md) (provides the `AddDocSite` / `UseDocSite` / `RunDocSiteAsync` host shape this tutorial extends)
 - Basic Razor familiarity — a `.razor` file with `@code {}` and `[Parameter]` properties should feel routine
 
 
The finished code for this tutorial lives in [examples/BeyondCustomRazorComponentExample](https://github.com/usepennington/pennington/tree/main/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.

 
```razor
@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 `.csproj` name).

 
**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.

 
```razor:symbol
<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.

 
```csharp:symbol,bodyonly
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.

 
```markdown:symbol
---
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](https://usepennington.net/reference/ui/content.md) 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>()` after `AddDocSite` (or after `AddPennington` on 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.
 
 
[Previous
                
                Add a second locale to your site](https://usepennington.net/tutorials/beyond-basics/add-a-locale.md)[Next
                    
                Publish your docs as Markdown for AI agents](https://usepennington.net/tutorials/beyond-basics/markdown-for-agents.md)