Blog v2: Leaner and Meaner

Blog v2: Leaner and Meaner

Stefan Hodges-Kluck

by Stefan Hodges-Kluck on March 13, 2025

A year ago, I made this blog site as a way both to expand my skills and to write about my life. I set up a Dart Frog API to fetch content from my CMS, a Flutter Web app to display content in browsers, and a CI/CD workflow that leverages AWS App Runner and Amplify. It was a great chance to delve more deeply into all these tools, and I am very glad that I took the time to put all this together. 

I also found a few significant pain points, right from the start. 

Flutter: Great for Apps, Less Great for Sites

I don't want to go into too much debate over what the difference is between a web site and a web app, but this blog, with its read-only content, is more site than app. Flutter is a fantastic choice for web apps, especially now that they support Web Assembly (WASM) out of the box. For web sites, however, it might not be the best option. 

Search Engine Optimization (SEO), the ability to add appropriate metadata tags and useful content to allow content to be interpreted by a search engine, is something that Flutter Web doesn't do too great. A Flutter Web app creates a single-page application (SPA) that is rendered on your website’s root index.html page. This follows the pattern of other major web frameworks like Angular and React, and provides some great benefits if your app is complex and involves lots of logic around user authentication, managing state, and persisting data. But it also means that your entire app is being displayed on only one HTML page. SEO works best when you can add unique descriptive metadata tags to each piece of content you display. With only one index.html file presenting every page of your site, it is difficult to make this dynamic. This always bugged me when I shared blog posts–the preview of my post displayed metadata for my site, not for the individual posts.

Now there are ways to add custom metadata to a Flutter Web app’s index.html file–the meta_seo package does just this. But, that still leaves another pain point for my blog site: rendering HTML. My CMS returns blog posts as HTML content, which I need to render on the page. Flutter doesn't do this well out-of-the-box, so I first opted for a popular package to render HTML into widgets, only to encounter an error that broke the site for some iPhone users. Since that package was stale for a long time, I had to switch to a different package with more recent updates. But I still didn't feel great about having a third-party dependency for something so vital to my site. Additionally, rendering HTML into widgets always seemed a bit circuitous to me: I was getting HTML from my API, then getting the HTML rendered into Flutter widgets. Why couldn’t I just render the HTML that I get from the CMS?

Turns out, with Dart Frog, it's possible to do exactly that. 

Simple SSR with Dart Frog

With the ability to serve static files, Dart Frog can respond to HTTP calls with HTML content that can be rendered in-browser. This makes it possible to use Dart Frog for server-side rendering, which is a great way to improve site performance and optimize SEO. As before, I fetch blog content via a package dedicated to pulling data from my CMS. However, instead of delivering that content to the client as JSON, now I deliver an HTML response by specifying in the response header that the content is text/html

Future<Response> _get(RequestContext context) async {
  final request = BlogsRequest.fromJson(context.request.uri.queryParameters);

  final (statusCode, html) =

     await context.read<BlogRepository>().getBlogOverviewHtml(
           limit: request.limit,
           offset: request.offset,
          );

 return Response(
   statusCode: statusCode,
   body: html,
   headers: {'content-type': 'text/html'},
 );
}

The biggest challenge to this approach is how to efficiently write dynamic HTML on the server side. I wanted to have the ability to make HTML templates with dynamic data inserted into them based on the results of my API calls. To do this, I built a package that parses content from HTML files and inserts dynamic data following mustache syntax. After fetching a blog post from the API, my Dart Frog app reads my HTML templates, inserts data into them, then returns the updated HTML to be rendered on the client. Here is my template for a blog post:

