Before anyone can see your website and it's contents, it has to be running somewhere. Thankfully Go has a fully featured HTTP server in it's standard net/http package, which we'll use throughout this project. To get to know the basics I read an excellent introductory book Let’s Go by Alex Edwards that explained from the ground up how the net/http package works. Here I'll try to explain how the site works in general, omitting a bunch of stuff, but the whole source code is available at a public repo if you're interested to peek at it.
Writing a program that serves HTTP from "/" route is as simple as this:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "<h1>Hello from the server!</h1>")
})
http.ListenAndServe(":8080", nil)
}
This will serve a simple HTML string from the port 8080. http.HandleFunc is a function in the standard library that associates a URL path with a handler function. A handler is called whenever an incoming request matches that path. It receives two arguments: an http.ResponseWriter, which represents the HTTP response being sent back to the client, and an *http.Request, which contains information about the incoming request. In this example, the handler writes a simple HTML string to the http.ResponseWriter using fmt.Fprintln. The http.ResponseWriter implements Go's io.Writer interface.
The website application has a few more routes to handle:
func (app *application) addRoutes() *http.ServeMux {
// new mux, http request multiplexer (router)
mux := http.NewServeMux()
// make images available with a FileServer route
mux.Handle("GET /images/", http.StripPrefix("/images/",
http.FileServer(http.Dir(*app.imageDir))))
// make static assets available via http.FileServer
mux.Handle("GET /static/", http.StripPrefix("/static/",
http.FileServer(http.Dir("static"))))
//{$} is a catch-all preventer in go servemux,
//ie. all unknown subtrees don't match to /
mux.HandleFunc("GET /{$}", app.home)
mux.HandleFunc("GET /about", app.about)
mux.HandleFunc("GET /posts/{$}", app.posts)
mux.HandleFunc("GET /posts/{slug}", app.viewPost)
mux.HandleFunc("GET /kitchen/{$}", app.kitchen)
mux.HandleFunc("GET /kitchen/{slug}", app.viewPost)
mux.HandleFunc("GET /atom.xml", app.feed)
return mux
}
A servemux is a Go standard library request router, basically it takes incoming requests and decides which part of the program handles the request based on the URL (or HTTP method). The homepage and 'about' -page are hand written HTML and are not generated on the fly. 'Posts' and 'Kitchen' pages, and the individual posts and recipes on the other hand are dependent on the Markdown files where their content is generated from.
The posts are generated from Markdown to HTML with a wonderful 3rd party library goldmark, which does all of the heavy lifting turning the source files to HTML. To dynamically embed the post data into the website, Go templates are used. The templates allow HTML files to contain placeholders that are replaced with data from the website application when the page is rendered. On the Go code side of things a post looks like this:
// Metadata of a post
type FrontMatter struct {
Title string `yaml:"Title"`
Description string `yaml:"Description"`
Slug string `yaml:"Slug"`
Draft bool `yaml:"Draft"`
Date FrontMatterCustomDate `yaml:"Date"`
Tags []string `yaml:"Tags"`
}
// single item of content for website,
// md parsed to html with md frontmatter in FrontMatter struct
type ContentItem struct {
Meta FrontMatter // metadata
Content template.HTML // parsed main markdown content
}
// Final datatype sent to pages,
// contains a list of items and or one specific item to show
type TemplateData struct {
Item *ContentItem
Prev *ContentItem
Next *ContentItem
Items []*ContentItem
}
And on the HTML side the data can then be accessed like this:
<div>
<!-- Posts -->
{{if .Items}}
<ul class="space-y-2">
{{range .Items}} {{with .Meta}}
<li class="gap-4 md:text-xl">
<a href="{{.Slug}}"> {{.Title}} </a>
<div class="text-sm">{{.Date.Format "02 Jan 2006"}}</div>
<p class="text-sm">{{.Description}}</p>
</li>
{{end}} {{end}}
</ul>
{{else}}
<p>There’s nothing to see here.</p>
{{end}}
</div>
{{end}}
If you have used or know about Jinja templates, the Go templates are basically the same.
The HTML template gets it's data in the form of TemplateData and then it can access the struct data with the same field names as defined in the Go code. In the above example the 'Posts' page gets a pile of pages in the TemplateData field "Items", loops through it with 'range' and construct an HTML list with the post metadata.
On application startup, the program parses all HTML templates present into an in-memory template cache. This means that they need to be parsed only once and not on every page load. The final templates are made of a base template, partials (which are things like the footer, header, etc.) and individual page-specific content. The same startup parsing is done for the Markdown files, their content is parsed with goldmark, and then stored into a cache as plain HTML. When a page is requested, the router calls a handler, which then fetches the correct template from the cache and the data for the requested page, and merges them together.
I tested both un-cached and cached template and content parsing and the difference wasn't really that noticeable. I also implemented parallel and sequential Markdown parsing, where interestingly the parallel implementation won when the amount of Markdown files exceeded only three. Guess the Go goroutine overhead isn't heavy at all.
For example, this post has an URL of 'https://www.sorvali.net/posts/website-1". When a request is made to the URL, the server finds the post using the 'website-1' slug from the content cache. The handler then fetches the template for a 'Post' page and sends both of the items to a 'rendering' function which then finally merges the content HTML into the 'Post' page template. The main body of the post is just the parsed HTML from the Markdown, presented as 'embedded' into the template HTML file.
The previously mentioned static site generator Hugo is also built with Go, and it too heavily utilizes the html/template package. This site is basically a worse server implementation of a subset of Hugos features if you think about it.
On the frontend side of things, it's pretty basic. HTML and styling is done with Hugo, which felt easier to use than vanilla CSS. Sure raw CSS would've worked fine, but I admit I don't find fiddling with frontend stuff that interesting, and the Tailwind setup seemed to work fine enough without much previous knowledge, although I did use AI heavily to help with the Tailwind stuff, I admit.
All in all, this was quite a fun project, Go really is a fantastic language with it's nice standard library. I wanted to use as little 3rd party code as possible, but Markdown parsing into HTML is like really hard. The goldmark library creator also stated this in his blog.
I think I will write something else with Go in the future, I already have an interesting project idea worth trying where Go might shine. First I'll take a look at a discipline that combines math, art and computer science, the journey's already started and it's a very interesting one ;) There's also still some small fixes to be made to the site, mostly to the looks.