This is the seventh in a series of articles
about writing a small reading list app in Go for personal use.
So far we’ve built a sort-of usable app with Gin. We can see the list of
books we have in the system, and we can add new books. But there are some
serious usability problems:
there’s no link to the “New Book” page, or to the “Book List” page – you
have to know the URLs and type them in directly
both the input form and the list suffer from poor layout, almost to the
point of being unreadable
In this article we will fix those issues and set up a foundation so that
future work will have established patterns for styling.
When we’re done you’ll have:
external CSS and a way to reference it from the pages in the app
a navbar to be able to easily click around the app
some options for managing that external CSS with the app’s deployment
Fair warning: I’m not a CSS expert, so don’t assume that anything I’m doing
with the actual CSS is “the right way”. I’m more interested in
demonstrating the mechanisms and patterns that we can use for integrating
these pieces. When I need stuff to actually look nice and be maintainable
and scalable I talk to real designers.
First, let’s add a navbar. In templates/base/header.html, just below the
body, add a header element containing a list of the links to the pages
in the app.
Rebuild the app, run it, and load the main page in your browser to see:
Since we added this to the header template, we will get this in all the
pages of the app. Click on the New Book link and you will see the form, and
the navbar will be present on that page too.
Now we have our navbar and we’ve fixed the usability problem noted above,
we’ve also arguably made the app look a little bit worse because of the
clutter of the links at the top. Let’s dive into the CSS so we can make
this not look like trash.
To add styles we have two options: put them in a style tag in the header,
or put them in a separate file that is linked from the header. We’ll do the
latter, because duplicating the styles over and over in every page that we
send is a waste of bandwidth. This duplication would make pages load more
slowly for our users and depending on our hosting plan it could cost us
It’s worth reiterating the note above: this CSS is not great. If you try
to use this across a much larger site you will find that it is very
fragile. It’s also not responsive and probably a bunch of other problems
I don’t even know about. It’s intentionally simple so that we can see
what this looks like without adding more than 100 lines of code.
One More Thing!
While fixing up the form I realized there’s one more thing to improve: the
error messages that get shown when the form doesn’t validate.
Add a wrapper div, and a class to the inner div in templates/base/errors.html:
and if you forget to copy the styles then the app will run but because
browsers won’t be able to load the CSS, it will go back to being barely
And this is fine – you shouldn’t be deploying manually anyway, and the
automated deployment process should always copy everything that’s needed.
(We’ll talk about automated deployments in a future article.)
But let’s say that we want to be able to deploy the app by copying just one
file. Go has support for embedding files into the binary, using the
standard embed package (since 1.16).
First add three imports (code not shown): embed, io/fs, and
path/filepath. Then add this code just after the imports:
Note that the comments are specially formatted directives to the toolchain.
This will embed, recursively, the files under the directories templates
and static into the binary in those embed.FS variables.
The staticFS type there is to work around an annoyance. When we pass this
to gin.Engine.StaticFS (see below), it’s going to get URL requests for
the CSS file as /static/css/styles.css. Gin will strip off the /static
on the front and try to get css/styles.css out of the embed.FS – but
that file isn’t there, it’s under the static/ directory. So staticFS is
just a thin wrapper around the embed.FS struct that prepends the
static/ prefix back on to filenames so that the URLs map properly. (The
fact that the explanation is about four times longer than the code is a
testament to the simplicity of Go’s interfaces!)
With all of that set up, modify setupRouter to look like this:
Gin doesn’t have direct support for loading templates from embed.FS, but
we can make a simple change to setupRouter so that this works. The
template package provides ParseFS which can use our tmplEmbed, and
then we can pass the resulting teplate to SetHTMLTemplate.
After rebuilding this, we can run the app from anywhere, as long as the
database file is in the current working directory. (We’ll fix that in a
It’s worth pointing out that it’s somewhat more convenient to develop
without embedding this way, because without embedding, as you make
changes to the CSS or templates you can just reload the browser and see the
results. But when everything is built-in, you have to rebuild every time
you make a CSS tweak in order to be able to see the results in the browser.
We could modify this behavior base on an environment variable, but I’m
leaving that as an exercise for the reader…
Next Tuesday’s post is about applying processes to improve the accessibilty
of web apps. And on Friday we’ll work on adding a backend command to import
data from a Goodreads CSV export file into Aklatan’s database.
Looking further out in terms of app features and future topics, I’ll cover
pagination, adding notes to books (one-to-many relationships), and adding
tags (many-to-many relationships); plus fuzzing, migrations, and more.