Delving into the GOTH Stack: My Experience Building a Blog with Go, Templ, and HTMX.
Introduction
What is the GOTH Stack?
The GOTH Stack is a simple stack to create the most modern web applications.
I've used it to create GOTHStatic - My own blog from scratch.
I became aware of htmx.org beacuse of the great X memes.
The Primeagen created the beautiful name GOTH Stack
for using Go
+ templ
+ HTMX
.
- Go is used as backend language, creating a great performance.
- templ is used as templating engine with a awesome developer experience.
- HTMX enhances the static HTML files with various interactions.
Motivation
One of the primary motivations behind my decision to venture into the GOTH Stack, was a strong desire to learn something new and apply it in a real-world use case. While static site generators like Jekyll and Hugo are excellent choices for building blogs and websites, my goal went beyond just creating a blog — it was about expanding my skill set and exploring the capabilities of templ and HTMX.
I didn't necessarily need the extensive features and complexity of other tools at this stage. Instead, I wanted to start with something simpler that allowed me to grasp the fundamentals of templating and gradually build upon that foundation.
Essential Features for a Responsive and User-Friendly Blog
To start the project the right way I've wanted to point out some key features i want to accomplish in this project:
- Create, edit, and delete blog posts.
- Support Markdown for a superior writing style.
- Display blog posts with titles, content, and publication dates.
- Allow users to navigate between posts.
- Optimize the loading speed of the blog for a seamless user experience.
- Implement responsive design for various device types and screen sizes.
- Open Source on Github 😉
Markdown First!
I love the simplicity of Markdown, which is why I have been writing all my notes in Markdown for years. For this reason, I also wanted to write blog posts in Markdown.
1# Headline
2
3A paragraph describing something
4
5## Second Headline
6...
But Markdown doesn't look great in the Browser.
In the past, a time where i thought JavaScript is the answer for every frontend question, i would have used a JavaScript library to beautify the Markdown for the user. But why should I unnecessarily bloat the application with Javascript for the user interface? I need to convert Markdown to HTML in the backend to have a great performing website.
After a brief review, I decided to use goldmark
as Markdown parser, which is extended by custom rules in parsing/transformer.go
.
1// Add Tailwindcss classes to rendered nodes
2func (t *TailwindTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
3 ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
4 switch nt := n.(type) {
5 //...
6 case *ast.Paragraph:
7 var buf bytes.Buffer
8 buf.WriteString("text-lg leading-relaxed mb-4")
9 nt.SetAttributeString("class", buf.Bytes())
10 //...
11 return ast.WalkContinue, nil
12})
Of course, you could have made proper CSS definitions for the various elements, but that would have been boring.
Static File Generation with templ
After ensuring my Markdown get parsed to the correct HTML format, i needed to build templates to enhance the content with a navbar
, footer
and some smaller components.
To accomplish this task i've chosen templ.
.templ
files will look like a mixture of HTML and Go.
1package pages
2
3import "blog/web/components"
4import "blog/web/shared"
5import "blog/model"
6
7templ BlogPage(post model.Post, html string) {
8 @shared.Root() {
9 <div class="container mx-auto px-4">
10 <div class="md:flex">
11 <a href="/">
12 <button class="bg-steel-gray-700 hover:bg-steel-gray-800 rounded-lg px-6 py-2 flex">
13 <img src="/assets/back.svg" alt="back"/>
14 Return to Overview
15 </button>
16 </a>
17 <div class="md:ml-4 mt-2 md:mt-0 flex flex-wrap content-center">
18 for _, tag := range post.Tags {
19 @components.Tag(tag)
20 }
21 </div>
22 </div>
23 @components.Post(html)
24 </div>
25 }
26}
You can spot some go code in between of our template definition.
In line 18, a for loop is used to add each tag from post.Tags
. That's the great benefit of using templ.
After running templ generate
our post.templ
file got converted to Go code => post_templ.go
.
pages.BlogPage(post, "<h1>My raw HTML</h1>").Render(ctx, writer)
writes the file with the given context to any writer: io.Writer
.
In my case i've decided to write the content directly to a os.File
, served directly in the /static
folder.
After seeing the first results, i've seen the need for code highlighting. github.com/yuin/goldmark-highlighting/v2
does a beautiful job by using chroma
to highlight codeblocks.
Upload of static files
Basic text and code highlighting was supported. I hadn't even thought about images. However, in order to create attractive posts, I absolutely need images.
An API could quickly remedy this. Images are now stored in the static/assets
folder.
1 api.POST("/asset", func(c *gin.Context) {
2 form, _ := c.MultipartForm()
3 files := form.File["files"]
4 paths := []string{}
5 for _, file := range files {
6 path := fmt.Sprintf("assets/%s", file.Filename)
7 c.SaveUploadedFile(file, "static/"+path)
8 paths = append(paths, path)
9 }
10 c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!\n%v", len(files), paths))
11 })
After uploading a resource to the API i can use the default 
Markdwon definition.
I forgot to use HTMX
I've created a beautiful static site generator, turning http POST Requests with Markdown to HTML, but completly forgot that i want some API interactions with HTMX.
At this point i've had to find out which content i need to load dynamically. The whole content is static generated. I don't need any dynamic loading of content yet.
To provide a basic usage of HTMX i've decided to load the blog posts from a database. The HTML Pages are still static generated, but the list on the Landingpage is aggregated by an API call.
Adding a Database
On my workplace i mostly work with non-relational databases like MongoDB. To get back to the traditional SQL feeling i've chosen the most basic solution: SQLite.
SQLite is a preferable choice over managed database instances when you need a lightweight, embedded database solution directly integrated into your application, trading off advanced features for simplicity and reduced resource overhead.
The model.Post
represents the currently saved data to a blog post.
1package model
2
3type Post struct {
4 ID int
5 Title string
6 Subtitle string
7 Description string
8 Image string
9 Tags []string
10 Slug string
11}
Current usage of HTMX
Althrough the database is created now, i only use it to load the posts on the index.html
. That's something i've also could render static if a blogpost is updated.
In the future i want to add some features, that will make the HTMX integration worth it:
- Search posts by a searchterm.
- Filter posts by tags.
Lessons Learned
- Choosing the right tool for the job: In the future i need to define a project and choose the TechStack afterwards. I could have written this Blog in the current state without the usage of HTMX, but used it just because GOTH sounds great.
- Simplicity and Speed: HTMX, when paired with Go templates, can simplify frontend development by allowing you to update HTML elements dynamically without writing extensive JavaScript code.
- Timeboxing: Implementing timeboxing techniques in my side project planning allowed me to allocate focused, dedicated periods for development, fostering creativity, preventing procrastination, and ensuring steady progress.
- Writing Posts is hard: I've realized that writing a good blog post is quite a task. It's not just about dumping ideas onto a page, but organizing them in a way that's clear and engaging. It's a learning curve, but definitely a rewarding one.
The Future of GOTHStatic
The GOTHStatic project will still be the base of my blog for a while. I've had a lot of fun writing this project, and even more fun writing a blog post about the process.
As I continue to tinker and improve it, I'm excited to see where this journey takes me next. I appreciate you taking the time to follow my journey.