Create a front page

You'll learn
  • Creating an HTML front page using the gomponents view library.
  • Using TailwindCSS for nice styling.

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

See the diff on Github.

Using gomponents

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
Bonus: gomponents?

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.go
package 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.

The Page component

The 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.

The Navbar & NavbarLink components

The 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.

Our first 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.go
package 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.go
package 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.go
package server import ( "canvas/handlers" ) func (s *Server) setupRoutes() { handlers.Health(s.mux) handlers.FrontPage(s.mux) }
server/server_test.go
package 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) }) }

Try it out!

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!

Screenshot of the front page in a browser.

Alright! ๐Ÿ˜„ Run make build deploy to put your new front page into the cloud, and then go celebrate.

Review questions

Sign up or log in to get review questions with teacher feedback by email! ๐Ÿ“ง

Questions?

Get help at support@golang.dk.