It's here. It was inevitable. The explanation of the site you're visiting, posted to the site you're visiting. And here you are, trapped or otherwise consumed by it, like an ouroboros with an unassuming audience in the middle.

Perhaps that's too dramatic. Oh well, on to what you (hopefully) came here for, the explanation.
It's no secret that I've written a lot of C# at this point in my career. Not only was it pretty much the only language I used at my old job, I also have done a great deal of game development in Unity. Despite all of that, I'm actually a pretty big fan of other web stacks. But the .NET world has sucked me in once again and there are a few reasons for that.
First, I wanted something that would be easy for me to maintain, and as I mentioned, I've written a lot of C#. What's a little more?
Second, I was pretty sure I'd need some kind of a specialized markdown parser, or at least something a little more advanced than a simple HTML tag with a bunch of text stuffed into it, rendered dynamically.
Third, the initial, uglier version of the site was created in Blazor so I figured I'd stick with it. The other members of my team are more familiar with C# than anything else anyways, so it makes sense to stick with it.
Aside from the blog, to be honest, there really isn't much that's all that interesting about the site. Basically it's a Blazor WASM site built on Azure, and using GitHub Actions for the CI/CD (what little deployment automation I need for something this lightweight)
What's really interesting is...
All of the blog content lives in a folder that looks something like this:
Posts/2025-04-15-blazor-markdown-blog.md
Posts/2025-04-28-another-cool-feature.md
And each file is mapped with a .json file that looks a little like this:
[
{
"id": "welcome-to-the-lab",
"author": "Henry",
"publishDate": "April 29th, 2025",
"description": "An introduction to the Latent Arcana Blog",
"tag": "meta",
"title": "Welcome to the LAB"
},
{
"id": "about-the-site",
"author": "Henry",
"publishDate": "May 1st, 2025",
"description": "A breakdown of the site and how it was made",
"tag": "web-dev",
"title": "The Self-Referential Blog Post"
}
]
Then I basically just wrote a C# console program that converts the markdown files into HTML that can be rendered. I use Anglesharp and Markdig to do the conversion work.
Here's some sample code doing a little of what I'm talking about, using those two packages.
foreach (var mdFile in Directory.GetFiles(inputDir, "*.md"))
{
string markdown = await System.IO.File.ReadAllTextAsync(mdFile);
string html = Markdown.ToHtml(markdown, pipeline);
var document = await context.OpenAsync(req => req.Content(html));
foreach (var heading in document.QuerySelectorAll("h1, h2, h3"))
{
string titleText = heading.TextContent;
string id = titleText.ToLower().Replace(" ", "-");
var newDiv = document.CreateElement("div");
newDiv.ClassList.Add("row content-title");
// Title Text
var titleDiv = document.CreateElement("div");
titleDiv.ClassList.Add("col-12 col-md-6 text-center text-md-start");
titleDiv.InnerHtml = $@"<span>{titleText}</span>";
barcodeDiv.InnerHtml = $@"<a href='/Blog/{Path.GetFileNameWithoutExtension(mdFile)}#{id}'>
<span class='section-barcode' id='{id}'>{headerID}</span>
</a>";
newDiv.AppendChild(titleDiv);
newDiv.AppendChild(barcodeDiv);
heading.ReplaceWith(newDiv);
}
foreach (var paragraph in document.QuerySelectorAll("p"))
{
var newDiv = document.CreateElement("div");
newDiv.ClassList.Add("content-body");
newDiv.InnerHtml = paragraph.InnerHtml;
paragraph.ReplaceWith(newDiv);
}
foreach (var img in document.QuerySelectorAll("img"))
{
img.ClassList.Add("img-fluid");
}
// === Save HTML ===
string fileName = Path.GetFileNameWithoutExtension(mdFile);
string outputPath = Path.Combine(outputDir, $"{fileName}.html");
await System.IO.File.WriteAllTextAsync(outputPath, document.Body.InnerHtml);
}
At a high level what's happening is I'm iterating over each markdown file, converting it to HTML on the fly, swapping out my own classes and style settings for each tag I happen to care about, and then I save the HTML to a folder that the site can actually use.
So blog-post.md becomes blog-post.html, with all of my custom classes attached already. Then the blog index page iterates over the .json I mentioned earlier. You can see the data I shared there is actually real index data for the blog. Then the magic happens here:
posts = await Http.GetFromJsonAsync<BlogPost[]>("blog/blog.json");
// NOT PICTURED: Grab data out of the posts list before setting DisplayPost to it
if(DisplayPost != null)
{
RenderedContent = await Http.GetStringAsync($"blog/{DisplayPost.id}.html");
}
And there you have it. What's great about this setup in GitHub Actions is that I'm able to run the Markdown Preprocessor as a step in the Action before running the Blazor WASM site's deployment.
All that needs to be done to get the blog working on the site when pushing a new post is to make sure your .md file is located in the Posts folder, and your posts.json file is updated to reference the file you added. It uses the name of the file as the id in my case, but you could just as easily assign an id to each post when you create it.

Basically the reason I did all of this is because I refuse to use a CMS for something as simple as posting markdown to a website. And sure I could use Gatsby, or Jekyll, or any number of simple SPA tools that can read markdown. But in this case I really wanted to have control over the backend. In this case my "backend" is really just a simple function for parsing and saving the HTML, which runs every time the site gets deployed.
At the end of the day this is really lightweight. It wasn't particularly fast to build. It's not super optimized. But it's stupid easy to maintain, and even easier (and stupider?) to change it to meet my needs when I need to build a new feature.
And really, what better reason is there to build something than to enjoy using it more? I think software should be easy to use but I also think if you can make it fun to use, you should. "Fun" is up to interpretation but I know I had a lot of fun making this, and I am already enjoying using it.
I'm sure that's no secret by now...