---
title: How I Replaced WordPress with a .NET Razor Pages Site in a Single Session
slug: replacing-wordpress-with-dotnet-razor-pages
date: 2026-03-19
category: AWS
description: I replaced my bloated WordPress site with a lean ASP.NET Core Razor Pages app — no database, markdown-powered, deployed to the same Lightsail server. Here's exactly how I did it.
---

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](/blog/yaml-frontmatter-explained), 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.

```csharp
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:

```yaml
---
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`.

```csharp
// 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:

```bash
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

```ini
# /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
# 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](/contact).*