AWS

How I Replaced WordPress with a .NET Razor Pages Site in a Single Session

2026-03-19

dotnet razor-pages wordpress aws lightsail migration

My WordPress site had 122 blog posts, ~105 pages, a MySQL database, PHP, and a pile of plugins I hadn't touched in months. It was slow to update, expensive to maintain mentally, and felt like driving a school bus to the grocery store.

So I replaced it with a .NET Core Razor Pages app. No database. No CMS. No PHP. Just markdown files, C#, and Bootstrap. Here's the full playbook.

Why Leave WordPress?

WordPress is great — until it isn't. For a content-focused personal site, the overhead was hard to justify:

  • Plugin fatigue — Security updates, compatibility issues, and plugins I installed years ago for features I no longer use
  • Database dependency — MySQL running 24/7 to serve what is essentially static content
  • Performance — Page load times were fine, but they could be great
  • Cost — Not dollars (Lightsail is cheap), but time. Every WordPress update was a risk assessment

What I actually needed was a fast site that renders blog posts, drives readers to my book, and has a contact form. That's it.

The Architecture

I landed on ASP.NET Core 9 Razor Pages with a dead-simple content pipeline:

  • Blog posts are markdown files with YAML frontmatter, stored right in the repo
  • No database — a singleton service loads all posts into memory at startup
  • Bootstrap 5 for styling (already bundled by the dotnet template)
  • Markdig for markdown-to-HTML rendering
  • YamlDotNet for parsing frontmatter

The entire "CMS" is about 100 lines of C# in a MarkdownService class. It reads .md files from a Content/posts/ directory, parses the YAML header, converts the markdown to HTML, and caches everything in a dictionary keyed by slug.

public class MarkdownService : IMarkdownService
{
    private Dictionary<string, BlogPost> _posts = new();

    // Loads all .md files at startup
    // FileSystemWatcher reloads in dev mode
    // Exposes: GetAll(), GetBySlug(), GetByCategory(), GetFeatured()
}

Each markdown file looks like this:

---
title: "AWS Explained: What is EC2?"
slug: "aws-explained-what-is-ec2"
date: 2023-06-15
category: "AWS"
description: "A beginner-friendly explanation of Amazon EC2..."
tags: ["aws", "ec2", "cloud"]
featured: true
---

Your content here in standard markdown...

Adding a new blog post means creating a .md file and deploying. That's it.

Preserving WordPress URLs

This was the most important technical requirement. I had 122 posts indexed by Google with URLs like natthompson.com/aws-explained-what-is-ec2. Breaking those would tank my SEO.

The routing strategy uses three layers:

  1. Static Razor Pages handle /about, /blog, /contact, etc.
  2. /blog/{slug} is handled by Blog/Post.cshtml with an explicit route
  3. MapFallbackToPage("/Slug") catches any /{old-wordpress-slug} and renders the post if found, returns 404 if not

A TrailingSlashMiddleware strips trailing slashes with a 301 redirect, so /aws-explained-what-is-ec2/ (WordPress style) redirects cleanly to /aws-explained-what-is-ec2.

// In Program.cs
app.MapRazorPages();
app.MapFallbackToPage("/Slug");  // Catches old WordPress URLs

Every old WordPress URL just works.

The Content Migration

WordPress has a built-in export tool, but since the domain was already pointing to the new site, I used WP-CLI directly on the server:

sudo wp export --path=/opt/bitnami/wordpress --dir=/tmp --post_type=post

Then a Python script (wp-export.py) converted the WXR XML into individual markdown files with frontmatter. The script handles:

  • HTML to markdown conversion (headings, links, lists, code blocks)
  • Category mapping from WordPress's sprawl into 6 clean categories
  • Slug extraction for URL preservation
  • Tag extraction

From 122 WordPress posts, I curated down to 79 keepers — cutting outdated content, duplicates, and off-brand posts.

Deploying to the Same Lightsail Server

Here's where it gets interesting. My WordPress site ran on a Bitnami Lightsail instance with Apache. Instead of spinning up new infrastructure, I deployed the .NET app alongside WordPress on the same server:

  1. Installed the .NET 9 ASP.NET runtime via the official install script (~45 MB)
  2. Published the app locally with dotnet publish targeting linux-x64
  3. SCP'd the build to the server
  4. Created a systemd service running Kestrel on localhost:5001
  5. Added an Apache vhost for natthompson.com that reverse-proxies to the .NET app
# /etc/systemd/system/natthompson.service
[Service]
WorkingDirectory=/home/bitnami/natthompson
ExecStart=/home/bitnami/.dotnet/dotnet NatThompson.Web.dll
Environment=ASPNETCORE_URLS=http://localhost:5001
# Apache vhost
<VirtualHost *:443>
    ServerName natthompson.com
    SSLEngine on
    ProxyPreserveHost On
    ProxyPass / http://localhost:5001/
    ProxyPassReverse / http://localhost:5001/
</VirtualHost>

The existing Let's Encrypt SSL certificate required zero changes. WordPress is still on disk as a fallback — I just pointed the Apache vhost to the .NET app instead.

Total hosting cost change: $0. Same Lightsail instance, same price.

The Extras

A few features I added that WordPress gave me "for free" but were trivial to implement:

  • RSS feed at /feed — a minimal endpoint in Program.cs that generates XML from the post list
  • Sitemap at /sitemap.xml — auto-generated from all posts plus static pages
  • Contact form with AWS SES integration and reCAPTCHA v3 (invisible)
  • Newsletter signup — embedded Kit (ConvertKit) form posting directly to their API
  • Google Analytics — one <script> tag in the layout
  • SEO meta tags — Open Graph and Twitter Card tags driven by each page's ViewData

The Results

Metric WordPress .NET Razor Pages
Database MySQL (always running) None
Runtime PHP + Apache .NET + Kestrel behind Apache
Memory usage ~860 MB ~25 MB
Dependencies 12+ plugins 3 NuGet packages
Deploy process FTP/SSH + pray dotnet publish + scp + systemctl restart
Adding a post Log into WP admin Create a .md file
Hosting cost ~$5/mo ~$5/mo (same server)

The memory drop alone is remarkable. WordPress with MySQL was consuming ~860 MB. The .NET app uses about 25 MB.

Would I Recommend This?

If you're a developer with a content-focused site and you're comfortable with C# — absolutely. The simplicity of markdown files in a repo is hard to beat. Version control for your blog posts. No database backups to worry about. Deployments are a one-liner.

If you're not a developer, stick with WordPress. It exists for a reason, and the ecosystem is unmatched.

The whole migration — from empty dotnet new to live production with 79 migrated posts — took a single working session. The hardest part was deciding which posts to keep.


The source code for this site is a standard ASP.NET Core 9 Razor Pages project. If you're interested in the technical details or want to do something similar, get in touch.


// enjoyed this post?