Context and My Blog
I like my blog. I don't write nearly as much as I should for it but at the endof the day I like the three small posts that I have put into it. However, I never really liked how I had it set up. I have no real qualms with the SSG I chose (11ty) or the web server to host the HTML (NGiNX) but I never really felt like it was mine.
I have to be very clear: 11ty is very nice, very good, and has a fantasic community and plugins. This post, and this work, was not in response to anything specific to 11ty. Go use it if you liked it.
Over the last few months I have been noodling with Bun and Hono as a stack to build web applications with. While I know JavaScript isn't all that popular insome of the circles I travel in - its a language that, oddly enough, gives me joy. And a runtime like Deno or Bun giving us TypeScript out of the box is a huge win.
Because of this, I was looking at building a simple blog application with a micro-framework, similar to Express.js. I thought about using Express.js itself but the fact that its old [sic] and slow has made look for other options. One particular thing I needed the framework to do - is have a view layer of some sort. I ended up picking Hono as it ticked all the boxes for me and I can say, after building very simple applications with it, it brings me joy. One of the big selling points of Hono was the jsxRenderer
middleware that allows me to write plain old JSX/TSX and create frontend templates and components.
Finally, I decided that my entire application will just run in memory. While I have static Markdown files that make up my posts, on each build of the application it will parse the files, organize the post data and render them to the client in the response. I have thought about maybe parsing the Markdown into a relational database like SQLite and using the built-in Bun functionality to handle that database.
But that made no sense once I actually thought it. What advantage does having an in-memory SQLite instance of my blog give me over just rendering a bunch HTML and holding that in memory? These are good conversations I have had with myself that I wish I had with someone else to get to the conclusion faster.
Note: I am going to talk a lot about JavaScript and TypeScript, and I want to make it very clear that I am talking about running JavaScript on the server. I feel like when people talk about JavaScript, there is an assumption we are talking about building frontend SPAs and not high-IO server applications (for good reasons, honestly). However, in my career and experience, the bulk of JavaScript I have written runs on Node.js. Anyways, after such a long introduction, here is my blog post about what I learned building a very basic blog with Hono and Bun.
Organizing Hono applications
I make no effort to hide the fact that I come from the MVC Web Framework world. I have spent a lot of time in my career thinking of web applications in the terms of Models, Views, and Controllers. This has made a lot of sense to me over the years, and I posit that it's still a great framework for organizing your code. However when it comes to JavaScript, it feels verbose to create classes full of methods to handle requests and responses. I think this verbosity comes from how JavaScript code is actually organized for Node.js (Deno, Bun, etc).
JavaScript can best be described as Modules. These modules are single files that define some form of behaviour, state, or shape to data. Because of this, I find its not particularly useful to oganize code into Classes unless there is a specific state that many methods/functions need to keep track of. Because of this revelation in how JavaScript code is run and organized, I feel nothing but regret for creating many, many Controller classes from scratch for Express.js applications.
So with my understanding that a Class in JavaScript should be a collection of methods with shared state - why should we make a Controller class with discrete methods for handling requests and responses when each individual method has its own state? Previously, I would assume that we would want to share some sort of resource - like a Repository class for interacting with a database. But the more I read and tinkered with the Hono framework (and this is not a Hono specific thought) the more I realized we should be centering our dependencies within the Context
object of the Hono application.
I like to call this pattern Handler, Service, Presentation. This pattern is nearly identical to MVC and you can immediately see the analogues to the original acronym. I don't think there is a clear advantage of using these words in particular, other than it can hopefully erode some of the web-brainrot on how we organize our we applications.
An Example of a Service
Let's think of a Blog. This blog. What services do we have in the code for this blog? Right now, it is solely the PostService
. This is a class that is given a list of Post
types, and creates an internal Map of that with the slug
as a key. From there we can do things like, get all the posts, get the latest post, get a post by slugs, get un-published posts, etc.
Within the PostService
module is two helper functions. One of these is an async
function for reading and parsing the Post markdown files, and the other will utilize that function to instantiate a PostService
class. This is a great way to do some async
shit for construction an object (like reading a file from disk).
Another good way to think of a service, is a Repository class. Think of a class that should handle querying data to and from a database. If you need another example, image an HTTP Client for a specific HTTP API. Think of something that provides data to something else. I guess that's how I'd describe it.
What Handlers Are
Handlers should be thought of as callback functions for particular requests and responses. The handle the request and response. In the world of Hono, we get to decide what Middleware is type'd into the Application and can be accessed within a handler. This allows us to bootstrap our middleware elsewhere and be assured it will be there when it runs.
Here is the handler for showing a single Post on the blog:
import { Hono, Context } from 'hono';
import { PostPage } from '@blog/templates/Pages/PostPage';
import { FourOhFour } from '@blog/templates/Pages/FourOhFour';
import { SiteMeta } from '@blog/models/SiteMeta';
import { PostService } from '@blog/services/post-file';
type Posts = {
postService: PostService
}
const posts = new Hono<{ Variables: Posts }>();
export async function handleSinglePost(c: Context) {
const postSlug: string = c.req.param("slug");
const postService: PostService = c.get('postService');
try {
const post = postService.getPost(postSlug);
const meta: SiteMeta = {
description: post.meta.description,
tags: post.meta.tags,
author: "Dave Smith-Hayes"
};
return c.render(<PostPage post={post} />, { meta });
} catch (e) {
const description: string = "Page does not exist.";
console.error(description);
console.error(e);
c.status(404);
const meta: SiteMeta = { description };
return c.render(<FourOhFour />, { meta });
}
}
posts.get('/:slug', handleSinglePost);
export default posts;
src/handlers/posts.tsx
Now I haven't done this yet - but if I need to test the handleSinglePost
function, I can properly mock the Context
object with the right PostService
class.
How Presentation Works
Like I mentioned earlier, one of the selling points of using Hono for the framework was its suppose of rendering JSX with jsxRenderer
middleware. Its trivial to set up, but you have to remember to re-save your files as tsx
and jsx
if you want to use it. I have not spent a lot of time writing JSX in my life but once I got some of the basics it became super easy to understand. I can understand why people like React, honestly.
One of the first things to understand about JSX is that its solely a markup syntax. While React utilizes it, this it is not React. You could easily compre JSX to something like Twig, Pug, or another Templating language. The big difference is that its JavaScript centric, and you can really compose the components you build with functions.
You can set up a super basic Page tempalte that every page will render within.
import { Style } from 'hono/css';
import { SiteMeta } from '@blog/models/SiteMeta';
import { MetaTags } from '@blog/templates/components/MetaTags';
export function Page({ children, meta }: { children: any, meta: SiteMeta }) {
return (
<html lang="en">
<head>
<title>davesmithhayes.com</title>
<MetaTags meta={meta} />
<Style />
<link rel="stylesheet" href="/static/main.css" />
<link rel="stylesheet" href="/static/intellij-light.min.css" />
</head>
<body>
<header>
<div>
<a href="/">davesmithhayes.com</a>
</div>
</header>
<main>
{children}
</main>
<footer>
<div class="copyright">© 2024 Dave Smith-Hayes</div>
</footer>
</body>
</html>
);
}
src/templates/Page
And then you can set up the jsxRenderer
middleware within the main Hono App instatiation.
import { Page } from "@blog/templates/Page";
app.get(
'*',
jsxRenderer(
({ children, meta }) => <Page meta={meta}>{children}</Page>,
{ docType: true }
)
);
Development
If the environment variable DEPLOY_MODE
is set and the value is DEVELOPMENT
then we can easily grab all the Posts, including Drafts, so I can see how my content will be rendered out to the page.
That's really it, there isn't much else to talk about in regards to how to develop this blog. Things will change when they are needed.
Deployment
A while back I had set up Dokku on one of my VPS's. I was really impressed with how quick I could deploy some basic code and have it all set up behind NGiNX and handle the Let's Encrypt SSL for me. If you are reading this blog right now, I have set this up for the Bun application.
Setting Up The Container
To get an application up and runnning on Dokku, the easiest way is to set up a container with the Dockerfile
. This was pretty easy to set up but did trip me up in two ways when I was writing the Dockerfile
- the first being that as I followed the Official Bun Documentation on Docker and completely forgot my own advice from above - my index.ts
for the entrypoint of the Blog application is now index.tsx
.
The other thing about the Docker set up that tripped me up was making sure that I had the tsconfig.json
file included as well. This configuration is what allows us to actually write and render JSX within our application.
{
"compilerOptions": {
"strict": true,
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"paths": {
"@blog/*": [ "./src/*" ]
}
}
}
The tsconfig.json
that allows us to render JSX.
Another important point to make - that is not important to the deploymen to Dokku at all is that we can set up custom import paths to keep TypeScript projects nice and neat. As you can see in the JSON above, I have opted for using @blog/
, and you may have noticed in the code examples above that's how I'm organizing all my imports for the code I write.
Okay - sorry for the tangent, let me get back to the deployment of Dokku. If you read the documentation for Dokku, you will notice that all you need to do on your code side (besides creating the Dockerfile
for running your application) is a Git remote entry for your service. I am not going to go over setting up Dokku directly - the Documentation does a really good job on how to set this up.
The first thing we need to do - after setting up Dokku - is to create the Blog application.
dokku apps:create davesmithhayes.com
After that, I would need to set the new remote in my Git repository:
git remote add dokku dokku@davesmithhayes.com:davesmithhayes.com
And simply do a:
git push -u dokku main
Or so I thought. Once the application was deployed and declared running, I went to https://davesmithhayes.com and noticed right away I had no SSL. Looking at my Bash history I noticed I forgot to set up Let's Encrypt to get me an SSL for my website. So I did that through Dokku:
dokku letsencrypt:enable davesmithhayes.com
Which failed. With this message:
Certificate retrieval failed!
Of course there was more information in the error, but I am unsure how safe it is to show you the details of the failure. I think, however, this is failing because I have existing certbot
set up for my old 11ty blog on this same host. So I revoked the certificae with certbot
:
sudo certbot revoke --cert-name davesmithhayes.com
At this time Let's Encrypt had rate-limited my many, many failed attempts to get a new certificate for my own domain. Time to wait it out. An hour to be exact.
So, instead of actually waiting an hour I went to bed and waited for a few days to get back to work. I knew I had previously deployed a really basic Node.js application with Dokku, hence why I decided to keep using it as a platform to deploy applications on.
I realized that application was running, by default, on port 3000
and I think Dokku is expecting all the applications to run on 5000
by default. I can't seem to find any documentation on that but the moment I removed the following line from the Dockerfile
:
EXPOSE 3000
And set the application environment required an update to the APP_PORT
envvar that I run the application on.
dokku config:set davesmithhayes.com APP_PORT=5000
et voila! Now I could set up certificates with Let's Encrypt and we are good to go.
Conclusions
Its been a long time since I started and finished a software project, on my time, and I am happy with. Using Hono and Bun, two technologies I am adjacently familiar with, was also a huge plus to me. I enjoyed it so much that I think this PHP project I have been hobbling together for the last couple of months will get a complete rewrite in TypeScript using Hono and Bun.
I really hope that you read this far and took something away about how to do projects on your own, how viable these technologies are, and how to have fun doing basic things. I don't think this blog would have required as much work as I had put into it, but I am happy that I did. Please feel free to contact me about my Bun and Hono experience.
That's it. I'm a convert. Praise Bun.