More learning Go: bundling more static and HTML
All checks were successful
Build and copy to prod / build-and-copy (push) Successful in 2m19s

This commit is contained in:
Lewis Dale 2024-05-15 07:55:53 +01:00
parent e5efc3ca28
commit 7934f5f75b

View File

@ -0,0 +1,124 @@
---
title: "Learning Go: Day Fourteen"
date: 2024-05-17T08:00:00.0Z
tags:
- learning
- go
- css
excerpt: "Nesting templates, serving static files, and some unexpected (to me) behaviour with request matching"
---
In my [last post](/post/learning-go-day-13/), I managed to get static files bundled into my binary and deployed to the server, meaning I can easily create some HTML output. Now, I want to make that output actually something worthwhile.
## Nesting templates
This HTML templating language is pretty powerful, and works fairly similarly to the [Nunjucks](mozilla.github.io/nunjucks) templates I'm used to using for this blog. To make some of my HTML more repeatable, I want to be able to split my template files up so I can reuse them.
It turns out this is pretty straightforward, as Go provides a `template` tag for doing exactly that. So I can extract the content of my `head` section into `head.html`, and then just reference it in `index.html`:
{% raw %}
```html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
{{ template "head.html" . }}
<body>
<h1>Oopsie uptime monitoring</h1>
</body>
</html>
<!-- head.html -->
<head>
<title>Oopsie</title>
</head>
```
{% endraw %}
And then just add my template to the list of templates passed to `ParseFS`:
```go
// main.go
t, _ := template.ParseFS(content, "templates/index.html", "templates/head.html")
```
And that works! The output is the same, but now I can reuse the `head` tag easily.
## Template blocks
I can actually go even further. If I then create a template called `base.html`, which contains the general structure of one my web pages, I can define a `block` in there, which will let me just define content in templates that inherit it, like `index.html`.
{% raw %}
```html
<!-- base.html -->
<!DOCTYPE html>
<html lang="en">
{{ template "head.html" . }}
<body>
{{ block "content" . }}{{ end }}
</body>
</html>
<!-- index.html -->
{{ template "base.html" . }}
{{ define "content" }}
<h1>Oopsie uptime monitoring</h1>
{{ end }}
```
{% endraw %}
And of course I then need to add it to my list of parsed templates:
```go
// main.go
t, _ := template.ParseFS(content, "templates/index.html", "templates/base.html", "templates/head.html")
```
Cool. This has had limited impact so far, because I've got no content, but it'll make creating new pages far easier.
## Serving static files
Now I'm going to need some static files. In particular, I want to serve some CSS. For now, I've just added [Andy Bell's excellent reset CSS](https://piccalil.li/blog/a-more-modern-css-reset/) to a newly-created `static` directory. I can use the `embed` package to embed the static files in my binary, just like I did with my template. Then if I want to serve them, it turns out it's ludicrously easy:
```go
// main.go
//go:embed static/*
var staticFiles embed.FS
http.Handle("/static/", http.FileServerFS(staticFiles))
```
The HTTP library has premade handlers for serving static files, including ones that can take an instance of `FS`, much like the one created by `embed`. This was too easy, right?
## Yep, sort of
When I ran the code, I got this error
```bash
panic: pattern "GET /" (registered at /Users/lewis/Development/personal/oopsie/main.go:38) conflicts with pattern "/static/" (registered at /Users/lewis/Development/personal/oopsie/main.go:36):
GET / matches fewer methods than /static/, but has a more general path pattern
goroutine 1 [running]:
net/http.(*ServeMux).register(...)
/Users/lewis/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.2.darwin-arm64/src/net/http/server.go:2733
net/http.HandleFunc({0x1025590f0?, 0x140000ddf28?}, 0x1026ce3a8?)
/Users/lewis/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.2.darwin-arm64/src/net/http/server.go:2727 +0x9c
main.main()
/Users/lewis/Development/personal/oopsie/main.go:38 +0x218
exit status 2
```
It looks like my `GET /` pattern is also matching `/static/`, for some reason? My expectation would be that `/` matches _only_ the root path, and nothing else[^1]. But apparently not, it'll just match anything. However, the fix is pretty simple, I just need to append `{$}` to the root patterns:
```go
http.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
```
And that limits it to _only_ the root path, and I'm able to successfully run my code. So far Go is really impressing me with how easy it is to build this stuff. I've been so entrenched in the NodeJS/Typescript world over the last few months I forgot how nice it is to have a decent standard library[^2].
The bundled binaries are fairly large - as of writing it's around 14mb, but that's kind of to be expected given all the stuff it's doing. I'm not going to bother reducing it, it's not like I'm serving the binary to the client - in fact right now the client only receives around 1.8kb of data.
[^1]: This is why you should always read the docs
[^2]: I'm being unfair, the NodeJS standard library is so much better these days, I'm just not using a lot of it