⚠ Deprecated: Legacy, CriticalBugs
Complete solution for converting Markdown files to Blazor Razor pages with runtime rendering, build-time code generation, MSBuild integration, FluentUI styling, syntax highlighting, and YAML frontmatter support. Includes both runtime components and build-time tools in a single package.
$ dotnet add package MarkdownToRazorTransform your Markdown files into beautiful Blazor pages with automatic routing and syntax highlighting.
MarkdownToRazor is a powerful .NET library that bridges the gap between Markdown content and Blazor applications. Whether you're building documentation sites, blogs, or content-driven applications, this library provides everything you need to seamlessly integrate Markdown into your Blazor projects.
🔧 Enhanced Package Quality - Professional-grade NuGet package with debugging support!
Source Link & Debugging:
Build & CI Enhancements:
🎯 Single Unified Package - We've consolidated everything into one powerful package!
Package Consolidation:
MarkdownToRazor.Components, MarkdownToRazor.CodeGeneration, MarkdownToRazor.MSBuild)MarkdownToRazor package with everything included!Modernized Naming:
MarkdownToRazor → MarkdownToRazor (already updated)MarkdownToRazor.* → MarkdownToRazor.* (already updated)AddMarkdownToRazorServices() → AddMarkdownToRazorServices()Framework Support:
// Before v2.0
builder.Services.AddMarkdownToRazorServices("../content");
// After v2.0+ (including v2.0.1)
builder.Services.AddMarkdownToRazorServices("../content");
// Get file-to-route mapping for dynamic navigation
var fileRoutes = await MdFileService.DiscoverMarkdownFilesWithRoutesAsync();
foreach (var (fileName, route) in fileRoutes)
{
Console.WriteLine($"File: {fileName} → Route: {route}");
}
// Relative paths from project root
builder.Services.AddMarkdownToRazorServices("content/docs");
// Multiple folders up (perfect for shared content)
builder.Services.AddMarkdownToRazorServices("../../../SharedDocumentation");
// Absolute paths (cross-project content sharing)
builder.Services.AddMarkdownToRazorServices(@"C:\SharedContent\ProjectDocs");
// Project root directory
builder.Services.AddMarkdownToRazorServices(".");
✨ IGeneratedPageDiscoveryService - Programmatically discover and work with your generated Razor pages!
// Inject the service into your components
@inject IGeneratedPageDiscoveryService PageDiscovery
// Get all pages with metadata
var pages = await PageDiscovery.GetAllPagesAsync();
// Filter by tags
var blogPosts = await PageDiscovery.GetPagesByTagAsync("blog");
// Find pages by route pattern
var apiDocs = await PageDiscovery.GetPagesByRoutePatternAsync("/api/*");
// Build dynamic navigation menus
foreach (var page in pages)
{
Console.WriteLine($"Route: {page.Route}, Title: {page.Title}");
}
Perfect for building:
Single package with everything included!
dotnet add package MarkdownToRazor
dotnet add package MarkdownToRazor --source https://nuget.pkg.github.com/DavidH102/index.json
This is the primary use case - automatically convert markdown files to routable Blazor pages during build:
# Single package with all features included
dotnet add package MarkdownToRazor
Two approaches for build-time generation:
Add to your .csproj:
<PropertyGroup>
<MarkdownSourceDirectory>$(MSBuildProjectDirectory)\content</MarkdownSourceDirectory>
<GeneratedPagesDirectory>$(MSBuildProjectDirectory)\Pages\Generated</GeneratedPagesDirectory>
</PropertyGroup>
<Target Name="GenerateMarkdownPages" BeforeTargets="Build">
<Exec Command="dotnet run --project path/to/MarkdownToRazor.CodeGeneration -- "$(MarkdownSourceDirectory)" "$(GeneratedPagesDirectory)"" />
</Target>
# Run code generation manually
cd CodeGeneration
dotnet run -- "../content" "../Pages/Generated"
Service Registration for Dynamic Navigation:
Even for build-time scenarios, you often want service registration to build dynamic navigation menus from discovered routes:
using MarkdownToRazor.Components.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
// Register services to enable dynamic UI generation from discovered routes
builder.Services.AddMarkdownToRazorServices(options =>
{
options.SourceDirectory = "content"; // Where your markdown files are located
options.BaseRoutePath = "/docs"; // Optional route prefix for generated pages
// OutputDirectory NOT needed - files are generated at build-time, not runtime
});
var app = builder.Build();
// ... rest of configuration
Dynamic Navigation Example:
@inject IMdFileDiscoveryService MdFileDiscovery
<FluentNavGroup Title="Documentation" Icon="@(new Icons.Regular.Size24.DocumentText())">
@{
var documentationRoutes = await MdFileDiscovery.DiscoverMarkdownFilesWithRoutesAsync();
var orderedFiles = documentationRoutes.OrderBy(kvp => GetFileDisplayName(kvp.Key)).ToList();
}
@foreach (var (filename, route) in orderedFiles)
{
var displayName = GetFileDisplayName(filename);
<FluentNavLink Href="@route" Match="NavLinkMatch.All"
Icon="@GetDocumentationIcon(displayName)">
@displayName
</FluentNavLink>
}
</FluentNavGroup>
For displaying dynamic markdown content without generating pages:
Your Blazor Page:
@page "/docs"
@using MarkdownToRazor.Components
@inject IMdFileDiscoveryService MdFileDiscovery
<MarkdownSection Content="@markdownContent" />
<!-- Optional: Display discovered files -->
<MarkdownFileExplorer />
@code {
private string markdownContent = @"
# Welcome to My Documentation
This is **bold text** and this is *italic text*.
```cs
public class Example
{
public string Name { get; set; } = ""Hello World"";
}
```
";
}
}
```
### 2. Build-Time Page Generation
Automatically convert markdown files to routable Blazor pages:
```bash
dotnet add package MarkdownToRazor
Create markdown files with YAML frontmatter:
content/about.md:
---
title: About Us
route: /about
layout: MainLayout
---
# About Our Company
We build amazing software...
Or use HTML comment configuration (new in v1.2.0):
content/about.md:
<!-- This is configuration data -->
<!-- @page "/about" -->
<!-- title: About Us -->
<!-- layout: MainLayout -->
# About Our Company
We build amazing software...
💡 Tip: HTML comment configuration takes precedence over YAML frontmatter when both are present. This provides flexibility for different authoring preferences and tool compatibility.
Add to your .csproj:
<PropertyGroup>
<MarkdownSourceDirectory>$(MSBuildProjectDirectory)\content</MarkdownSourceDirectory>
<GeneratedPagesDirectory>$(MSBuildProjectDirectory)\Pages\Generated</GeneratedPagesDirectory>
</PropertyGroup>
<Target Name="GenerateMarkdownPages" BeforeTargets="Build">
<Exec Command="dotnet run --project path/to/MarkdownToRazor.CodeGeneration -- "$(MarkdownSourceDirectory)" "$(GeneratedPagesDirectory)"" />
</Target>
Result: Automatic /about route with your markdown content as a Blazor page!
What happens:
content/about.md → Pages/Generated/About.razor.razor files have @page "/about" directivesWhen to use: Documentation sites, blogs, content-driven applications where routes are known at build time.
Key insight: Service registration is NOT for generating files - it's for building dynamic UI from the files that exist.
// Service registration enables dynamic navigation, NOT file generation
builder.Services.AddMarkdownToRazorServices(options =>
{
options.SourceDirectory = "content"; // Where markdown files are
options.BaseRoutePath = "/docs"; // Route prefix for generated pages
// OutputDirectory NOT needed - files generated by build tools
});
Dynamic Navigation Example:
@inject IMdFileDiscoveryService DiscoveryService
<FluentNavGroup Title="Documentation">
@foreach (var route in markdownRoutes)
{
<FluentNavLink Href="@route.Route">@route.Title</FluentNavLink>
}
</FluentNavGroup>
@code {
private MarkdownRouteInfo[] markdownRoutes = Array.Empty<MarkdownRouteInfo>();
protected override async Task OnInitializedAsync()
{
markdownRoutes = await DiscoveryService.DiscoverMarkdownFilesWithRoutesAsync();
}
}
What happens:
MarkdownSection component<MarkdownSection FromAsset="file.md" />When to use: Dynamic content systems, CMS-like scenarios, when content changes frequently.
Important: Runtime scenarios do NOT automatically create routable pages - you must create the pages manually.
MarkdownToRazor follows convention-over-configuration principles to automatically discover and process your markdown files:
The AddMarkdownToRazorServices method supports various path configurations:
// Relative paths from project root
services.AddMarkdownToRazorServices("docs/content");
services.AddMarkdownToRazorServices("../../../SharedDocumentation");
// Project root directory
services.AddMarkdownToRazorServices(".");
// Absolute paths (useful for shared content across projects)
services.AddMarkdownToRazorServices(@"C:\SharedDocs\ProjectDocs");
// Advanced configuration with recursive search
services.AddMarkdownToRazorServices(options => {
options.SourceDirectory = "content/posts";
options.SearchRecursively = true; // Finds files in all subdirectories
options.FilePattern = "*.md";
});
Default Behavior:
MDFilesToConvert/ (relative to your project root)Pages/Generated/ (relative to your project root)*.md files are discovered recursivelyDirectory Structure Example:
YourProject/
├── MDFilesToConvert/ ← Source markdown files
│ ├── about.md ← Becomes /about route
│ ├── docs/
│ │ ├── getting-started.md ← Becomes /docs/getting-started route
│ │ └── api-reference.md ← Becomes /docs/api-reference route
│ └── blog/
│ └── 2024/
│ └── news.md ← Becomes /blog/2024/news route
├── Pages/
│ └── Generated/ ← Auto-generated Razor pages
│ ├── About.razor ← Generated from about.md
│ ├── DocsGettingStarted.razor
│ ├── DocsApiReference.razor
│ └── Blog2024News.razor
└── YourProject.csproj
For Build-Time Generation (Most Common):
// Program.cs - Service registration for dynamic navigation only
builder.Services.AddMarkdownToRazorServices(options =>
{
options.SourceDirectory = "content"; // Where to find .md files
options.BaseRoutePath = "/docs"; // Optional route prefix
options.DefaultLayout = "MainLayout"; // Default layout component
options.EnableYamlFrontmatter = true; // Enable YAML frontmatter
// OutputDirectory NOT needed - files generated by build tools
});
For Runtime Rendering (Advanced scenarios):
// Program.cs - Service registration for file loading and rendering
builder.Services.AddMarkdownToRazorServices(options =>
{
options.SourceDirectory = "content"; // Where to find .md files
options.FilePattern = "*.md"; // File pattern to search for
options.SearchRecursively = true; // Search subdirectories
options.EnableHtmlCommentConfiguration = true; // Enable HTML comment config
options.EnableYamlFrontmatter = true; // Enable YAML frontmatter
// OutputDirectory NOT used for runtime scenarios
});
MSBuild Configuration (Build-Time Only):
<PropertyGroup>
<!-- Customize source directory for code generation -->
<MarkdownSourceDirectory>$(MSBuildProjectDirectory)\docs</MarkdownSourceDirectory>
<!-- Customize output directory for generated .razor files -->
<GeneratedPagesDirectory>$(MSBuildProjectDirectory)\Pages\Auto</GeneratedPagesDirectory>
</PropertyGroup>
Simple Service Registration Shortcuts:
// Use defaults for navigation discovery
builder.Services.AddMarkdownToRazorServices();
// Custom source directory only
builder.Services.AddMarkdownToRazorServices("content");
Use service registration to build navigation menus and UI from your markdown files:
Choose Your Service:
IMdFileDiscoveryService - Basic route discovery (faster, simpler)IGeneratedPageDiscoveryService - Rich metadata with titles, descriptions, tags (more features)@inject IMdFileDiscoveryService FileDiscovery
@code {
private MarkdownRouteInfo[] navigationRoutes = Array.Empty<MarkdownRouteInfo>();
protected override async Task OnInitializedAsync()
{
// Get files with their generated routes for navigation
navigationRoutes = await FileDiscovery.DiscoverMarkdownFilesWithRoutesAsync();
}
}
// Build navigation UI
<FluentNavGroup Title="Documentation">
@foreach (var route in navigationRoutes)
{
<FluentNavLink Href="@route.Route">@route.Title</FluentNavLink>
}
</FluentNavGroup>
Legacy File Discovery (if needed):
// Get all markdown file paths (less common)
var markdownFiles = await FileDiscovery.DiscoverMarkdownFilesAsync();
<!-- Components/DocumentationNav.razor -->
<FluentNavGroup Title="Documentation">
@foreach (var route in documentationRoutes.Where(r => r.Route.StartsWith("/docs")))
{
<FluentNavLink Href="@route.Route"
Title="@route.Description">
@route.Title
</FluentNavLink>
}
</FluentNavGroup>
<FluentNavGroup Title="Blog Posts">
@foreach (var route in documentationRoutes.Where(r => r.Route.StartsWith("/blog")))
{
<FluentNavLink Href="@route.Route">@route.Title</FluentNavLink>
}
</FluentNavGroup>
@code {
[Inject] private IMdFileDiscoveryService FileDiscovery { get; set; } = default!;
private MarkdownRouteInfo[] documentationRoutes = Array.Empty<MarkdownRouteInfo>();
protected override async Task OnInitializedAsync()
{
documentationRoutes = await FileDiscovery.DiscoverMarkdownFilesWithRoutesAsync();
}
}
Route Generation Examples:
index.md → / (root route)getting-started.md → /getting-starteduser_guide.md → /user-guide (underscores become hyphens)API Reference.md → /api-reference (spaces normalized)Available Services:
IMdFileDiscoveryService - Discover markdown files based on configurationIStaticAssetService - Load markdown content from configured directoriesIGeneratedPageDiscoveryService - Discover generated Razor pages with routes and metadata (new!)MarkdownToRazorOptions - Access current configuration settingsNew in v1.3.0! The IGeneratedPageDiscoveryService allows you to build dynamic navigation and UI components by discovering all generated Razor pages with their routes and metadata.
Perfect for:
@inject IGeneratedPageDiscoveryService PageDiscovery
@code {
private List<GeneratedPageInfo> pages = new();
protected override async Task OnInitializedAsync()
{
pages = (await PageDiscovery.DiscoverGeneratedPagesAsync()).ToList();
}
}
<!-- Components/DynamicNavigation.razor -->
@inject IGeneratedPageDiscoveryService PageDiscovery
<FluentNavGroup Title="Documentation">
@foreach (var page in documentationPages.Where(p => p.Route.StartsWith("/docs") && p.ShowTitle))
{
<FluentNavLink Href="@page.Route"
Title="@page.Description">
@page.Title
</FluentNavLink>
}
</FluentNavGroup>
<FluentNavGroup Title="Blog Posts">
@foreach (var page in documentationPages.Where(p => p.Route.StartsWith("/blog")))
{
<FluentNavLink Href="@page.Route">@page.Title</FluentNavLink>
}
</FluentNavGroup>
@code {
private GeneratedPageInfo[] documentationPages = Array.Empty<GeneratedPageInfo>();
protected override async Task OnInitializedAsync()
{
documentationPages = (await PageDiscovery.DiscoverGeneratedPagesAsync()).ToArray();
}
}
<!-- Components/ContentBrowser.razor -->
@inject IGeneratedPageDiscoveryService PageDiscovery
<FluentSelect Items="@allTags" @bind-SelectedOption="@selectedTag">
<OptionTemplate>@context</OptionTemplate>
</FluentSelect>
@if (!string.IsNullOrEmpty(selectedTag))
{
<div class="content-grid">
@foreach (var page in filteredPages)
{
<FluentCard>
<FluentAnchor Href="@page.Route">@page.Title</FluentAnchor>
<p>@page.Description</p>
<div class="tags">
@foreach (var tag in page.Tags)
{
<FluentBadge>@tag</FluentBadge>
}
</div>
</FluentCard>
}
</div>
}
@code {
private List<GeneratedPageInfo> allPages = new();
private List<string> allTags = new();
private string selectedTag = "";
private List<GeneratedPageInfo> filteredPages = new();
protected override async Task OnInitializedAsync()
{
allPages = (await PageDiscovery.DiscoverGeneratedPagesAsync()).ToList();
allTags = allPages.SelectMany(p => p.Tags).Distinct().OrderBy(t => t).ToList();
}
private void OnTagSelected()
{
filteredPages = allPages.Where(p => p.Tags.Contains(selectedTag)).ToList();
}
}
public class GeneratedPageInfo
{
public string Route { get; set; } // Page route (e.g., "/docs/getting-started")
public string Title { get; set; } // Page title from frontmatter or filename
public string? Description { get; set; } // Meta description
public string[] Tags { get; set; } // Tags for categorization
public bool ShowTitle { get; set; } // Whether to display title
public string? Layout { get; set; } // Layout component name
public string FilePath { get; set; } // Original markdown file path
}
// Filter by tag
var docPages = await PageDiscovery.DiscoverGeneratedPagesAsync("documentation");
// Get all pages
var allPages = await PageDiscovery.DiscoverGeneratedPagesAsync();
// Filter programmatically
var blogPosts = allPages.Where(p => p.Route.StartsWith("/blog/"));
var recentPosts = allPages.Where(p => p.Tags.Contains("recent"));
2. Using MSBuild Package (Zero Configuration):
dotnet add package MarkdownToRazor --source https://nuget.pkg.github.com/DavidH102/index.json
✨ Zero Config: The MSBuild package automatically uses conventions and runs during build!
3. Manual Tool Execution:
dotnet run --project MarkdownToRazor.CodeGeneration -- "source-dir" "output-dir"
What Gets Processed:
.md files in source directory (recursive)What Gets Generated:
.razor file per markdown file@page directiveMarkdownSection component for contentRoute Generation Examples:
Source File → Generated Route
about.md → /about
docs/getting-started.md → /docs/getting-started
blog/2024/my-post.md → /blog/2024/my-post
With @page directive:
docs/quick-start.md → /quick-start (if @page "/quick-start")
1. Organize by Content Type:
MDFilesToConvert/
├── docs/ ← Documentation pages
├── blog/ ← Blog posts
├── guides/ ← User guides
└── legal/ ← Legal pages (privacy, terms)
2. Use Meaningful File Names:
✅ getting-started.md → /getting-started
✅ api-reference.md → /api-reference
❌ page1.md → /page1 (not descriptive)
3. Include in Version Control:
<!-- Include markdown files in your project -->
<ItemGroup>
<Content Include="MDFilesToConvert\**\*.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
4. Configure Build Integration:
<!-- Automatic generation on build -->
<Target Name="GenerateMarkdownPages" BeforeTargets="Build">
<!-- Your generation command -->
</Target>
<!-- Clean generated files -->
<Target Name="CleanGeneratedPages" BeforeTargets="Clean">
<RemoveDir Directories="$(GeneratedPagesDirectory)" />
</Target>
For complete guides and examples, visit our documentation:
We welcome contributions! Here's how to get involved:
This project is licensed under the MIT License - see the LICENSE file for details.
Built with these amazing open-source projects:
⭐ Found this helpful? Give us a star and share with your fellow developers!