<div class="container-xl">
 <header class="w-full px-2 md:px-4 lg:px-8 text-text-light dark:text-text-dark">
   {{#featuredImage}}
   <img src="{{featuredImage}}" alt="{{title}}" class="w-full h-96 object-cover object-center">
   {{/featuredImage}}
   <h1 class="pt-4 pb-2 text-4xl font-bold">{{title}}</h1>
   <div class="pt-2 pb-4 flex flex-row flex-wrap items-center">
     {{#authorImage}}
     <img src="{{authorImage}}" alt="{{authorName}}" class="w-12 h-12 rounded-full object-cover object-center pr-2"/>
     {{/authorImage}}
     <p class="tex-sm text-text-light dark:text-text-dark">
       by {{authorName}} on {{published}}
     </p>
   </div>
 </header>
 <article class="mx-auto px-2 prose text-text-light dark:text-text-dark">
   {{body}}
 </article>
</div>

And here is how I inject data into the template in my Dart code:

      final html = await _templateEngine.render(
       filePath: 'blog_detail_page.html',
       context: {
         'title': blogDetail.title,
         'published': blogDetail.publishDateFormatted,
         'body': blogDetail.body,
         'authorName': blogDetail.authorName,
         'authorImage': blogDetail.author.profileImage,
         'featuredImage': blogDetail.featuredImage,
         'metaTitle': blogDetail.seoTitle,
         'metaDescription': blogDetail.metaDescription,
         'year': currentYear,
       },
     );

Of course, rendering HTML also requires me to reckon with an old web-dev skill I haven't applied much in my recent years: CSS styling. Luckily, Tailwind makes CSS styling relatively painless, with its extensive out-of-the-box styles and easy customizability. I'm a terrible designer, so I don't want to make too many boastful claims about how this blog looks–if you have an eye for design and can suggest improvements, please do. However, everything here that looks semi-decent is thanks to Tailwind. They even have a plugin that allows for easy styling of raw HTML articles, which is perfect for my use case.

There is one bit of important UX that I had on my old Flutter app that I can't achieve on a plain-jane HTML page: the ability to fetch more blog posts when scrolling to the bottom of the landing page. However, thanks to HTMX, I can achieve infinite scrolling in an HTML page without even adding any Javascript. All I need to do is add a <div> on my final list item to trigger a request to fetch more items when visible:

{{#isLast}}
<div hx-get="?offset={{offset}}" hx-trigger="revealed" hx-swap="afterend"></div>
{{/isLast}}

As of writing this post, there isn’t enough content on this site yet to trigger a second API call on scrolling to the bottom–I guess this means I have to write more! But take my word for it that this is all the markdown you need to make infinite scroll work with HTMX.

Jumping on the Railway Train

I've also taken this opportunity to update how this site is hosted. Previously, I was building the api in a Docker image deployed to AWS App Runner, while hosting the frontend on AWS Amplify. With HTML served from Dart Frog, I could have removed Amplify and just hosted this site from App Runner. This would work fine, but I also have plans to add an email service to this site, and it could get laborious to connect that service to an existing app runner app. The other option I considered was coordinating services in multiple docker images managed in AWS Fargate, but that would still require a lot of extra infrastructure, like setting up a VPC to host my services and a load balancer to manage traffic. All of that starts to add costs, and it's not always easy to figure out just how expensive things will be on AWS’ pricing calculator. Plus, the more services I set up on AWS, the more work I have to do figuring out IAM permissions, and that's just not something I like spending my free time on.

I recently learned about Railway, a new infrastructure offering that seeks to simplify the deployment and hosting of cloud apps. Among other things, Railway allows you to easily create projects with multiple services on a private network, and to deploy services directly from Github . They offer an affordable ($5/month) hobby subscription, though users are responsible for additional usage fees beyond $5. All services on Railway are deployed in Docker images, which is handy for me since I already had a Dockerfile for the Dart Frog API. Plus, while Railway initially ran on Google Cloud servers, they have recently shifted to hosting in their own data centers, which gives me a nice opportunity to get away from the tech giants that I've become increasingly dissatisfied with. They also have a pretty intuitive UI for managing projects:

Who knows if Railway will work for me long-term. It's possible that when I eventually deploy my email service, I'll find new pain points, and it's also possible that after a few months of usage I'll find out that it's more costly than I had anticipated. But for now, it looks like an appealing solution for a personal side project like this blog. 

My favorite side projects are ones that allow me to create something I want to make, while allowing me to tinker around with languages, frameworks, and concepts that I want to learn more of. While last year, I wanted to learn more about Flutter web apps, I have since learned that a web app isn’t the only way you can write Dart for the web. This redesign has been a great opportunity for me to simplify how my blog works under the hood. With significantly less code and infrastructure setup, I've built a more performant and searchable app with fewer third party dependencies. Flutter is a great solution for complex web apps, but sometimes simpler is better, and Dart Frog offers a great way to serve a website. 

Feel free to check out the code for v2 of this site here. Feel free to hit me up on LinkedIn with questions, comments, and critiques on the project--I'd love to hear from you!