📅 October 26, 2024

(Re)building My Blog

tldr: I rebuilt my blog using .NET 9 and Blazor static server-side rendering. If you're reading this, everything worked!

When I started my blog ~5 years ago I wanted something that would be low friction and wouldn't get in the way. Using Netlify and a static site seemed straightforward. I came across a GitHub project that had a starter template that used Jigsaw and included docs for hosting on Netlify. That setup has been working fine for the past several years until recently. I started getting Node/npm version issues in my builds on Netlify. This was mostly my fault as I haven't kept the dependencies up to date. This was the push I needed to decide to build my blog.

I heavily use C# and Blazor at my current job so I thought this would be a good opportunity to learn more about Blazor static server-side rending (SSR). I also thought it would be a good opportunity to build out the CI/CD in GitHub Actions and deploy to a Linux server using containers, something I haven't had a chance to work with much.

So with that in mind, my forecasted tech stack looked something like this:

  • Blazor static server-side rendering running on .NET 9
  • CI/CD via GitHub Actions
  • Hosting via Linux virtual machine with Docker/containers

Create a new project

The first step was creating a new project. I used the Blazor Web App project template. This type of project provides the ability to use server-side or client-side rendering. I chose to use server-side rendering for now as currently, client-side rendering with WASM can result in a large initial download for users. I also chose to forego using any interactivity and rely solely on static server-side rendering for now. The relevant bits in Program.cs are below:

Program.cs

...
builder.Services.AddRazorComponents();
var app = builder.Build();
...
app.MapStaticAssets();
app.MapRazorComponents<App>();
app.Run();

Markdown to HTML

My blog posts are written in Markdown with a metadata section at the top of each file with a few properties:

---
title: My Title Here
date: {{date}}
comments: true
tags:
  - random
---

Lorem ipsum...

I copied my existing Markdown files into a /wwwroot/posts directory in my new app.

I was able to deserialize the Markdown metadata with YamlDotNet.

...
var postsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "posts");
var markdownFile = Path.Combine(postsPath, $"{fileName}.md");
var lines = File.ReadLines(markdownFile).ToList();

if (lines.FirstOrDefault() == "---")
{
  var yamlMetadata = string.Join("\n", lines.Skip(1).TakeWhile(line => line != "---"));
  var yamlDeserializer = new DeserializerBuilder().WithNamingConvention(UnderscoredNamingConvention.Instance).Build();
  try
  {
    var metadata = yamlDeserializer.Deserialize<BlogPostMetadata>(yamlMetadata);
    return metadata;
  }
  catch (Exception)
  {
    Console.WriteLine("Error deserializing: " + fileName);
  }
}

I handled displaying the Markdown content as HTML with Markdig.

var markdownFilePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", $"posts/{fileName}.md");
var markdownContent = File.ReadAllText(markdownFilePath);
var contentWithoutMetadata = RemoveMetadataFromMarkdown(markdownContent);
return Markdig.Markdown.ToHtml(contentWithoutMetadata);

Code blocks in my blog posts are styled using highlight.js. I imported those scripts:

App.razor

<!-- highlight.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/github-dark.min.css">
<!-- highlight.js additional languages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/autohotkey.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/highlightjs-cshtml-razor/dist/cshtml-razor.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/bash.min.js"></script>

I experienced an issue with Blazor static server-side rendering where the highlight.js JavaScript methods weren't getting called when navigating to each blog post. This was solved by utilizing a newer library called BlazorJSComponents. This enabled me to call my highlight.js method on every re-render:

highlight-init.js

export default class extends BlazorJSComponents.Component {
	setParameters(...args) {
		document.querySelectorAll('pre code').forEach((block) => hljs.highlightElement(block));
	}
}

Markdown.razor

<JS Src="./js/highlightjs-init.js" />

@((MarkupString)MarkdownHtml)

Server Configuration

With the app working on my local machine, it was time to figure out how to deploy it to a server. I took this opportunity to learn more about containerization and hosting .NET on Linux.

I have some "free" Linux virtual machines hosted on Oracle Cloud so I decided to use those. The main steps were:

  1. Log into my Linux VM using SSH
  2. Install Docker

This is an oversimplification. In practice, this involved much toe-stubbing, but I'll save those details for another time.

With Docker installed, I used some additional Docker images to make things simpler for setting up a reverse proxy (nginx), certificates (https), and logging:

The setup involved here is probably worthy of a separate blog post.

CI/CD

I am using GitHub to host my blog repository and GitHub Actions were a natural choice for CI/CD. The biggest "issue" was finding a way to connect to my Linux VM. This was accomplished using SSH and utilizing GitHub actions like ssh-action. I plan on creating an opensource repository that shows the full process, but the general build and deploy steps are:

  1. Checkout repository
  2. Login to the GitHub Container Registry (for publishing my .NET project as a container)
  3. Run dotnet publish with the PublishContainer argument (docs)
  4. Copy a docker-compose file to my Linux VM using SCP
  5. Remote into Linux VM using SSH and run docker commands to pull and run the published container

Conclusion

I'm sure I could have utilized one of the many prebuilt static site hosting options available, but I was able to learn some new concepts and technologies by building my blog myself. .NET's integration with containers these days is fantastic and it is nice to be able to package everything in a portable artifact.

#dotnet#blazor#technology#development
An unhandled error has occurred. Reload 🗙