๐๏ธ Building a Personal Projects Portfolio with HTMX, Go, and Firebase
Blog post about the blog that I blog post too
Over the years I have gotten into a multitude of different things in my spare time. Lately it has been programming and web development, so I decided to build a site to showcase and record what I've gotten into. And of course, a post about building the site is only par for the course so here I'll go into how I built it, decisions along the way and problems I faced.
๐ Choosing the Stackโ
๐ HTMX and Goโ
Since I am relatively new to web development, I'm not married to any frameworks or languages. I chose GO as the backend language since its fast and its gaining popularity sparked my interest. Then I wanted the frontend to be responsive and seamless like a single page application is. I had developed some sites before with plain vanilla Javascript, HTML and CSS and wanted to avoid full page refreshes. So I chose HTMX, it was easy to pick up and start using and with it gaining popularity, I thought it would be a good choice. Overall I didn't want to spend too much time finding the best stack, I wanted to get something up and running quickly, learn as I go and just start building.
โ๏ธ Firebase Hosting and Google Cloud Runโ
Prior to making this site I had been using Firebase and its several different services like functions, hosting, authentication and firestore. But its Functions don't natively support GO and I wanted to use GO, so I decided to use Google Cloud Run for the backend server, Firebase hosting to serve the site, Firestore to store data and Firebase auth for user authentication.
๐ป Development Environmentโ
๐ฅ๏ธ Using a Node Server Locallyโ
Since I was using a Cloud Run container, I needed a rewrite in the firebase.json
file to redirect all requests to the Cloud Run container - Firebase Doc. So getting Firebase's emulator to work locally would have required a bit of work or writing the site so it would work locally but then having to do things like replace operations on files to get it to work publicly. Instead, I just wrote up a node.js server to serve the site locally -
// server.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const path = require('path');
const app = express();
// Serve static files from the public directory
app.use(express.static(path.join(__dirname, 'public')));
// Redirect all requests to the Cloud Run container
app.use('/', createProxyMiddleware({ target: 'http://localhost:8080' }));
app.use('/posts', createProxyMiddleware({ target: 'http://localhost:8080/posts' }));
app.use('/category', createProxyMiddleware({ target: 'http://localhost:8080/category' }));
app.use('/contact', createProxyMiddleware({ target: 'http://localhost:8080/contact' }));
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Then you can run your Cloud Run docker container locally or I would just run the GO server -
go run main.go
๐ Fixing CORS Header Issuesโ
One of the first issues I ran into was Cross-Origin Resource Sharing (CORS) issues. Since I'm using a Cloud Run container, I assumed the website url routes in my HTML code needed to point to the Cloud Run's public domain name. So when the site was hosted by Firebase, the requests were being blocked by the browser since I had them going to the Cloud Run Domain. You can actually write relative url paths to the domain Firebase serves i.e. /posts, /category, /contact and the requests will be routed to the Cloud Run container with no CORS issues.
๐ Advanced Featuresโ
๐ Managing Browser History and HTMXโ
Since I'm using HTMX to build a single page application, data is sent and received from the server without a full page refresh or load. So technically the user isn't navigating to different pages on the site which makes allowing the user to use the browser back and forward buttons a bit tricky. Lets say a user clicks a category link, HTMX asks the backend server for all the posts in that category and updates the section on the page to show those posts. We can use the hx-push-url attribute to update the browser's history but then if the user hits refresh or shares that link, they will get a page with just the posts and the site nav bar, header or anything else won't be there.
So I solved this problem by adding checks to all my GO app routes for the "HX-Request" header and in the template rendering process where the server would render the posts to return, if the request header is not present, then I add onto the render the nav bar and header of the site. Below is an example of how I did this in the about page route and HTML template rendering process.
func about(w http.ResponseWriter, r *http.Request) {
data := TemplateData{
IsHTMX: r.Header.Get("HX-Request") == "true",
}
tmpl := template.Must(template.ParseFiles("templates/about.html", "templates/indexBefore.html", "templates/indexAfter.html"))
tmpl.Execute(w, data)
}
{{if not .IsHTMX}}
{{template "indexBefore.html"}}
{{end}}
<div class="grid gap-4" id="688gsnjtere">
<h2 class="text-2xl font-bold">About This Blog</h2>
<p class="text-gray-500 dark:text-gray-400">
Over the years, I've worked on a variety of personal projects and built a multitude of things. I wanted a place
to document it all, so I decided to code my own blog site. This blog is a culmination of my experiences and
learnings. ๐ง
</p>
<p class="text-gray-500 dark:text-gray-400">
Here, you'll find articles on a wide range of topics including 3D printing, manufacturing, programming,
networking, automation, hacking, electronics, and more. Each post is a reflection of my journey in these fields,
and I hope they can provide some insight or inspiration to you. ๐ก
</p>
<p class="text-gray-500 dark:text-gray-400">
I believe in the power of sharing knowledge, and this blog is my way of contributing to the community. Whether
you're a fellow enthusiast or a curious learner, I hope you find something of value here. ๐
</p>
</div>
{{if not .IsHTMX}}
{{template "indexAfter.html"}}
{{end}}
So if the user hits this url directly then I just prepend and append the index page code. I'm still looking for a better way to solve this problem, my way seems a bit hacky, forces me to maintain the index page code in 2 places and also in more complex applications I'm not sure this method would work.
๐ Rendering and Storing Markdownโ
I didn't want to write HTML pages for each post so I knew I would be using some sort of markdown to render the pages. I found some styling and markdown JS imports that I liked and on the backend use a GO module that renders the markdown into HTML to be sent to the browser. Each post is simply stored in the Firestore database with metadata like title, description, date, etc. but then the content of the post is base64 encoded markdown text that gets decoded and sent to the browser.
github.com/gomarkdown/markdown
github-markdown.css
๐ Handling admin auth with session cookiesโ
So in order for me to have a decent experience with adding and editing posts, I needed a way to authenticate to the site in order to do so. So I use Firebase Auth to sign in and then I use a session cookie to authenticate further requests on the site. This worked locally but when I deployed the site publicly, my backend GO server kept saying the cookie was not found when I checked requests for it. This blocked me for a couple days trying to figure out what was wrong. I read through several docs from Firebase on how to authenticate with session cookies and I was doing everything right. I eventually just happen to come across this firebase doc which states that unless you specifically name your browser cookie __session, Firebase strips your cookie from the request to the backend. Hadn't I come across this doc I would have never figured out what was wrong.
๐๏ธ Building Partsโ
๐ Search, Categories, and Recent Postsโ
To enhance navigation and discoverability within the site I added a search bar as well as a way to list categories and recent posts. These features are nothing fancy, the search uses HTMX's active search to find posts while the user is typing and it is simply matching text. I haven't added any tagging of posts or way to suggest posts the user might be searching for but that there is no exact text match. Then the categories and recent posts just query the Firestore DB for the data. Each post has metadata fields so that's pretty easy to do.
๐ Conclusionโ
I really enjoyed building this site, and hope to keep adding to not only its posts but features as well. If you have any questions or want to know more feel free to reach out to me or join the newsletter to get updates on new posts.