mostlylucid.MinimalBlog - How Simple Can an ASP.NET Blog Really Be? (English)

mostlylucid.MinimalBlog - How Simple Can an ASP.NET Blog Really Be?

Monday, 01 December 2025

//

10 minute read

Introduction

If you've been following this blog, you might have noticed that my main blogging platform is... let's call it "enthusiastically engineered." PostgreSQL AND vector databases, semantic AND full-text search with GIN indexes, automated translation to 14 languages, multiple hosted services, Hangfire job scheduling, Prometheus metrics, Serilog tracing, HTMX interactions, usign my own nuget packages, and enough Docker containers to make a ship jealous.

That's entirely deliberate. This site is my living lab - a playground where I experiment with technologies, test deployment strategies, measure performance characteristics, and build reusable packages. It's supposed to be over-engineered because that's how I learn: by solving problems that most blogs don't actually have, then packaging those solutions as open-source libraries others can use.

But here's the thing: you probably don't need any of that to run a blog.

That's why I created mostlylucid.MinimalBlog - to show what happens when you strip away all the experimentation and focus on the absolute essentials. No database. No build pipeline. No complexity. Just markdown files in a folder, appearing on the web. This is what a blog looks like when you're not using it as a laboratory.

NOTE: See the end of the article for a link to the source, I plan on releasing this as a nuget package as soon as I get time to ensure it's 100% reliable and it's perf isn't TOO awful (so look for k6 testing articles soon!).

The Philosophy: Less is More

The entire project is designed around one principle: keep it simple. No database, no build pipeline, no JavaScript framework. Just ASP.NET 9.0, Markdig for markdown parsing, and about 500 lines of code total. That's it. NOTE: You COULD even do this client side by using the likes of markdown-it then just have the server site map static .md files and make it even SIMPLER but...well this is an ASP.NET blog (kinda sorta 🤓).

Project Structure

Let's look at how the project is organized:

Mostlylucid.MinimalBlog/
├── Pages/
│   ├── Index.cshtml              # Homepage with post list
│   ├── Post.cshtml                # Individual post page
│   ├── Categories.cshtml          # List of all categories
│   ├── Category.cshtml            # Posts in a category
│   ├── _Layout.cshtml             # Shared layout
│   ├── _ViewImports.cshtml        # Shared imports
│   └── _ViewStart.cshtml          # Layout selection
├── wwwroot/
│   └── css/
│       └── site.css               # All the CSS you need
├── MarkdownBlogService.cs         # Core blog logic
├── MetaWeblogService.cs           # XML-RPC for external editors
├── Program.cs                     # Application setup
├── appsettings.json               # Configuration
└── Mostlylucid.MinimalBlog.csproj # Project file

The Heart: MarkdownBlogService

The core of the blog is the MarkdownBlogService class. It's remarkably simple-just 120 lines of code that handle:

  1. Reading markdown files from a directory
  2. Parsing metadata (title, categories, publish date)
  3. Converting markdown to HTML using Markdig
  4. Caching everything in memory

Here's how it works:

Loading Posts

The service scans a configured directory for .md files and loads them all into memory:

private List<BlogPost> LoadAllPosts()
{
    if (!Directory.Exists(_markdownPath)) return [];

    return Directory.GetFiles(_markdownPath, "*.md", SearchOption.TopDirectoryOnly)
        .Where(f => Path.GetFileName(f).Count(c => c == '.') == 1) // Only base .md files
        .Select(ParseFile)
        .Where(p => p is { IsHidden: false })
        .OrderByDescending(p => p!.PublishedDate)
        .ToList()!;
}

Notice the clever filtering: Count(c => c == '.') == 1 ensures we only get base .md files, not translated versions like post.ar.md or post.de.md (in case you want to add translations later).

Parsing Metadata

Each markdown file follows a simple convention:

# Post Title




Your content here...

The parser extracts this metadata using regular expressions and the Markdig AST:

private BlogPost? ParseFile(string filePath)
{
    var markdown = File.ReadAllText(filePath);
    var slug = Path.GetFileNameWithoutExtension(filePath);
    var document = Markdown.Parse(markdown, _pipeline);

    // Extract title from first H1
    var title = document.Descendants<HeadingBlock>()
        .FirstOrDefault(h => h.Level == 1)?
        .Inline?.FirstChild?.ToString() ?? slug;

    // Extract categories: 
    var categoryMatch = CategoryRegex().Match(markdown);
    var categories = categoryMatch.Success
        ? categoryMatch.Groups[1].Value.Split(',', StringSplitOptions.TrimEntries)
        : [];

    // Extract date: 
    var dateMatch = DateTimeRegex().Match(markdown);
    var publishedDate = dateMatch.Success && DateTime.TryParse(dateMatch.Groups[1].Value, out var dt)
        ? dt : File.GetCreationTimeUtc(filePath);

    return new BlogPost
    {
        Slug = slug,
        Title = title,
        Categories = categories,
        PublishedDate = publishedDate,
        HtmlContent = Markdown.ToHtml(markdown, _pipeline),
        IsHidden = markdown.Contains("<hidden")
    };
}

Caching Strategy

Every method in the service uses IMemoryCache to avoid re-reading and re-parsing files on every request:

public IReadOnlyList<BlogPost> GetAllPosts()
{
    return cache.GetOrCreate("all_posts", entry =>
    {
        entry.SetOptions(CacheOptions);
        return LoadAllPosts();
    }) ?? [];
}

Cache entries have a 30-minute sliding expiration and 2-hour absolute expiration. Simple, effective.

Application Setup: Program.cs

The entire application setup is just 43 lines: Razor Pages, memory cache, output cache, two singleton services, static file serving, and a MetaWeblog XML-RPC endpoint. Everything cached as singletons because nothing changes unless files are modified.

