Time to get rid of that 404 on the front page!
We are going to build a good old, server-side-rendered front page. This means that the app sends the HTML to the user's browser, without any Javascript frameworks like React or Vue involved.
Why do it this way? Because this is often the best solution! A lot of developers automatically reach for something like a Go backend with an API, together with a Javascript (SPA). But unless you're building a heavily real-time app (like a chat app), building an SPA is often way too complex. It will make things harder and development slower for a lot of web apps, so there is definitely a trend towards classic web views.
Plus, you get to write your frontend in Go! ๐ And you can always add some Javascript, if you need interactivity that should not refresh the page.
So let's get started. Follow along yourself, or get the result using:
$ git fetch && git checkout --track golangdk/front
We are going to build the frontend using a component library called gomponents. gomponents, which is short for Go components, enables you to write HTML directly in Go. You can build custom components with it that you can reuse across your applications. The first thing you will need to do is go get it, together with an icon library:
$ go get github.com/maragudk/gomponents@v0.22.0 github.com/maragudk/gomponents-heroicons@v0.5.0
With that out of the way, let's create a basic page template with a layout, a navbar, and a few helper components.
We'll be using TailwindCSS for our styling. TailwindCSS is a CSS framework that works perfectly together with component-based views like ours. Even though this isn't a TailwindCSS course, you can pick up a few tricks along the way that make your views look good in no time. ๐
Put this into a file at views/page.go
:
views/page.gopackage views import ( g "github.com/maragudk/gomponents" "github.com/maragudk/gomponents-heroicons/outline" c "github.com/maragudk/gomponents/components" . "github.com/maragudk/gomponents/html" ) // Page with a title, head, and a basic body layout. func Page(title, path string, body ...g.Node) g.Node { return c.HTML5(c.HTML5Props{ Title: title, Language: "en", Head: []g.Node{ Script(Src("https://cdn.tailwindcss.com?plugins=forms,typography")), }, Body: []g.Node{ Navbar(path), Container(true, Prose(g.Group(body)), ), }, }) } func Navbar(path string) g.Node { return Nav(Class("bg-white shadow"), Container(false, Div(Class("flex items-center space-x-4 h-16"), Div(Class("flex-shrink-0"), outline.Globe(Class("h-6 w-6"))), NavbarLink("/", "Home", path), ), ), ) } func NavbarLink(path, text, currentPath string) g.Node { active := path == currentPath return A(Href(path), g.Text(text), c.Classes{ "text-lg font-medium hover:text-indigo-900": true, "text-indigo-700": active, "text-indigo-500": !active, }, ) } func Container(padY bool, children ...g.Node) g.Node { return Div( c.Classes{ "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8": true, "py-4 sm:py-6 lg:py-8": padY, }, g.Group(children), ) } func Prose(children ...g.Node) g.Node { return Div(Class("prose lg:prose-lg xl:prose-xl prose-indigo"), g.Group(children)) }
What you see here are your first view components. They're all just functions that return a Node
from the gomponents package. Node
is an interface that has one function in it:
type Node interface {
Render(io.Writer) error
}
This means that a Node
knows how to render itself to HTML. That's it. Anything you create that satisfies this interface can be a view component. Luckily for us, most common HTML elements and attributes are available in the gomponents package, so all we have to do is combine them.
Page
componentThe Page
component returns a whole HTML 5 page, including the necessary document header with a title, language attribute, and a link to the TailwindCSS .
For the body, it adds the Navbar
component, and wraps the page content in the Container
and Prose
components.
Navbar
& NavbarLink
componentsThe Navbar
component is a navigation bar that sits at the top of each page. It has a little globe icon in the beginning, from the gomponents-heroicons
package. That package provides nice SVG icons that you can use directly on your page.
The navigation bar then has links to the available pages, which is currently just the front page.
This is the first time you're seeing TailwindCSS classes in a component. They're basically like inline HTML styles, except way more fancy. They all go in the class
attributes of the HTML elements, and for example make the navigation bar white, with a shadow, by using the classes bg-white shadow
. Otherwise, you can ignore them for now. (If you don't want to ignore them, that's cool. I'll wait while you deep-dive on tailwindcss.com. ๐)
There's one more thing I'd like to point out, so you can understand the code. The Classes
component is a special kind of map, where the keys are CSS classes, and the values are a boolean that say whether the key should be included in the rendered HTML class
attribute or not. For example, if a NavbarLink
is active, it gets the CSS class text-indigo-700
, and otherwise text-indigo-500
.
Container
& Prose
I'll just quickly describe these two simple components.
Container
is a small component that restricts its own width, and sets some horizontal and optionally vertical padding inside it. It uses the same Classes
component as the NavbarLink
from before.
Prose
renders content inside it, such as text, images, and links, with a nice default style, so we don't have to style all page content manually.
Page
Now we have our building blocks. Let's create the actual front page!
We need two more files. The first goes into views/front.go
and has this content:
views/front.gopackage views import ( g "github.com/maragudk/gomponents" . "github.com/maragudk/gomponents/html" ) func FrontPage() g.Node { return Page( "Canvas", "/", H1(g.Text(`Solutions to problems.`)), P(g.Text(`Do you have problems? We also had problems.`)), P(g.Raw(`Then we created the <em>canvas</em> app, and now we don't! ๐ฌ`)), ) }
It defines a FrontPage
component, which uses the Page
component from before to render a page with a title ("Canvas"), a headline, and some text. Note how we can inline plain old HTML using the Raw
function. The Text
function would escape the <em>
text.
The second file is at handlers/views.go
, and creates a single HTTP handler for us that renders the front page, using our FrontPage
component:
handlers/views.gopackage handlers import ( "net/http" "github.com/go-chi/chi/v5" "canvas/views" ) func FrontPage(mux chi.Router) { mux.Get("/", func(w http.ResponseWriter, r *http.Request) { _ = views.FrontPage().Render(w) }) }
We ignore the error returned from Render
for now, because there's currently nothing sensible we could do with it.
The very last thing we need to do is add our new route to server/routes.go
, and update our server integration test to check for a HTTP 200
status code instead of HTTP 404
:
server/routes.gopackage server import ( "canvas/handlers" ) func (s *Server) setupRoutes() { handlers.Health(s.mux) handlers.FrontPage(s.mux) }
server/server_test.gopackage server_test import ( "net/http" "testing" "github.com/matryer/is" "canvas/integrationtest" ) func TestServer_Start(t *testing.T) { integrationtest.SkipIfShort(t) t.Run("starts the server and listens for requests", func(t *testing.T) { is := is.New(t) cleanup := integrationtest.CreateServer() defer cleanup() resp, err := http.Get("http://localhost:8081/") is.NoErr(err) is.Equal(http.StatusOK, resp.StatusCode) }) }
The moment has come! Run make start
and go to localhost:8080 in your browser, and you should see your new, be-a-u-tiful frontpage!
Alright! ๐ Run make build deploy
to put your new front page into the cloud, and then go celebrate.
Get help at support@golang.dk.