Anudwigna.StaticMarkdownEngine
1.0.0-beta.2
dotnet add package Anudwigna.StaticMarkdownEngine --version 1.0.0-beta.2
NuGet\Install-Package Anudwigna.StaticMarkdownEngine -Version 1.0.0-beta.2
<PackageReference Include="Anudwigna.StaticMarkdownEngine" Version="1.0.0-beta.2" />
<PackageVersion Include="Anudwigna.StaticMarkdownEngine" Version="1.0.0-beta.2" />
<PackageReference Include="Anudwigna.StaticMarkdownEngine" />
paket add Anudwigna.StaticMarkdownEngine --version 1.0.0-beta.2
#r "nuget: Anudwigna.StaticMarkdownEngine, 1.0.0-beta.2"
#:package Anudwigna.StaticMarkdownEngine@1.0.0-beta.2
#addin nuget:?package=Anudwigna.StaticMarkdownEngine&version=1.0.0-beta.2&prerelease
#tool nuget:?package=Anudwigna.StaticMarkdownEngine&version=1.0.0-beta.2&prerelease
Anudwigna.StaticMarkdownEngine
Beta release — Core API is stable. Feedback welcome before 1.0.0 GA.
A zero-configuration .NET library that turns a folder of Markdown files with YAML front matter into a fully-routed ASP.NET Core website — with a built-in content query API inspired by Nuxt Content.
Table of Contents
- Features
- How It Works
- Requirements
- Installation
- Quick Start
- Content API
- Configuration Reference
- YAML Front Matter Reference
- Tag Helper Reference
- CLI Reference
- API Reference
- Dependencies
- License
Features
| Feature | Details |
|---|---|
| Markdown rendering | Powered by Markdig with all advanced extensions: tables, footnotes, code blocks, task lists, and more |
| YAML front matter | Supports title, description, layout, date, and any custom key/value pairs via Extra |
| Internal link rewriting | [About](about.md) links are automatically rewritten to href="/about" |
| Two-tier content model | input/ for static pages, content/ for queryable articles — just like Nuxt Content |
| Content query API | contentRepo.Query() returns IEnumerable<ContentItem> — filter, sort, and paginate with standard LINQ |
| Recursive folder sync | Mirrors your entire input/ tree to output/, preserving directory structure |
| ASP.NET Core routing | app.MapMarkdownPages() one-liner wires up dynamic routes via DynamicRouteValueTransformer |
| Razor Tag Helper | <markdown-content> renders content from a file path or inline HTML string |
| CLI tool | Built-in generate command via System.CommandLine for standalone builds |
| Multi-target | Targets net8.0, net9.0, and net10.0 |
How It Works
The library distinguishes two types of Markdown content:
myapp/
├── input/ ← static pages (about, docs, etc.)
│ ├── index.md → output/index.html → GET /
│ ├── about.md → output/about.html → GET /about
│ └── docs/
│ └── guide.md → output/docs/guide.html → GET /docs/guide
│
└── content/ ← articles (queryable via ContentRepository)
├── my-post.md → output/my-post.html → GET /my-post
└── 2025/
└── news.md → output/2025/news.html → GET /2025/news
At startup, SiteGenerator.Generate() converts pages, and ContentRepository.Build() converts articles. Both write pre-generated HTML to the same output directory. The existing route transformer serves all of it with a single catch-all route.
.md files ──► FrontMatterParser ──► MarkdownConverter ──► .html files
│
MarkdownPageRouteTransformer
│
ASP.NET Core routes
Requirements
- .NET 8, 9, or 10
- ASP.NET Core (
Microsoft.AspNetCore.Appframework reference)
Installation
dotnet add package Anudwigna.StaticMarkdownEngine --prerelease
Or in your .csproj:
<PackageReference Include="Anudwigna.StaticMarkdownEngine" Version="1.0.0-beta.2" />
Quick Start
1. Register services in Program.cs
using Anudwigna.StaticMarkdownEngine.AspNetCore;
using Anudwigna.StaticMarkdownEngine.Content;
using Anudwigna.StaticMarkdownEngine.Core;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
// Pages engine
builder.Services.AddMarkdownEngine(options =>
{
options.OutputDirectory = Path.Combine(builder.Environment.ContentRootPath, "output");
});
// Content query API (optional — only needed if you use the content/ folder)
builder.Services.AddContentRepository(options =>
{
options.ContentDirectory = Path.Combine(builder.Environment.ContentRootPath, "content");
options.OutputDirectory = Path.Combine(builder.Environment.ContentRootPath, "output");
});
var app = builder.Build();
2. Generate HTML at startup
// Pages: input/ → output/
var generator = app.Services.GetRequiredService<SiteGenerator>();
generator.Generate(
inputDir: Path.Combine(app.Environment.ContentRootPath, "input"),
outputDir: Path.Combine(app.Environment.ContentRootPath, "output"));
// Articles: content/ → output/ + in-memory index
var contentRepo = app.Services.GetRequiredService<ContentRepository>();
contentRepo.Build();
app.UseRouting();
app.MapMarkdownPages();
app.Run();
3. Register the Tag Helper
In Views/_ViewImports.cshtml:
@addTagHelper *, Anudwigna.StaticMarkdownEngine
4. Create a Razor view for Markdown pages
In Views/MarkdownPage/Render.cshtml:
@{
Layout = "_Layout";
ViewData["Title"] = ViewBag.Title;
}
<article class="markdown-body">
<markdown-content html-content="@ViewBag.HtmlContent" />
</article>
5. Write Markdown content
input/about.md — a static page:
---
title: "About"
description: "Learn more about this site"
---
# About
Welcome! Check out our [home page](index.md).
content/my-first-post.md — a queryable article:
---
title: "My First Post"
description: "Hello from the content folder"
date: 2025-06-01
tags: intro
---
# My First Post
This article is queryable via `ContentRepository.Query()`.
Content API
The ContentRepository service provides a LINQ-based query API over all articles in your content/ folder. Routes for articles are derived directly from the file path with no prefix — content/my-post.md → /my-post.
Querying articles
// Inject ContentRepository into a controller or Razor Page
public class IndexModel : PageModel
{
private readonly ContentRepository _content;
public IReadOnlyList<ContentItem> TopPosts { get; private set; } = [];
public IndexModel(ContentRepository content) => _content = content;
public void OnGet()
{
// Top 5 articles by date, newest first
TopPosts = _content.Query()
.OrderByDescending(p => p.FrontMatter.Date ?? DateTime.MinValue)
.Take(5)
.ToList();
}
}
Because Query() returns a plain IEnumerable<ContentItem>, any LINQ operator works:
// Filter by a custom front matter field
var tagged = _content.Query()
.Where(p => p.FrontMatter.Extra.TryGetValue("tags", out var t)
&& t?.ToString()?.Contains("dotnet") == true)
.ToList();
// Look up a single article by its URL
var post = _content.GetByRoute("/my-first-post");
Rendering links in a Razor view
@foreach (var post in Model.TopPosts)
{
<article>
<h2><a href="@post.Route">@post.FrontMatter.Title</a></h2>
<time>@post.FrontMatter.Date?.ToString("MMM d, yyyy")</time>
<p>@post.Excerpt</p>
<a href="@post.Route">Read more →</a>
</article>
}
ContentItem properties
| Property | Type | Description |
|---|---|---|
FrontMatter |
FrontMatter |
Parsed YAML metadata (Title, Description, Date, Extra, …) |
Route |
string |
URL route — e.g. /my-post, /2025/news |
Slug |
string |
Route without the leading / |
HtmlContent |
string |
Fully rendered HTML body |
Excerpt |
string |
Plain-text preview (first 200 characters, configurable) |
Configuration Reference
AddMarkdownEngine
builder.Services.AddMarkdownEngine(options =>
{
options.OutputDirectory = Path.Combine(builder.Environment.ContentRootPath, "output");
});
| Property | Type | Default | Description |
|---|---|---|---|
OutputDirectory |
string |
"output" |
Directory containing pre-generated .html files, read by the route transformer |
AddContentRepository
builder.Services.AddContentRepository(options =>
{
options.ContentDirectory = Path.Combine(builder.Environment.ContentRootPath, "content");
options.OutputDirectory = Path.Combine(builder.Environment.ContentRootPath, "output");
options.ExcerptLength = 200; // optional, default is 200
});
| Property | Type | Default | Description |
|---|---|---|---|
ContentDirectory |
string |
"content" |
Absolute path to the folder containing article .md files |
OutputDirectory |
string |
"output" |
Where generated .html files are written (should match AddMarkdownEngine) |
ExcerptLength |
int |
200 |
Maximum plain-text characters included in ContentItem.Excerpt |
YAML Front Matter Reference
Front matter is a YAML block delimited by --- at the top of a Markdown file.
---
title: "Page Title"
description: "A short description shown in meta tags or previews"
layout: "_Layout"
date: 2025-06-15
tags: dotnet, aspnetcore
author: "Ada Lovelace"
---
# Page content starts here
| Field | Type | Default | Description |
|---|---|---|---|
title |
string |
"" |
Page title — available in views as ViewBag.Title |
description |
string |
"" |
Short description for SEO or previews |
layout |
string |
"_Layout" |
Razor layout name to use when rendering |
date |
DateTime? |
null |
Publication date — used for sorting in the content API |
| Extra fields | Dictionary<string, object> |
{} |
Any additional key/value pairs land in FrontMatter.Extra |
Accessing custom fields:
var author = item.FrontMatter.Extra["author"]?.ToString();
Tag Helper Reference
The <markdown-content> tag helper renders HTML content inside a Razor view. It produces no wrapper element — it outputs only the HTML.
From an inline HTML string (recommended)
<markdown-content html-content="@ViewBag.HtmlContent" />
| Attribute | Type | Description |
|---|---|---|
html-content |
string |
Pre-rendered HTML string to output directly |
From a file path
<markdown-content html-file="/absolute/path/to/output/about.html" />
| Attribute | Type | Description |
|---|---|---|
html-file |
string |
Absolute path to a .html file. If the file does not exist, nothing is rendered. |
CLI Reference
CliRunner provides a generate command for use in apps that expose a CLI entry point.
Wiring up in Program.cs
using Anudwigna.StaticMarkdownEngine.Cli;
if (args.Length > 0)
return await CliRunner.RunAsync(args);
// Otherwise start the web app as normal
generate command
dotnet run -- generate [--input <path>] [--output <path>]
| Option | Short | Default | Description |
|---|---|---|---|
--input |
-i |
input |
Path to the directory containing .md source files |
--output |
-o |
output |
Path to the directory where .html files will be written |
Example:
dotnet run -- generate -i content -o wwwroot/pages
Output:
Generating site from 'C:\myapp\content' → 'C:\myapp\wwwroot\pages'...
✓ / → wwwroot/pages/index.html
✓ /about → wwwroot/pages/about.html
Done! Generated 2 page(s).
API Reference
SiteGenerator
Recursively converts all .md files in a source directory to .html files.
var generator = new SiteGenerator(); // or inject via DI
IReadOnlyList<MarkdownPage> pages = generator.Generate(
inputDir: "/path/to/input",
outputDir: "/path/to/output");
ContentRepository
Scans the content/ directory, generates HTML, and provides a LINQ-queryable in-memory index.
// Call once at startup
contentRepo.Build();
// LINQ query
var recent = contentRepo.Query()
.OrderByDescending(p => p.FrontMatter.Date)
.Take(5)
.ToList();
// Direct lookup
ContentItem? post = contentRepo.GetByRoute("/my-post");
| Method | Returns | Description |
|---|---|---|
Build() |
void |
Scans ContentDirectory, generates HTML, populates the index. Safe to call even if the directory does not exist. |
Query() |
IEnumerable<ContentItem> |
Returns all indexed articles for LINQ chaining |
GetByRoute(string) |
ContentItem? |
Looks up an article by its exact route (case-insensitive) |
MarkdownConverter
Converts a single Markdown string to a MarkdownPage.
var converter = new MarkdownConverter(); // or inject via DI
MarkdownPage page = converter.Convert(rawMarkdown);
Console.WriteLine(page.FrontMatter.Title);
Console.WriteLine(page.HtmlContent);
MarkdownPage
| Property | Type | Description |
|---|---|---|
FrontMatter |
FrontMatter |
Parsed YAML metadata |
HtmlContent |
string |
Rendered HTML body (front matter stripped) |
Route |
string |
Derived URL route, e.g. /docs/guide |
OutputFilePath |
string |
Absolute path to the written .html file |
FrontMatterParser
var parser = new FrontMatterParser();
var (frontMatter, body) = parser.Parse(rawMarkdown);
Project Structure
static_markdown_engine/
├── src/
│ └── Anudwigna.StaticMarkdownEngine/ # NuGet package
│ ├── AspNetCore/
│ │ ├── MarkdownEngineExtensions.cs # AddMarkdownEngine / AddContentRepository / MapMarkdownPages
│ │ ├── MarkdownPageRouteTransformer.cs
│ │ ├── MarkdownPageRouteOptions.cs
│ │ └── MarkdownContentTagHelper.cs # <markdown-content> tag helper
│ ├── Cli/
│ │ └── CliRunner.cs # System.CommandLine entry point
│ ├── Content/
│ │ ├── ContentRepository.cs # Query API + HTML generation for articles
│ │ └── ContentRepositoryOptions.cs
│ ├── Core/
│ │ ├── FrontMatterParser.cs # YAML extraction
│ │ ├── LinkRewriter.cs # .md → route link rewriting
│ │ ├── MarkdownConverter.cs # Markdig rendering pipeline
│ │ └── SiteGenerator.cs # Folder sync: input/ → output/
│ └── Models/
│ ├── ContentItem.cs # Article model for the query API
│ ├── FrontMatter.cs
│ └── MarkdownPage.cs
├── tests/
│ └── Anudwigna.StaticMarkdownEngine.Tests/ # xUnit + FluentAssertions (42 tests)
└── samples/
└── Anudwigna.SampleWeb/ # ASP.NET Core demo application
├── content/ # Sample articles (queryable)
└── input/ # Sample static pages
Dependencies
| Package | Version | Purpose |
|---|---|---|
| Markdig | 0.40.0 | Markdown-to-HTML rendering with all advanced extensions |
| YamlDotNet | 16.3.0 | YAML front matter parsing |
| System.CommandLine | 2.0.0-beta4 | CLI generate command |
Microsoft.AspNetCore.App |
(framework ref) | ASP.NET Core routing, Tag Helpers, DI |
License
This project is licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Markdig (>= 0.40.0)
- System.CommandLine (>= 2.0.0-beta4.22272.1)
- YamlDotNet (>= 16.3.0)
-
net8.0
- Markdig (>= 0.40.0)
- System.CommandLine (>= 2.0.0-beta4.22272.1)
- YamlDotNet (>= 16.3.0)
-
net9.0
- Markdig (>= 0.40.0)
- System.CommandLine (>= 2.0.0-beta4.22272.1)
- YamlDotNet (>= 16.3.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-beta.2 | 90 | 2/26/2026 |
| 1.0.0-beta.1 | 68 | 2/26/2026 |
1.0.0-beta.2 — Added ContentRepository: LINQ-queryable content API for the content/ folder, ContentItem model with Excerpt, and AddContentRepository() DI extension.