The UI: Simple Razor Pages

The UI is pure server-rendered HTML. No JavaScript, no HTMX, no Alpine.js. The homepage lists posts, the post page renders @Html.Raw(post.HtmlContent) with an [OutputCache] attribute for hour-long HTML caching. Four pages total, each under 30 lines.

Styling: 55 Lines of CSS

The entire visual design is handled by a single CSS file with just 55 lines. It uses CSS custom properties for theming and creates a clean, dark GitHub-inspired look:

:root {
  --bg: #0d1117;
  --bg-card: #161b22;
  --text: #c9d1d9;
  --text-muted: #8b949e;
  --accent: #58a6ff;
  --border: #30363d;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: var(--bg);
  color: var(--text);
  line-height: 1.6;
  max-width: 48rem;
  margin: 0 auto;
  padding: 2rem 1rem;
}

/* ... more styles ... */

No preprocessor. No build step. No thousands of utility classes. Just clean, readable CSS that works.

Bonus Feature: MetaWeblog API

For writers who prefer dedicated markdown editors like Markdown Monster, the project includes a full MetaWeblog API implementation. This XML-RPC API allows external editors to:

  • List posts
  • Create new posts
  • Edit existing posts
  • Delete posts
  • Upload images
  • Retrieve categories

The implementation is in MetaWeblogService.cs and handles the complete XML-RPC protocol, parsing requests and generating responses. This means you can write your blog posts in your favorite editor and publish them directly to your blog.

Configuration

The entire configuration file is just 14 lines:

{
  "MarkdownPath": "../Mostlylucid/Markdown",
  "ImagesPath": "wwwroot/images",
  "MetaWeblog": {
    "Username": "admin",
    "Password": "changeme",
    "BlogUrl": "http://localhost:5000"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}
  • MarkdownPath - where your markdown files live
  • ImagesPath - where images are stored
  • MetaWeblog - credentials for external editor access

Using as a NuGet Package

As mentioned above it will SOON be available but not yet :)

The blog is now available as a NuGet package, making it trivial to add to any ASP.NET Core application:

dotnet add package mostlylucid.MinimalBlog

Then in your Program.cs:

builder.Services.AddRazorPages();
builder.Services.AddMinimalBlog(options =>
{
    options.MarkdownPath = "Markdown";
    options.ImagesPath = "wwwroot/images";
    options.EnableMetaWeblog = false; // Optional, defaults to true
});

var app = builder.Build();

app.UseStaticFiles();
app.UseMinimalBlog();
app.MapRazorPages();
app.Run();

That's it - just two method calls (AddMinimalBlog and UseMinimalBlog) and you have a working blog.

Running the Sample Project

To run the included sample project:

cd Mostlylucid.MinimalBlog
dotnet run

Visit http://localhost:5000 and you'll see the blog with markdown files from the configured path.

Creating Content

To create a new blog post:

  1. Create a new .md file in your configured MarkdownPath
  2. Add the standard metadata:
    # Your Post Title
    
    
    
    
    Your content here...
    
  3. Save the file
  4. The cache will expire within 30 minutes (or restart the app)

To add images, simply place them in your configured ImagesPath directory and reference them in your markdown:

![Alt text](your-image.jpg)

What's Missing (On Purpose)

This minimal blog intentionally doesn't include:

  • Comments - Use a third-party service if needed
  • Search - Keep your content organized with categories
  • Tags - Categories are sufficient for small blogs
  • RSS/Atom - Simple to add if you need it
  • Authentication - MetaWeblog API uses basic auth only
  • Analytics - Add JavaScript snippet if desired
  • SEO optimization - Works fine with basic meta tags
  • Responsive images - Browser handles it
  • Dark/light theme toggle - One theme is enough

These features are all possible to add, but they're not included by default because most small blogs don't need them.

Performance Characteristics

Despite its simplicity, this blog is fast:

  • Memory caching means no file I/O after first load
  • Output caching means no Razor rendering after first request
  • No database means no query overhead
  • No JavaScript means faster page loads
  • Simple CSS means minimal stylesheet parsing

For a small to medium blog (under 1000 posts), this architecture will outperform most database-backed blog platforms.

When to Use This vs. the Full Mostlylucid Blog

Use Mostlylucid.MinimalBlog when:

  • You're starting a personal blog
  • You have fewer than 500 posts
  • You don't need multiple languages
  • You want to keep things simple
  • You're comfortable with markdown files
  • You just want to write and publish

Use the full Mostlylucid platform when:

  • You're using your blog as a learning laboratory for new technologies
  • You want to experiment with deployment strategies, monitoring, and performance optimization
  • You need specific features like multilingual support, full-text search, or comments
  • You're building packages and need a real-world testbed
  • You're documenting complex technical implementations
  • The journey of building the platform is as valuable as the content it hosts

Conclusion: Simplicity as a Feature

In the modern web development world, we often reach for complex solutions by default. Need a blog? Better set up a database, configure an ORM, set up migrations, add caching, implement search, configure background jobs...

But sometimes the simple solution is the right solution. Mostlylucid.MinimalBlog proves that you can build a functional, fast, and maintainable blog platform with:

  • 342 lines of C# (MarkdownBlogService + MetaWeblogService + Program.cs)
  • ~120 lines of Razor markup (4 pages)
  • 55 lines of CSS
  • 1 NuGet dependency (Markdig)

That's less than 520 lines of code total for a complete blogging platform.

The project serves as both a functional blog platform and a reminder: before you add complexity, ask yourself if you really need it. Sometimes a folder full of markdown files is all you need.

You can find the complete source code in the Mostlylucid.MinimalBlog directory of the main repository. I'll release the nuget package as soon as I'm happy with the code.

Happy blogging!

logo

© 2025 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.