Keep Reading
If you enjoyed the post you just read, we have more to say!
Sites built with JavaScript frameworks like Next.js are often complicated and rely heavily on JavaScript, even for primitive tasks like submitting forms, state management, hydration, etc.
Modern sites built with React tend to use JavaScript as a holistic language for serving, rendering, and routing in a website. The problem with this approach is that if you have business logic in a different programming language like Go for example, you have to create more abstractions and APIs between your backend and frontend.
Next.js is a React meta-framework, which means that you’re already starting with a fairly large bundle size because of the React and Next.js dependencies. Rect-dom
is ~ already 42kB (minified and gzipped). Next.js also sends a huge chunk of JSON in script tags for hydration and pre-loading data; for a simple static website, this is an overkill.
HTMX is a small (~14k minified and gzipped), dependency-free JavaScript library. It doesn't matter which language you use for your backend or business logic, which is great because we can use our existing business logic code in Go for our website.
You can use a Go templating language to generate HTML markup and use HTMX to make the site dynamic.
Templ is a tool for building HTML with Go. It includes features like a templating language but can really do more than that—server-side rendering, static rendering, loops, variables, and other Go stuff.
You can create a Templ component by creating a .templ
file.
package main
templ hello(name string) {
<div>Hello, { name }</div>
}
Templ comes with a CLI that can be used to compile .templ
files to Go files, the Go files act as templates and can be rendered using an HTTP server.
mkdir hello-world
cd hello-world
go mod init github.com/<username>/<repository>
net/http
library to create a simple web server; first, create a main.go
file:// main.go
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
io.WriteString(w, "Hello World! \n")
})
http.ListenAndServe(":3000", nil)
fmt.Println("Listening on Port 3000 🚀")
}
You can run this program with the command
go run .
If you visit localhost:3000 in your browser, you will see "Hello World!"
go install
commandgo install github.com/a-h/templ/cmd/templ@latest
go get github.com/a-h/templ
hello.templ
in the same directory as main.go
and add the following code// hello.templ
package main
templ hello(name string) {
<h1>Hello, { name }</h1>
}
templ generate
The generate
commands creates a hello_templ.go
file. The hello_templ.go
file will have a hello
function that can be called with a name argument.
github.com/a-h/templ
and replace the existing handler function with a templ.Handler
function.// main.go
import (
"fmt"
"github.com/a-h/templ"
"net/http"
)
func main() {
component := hello("Templ!")
http.Handle("/", templ.Handler(component))
fmt.Println("Listening on port 3000 🚀")
http.ListenAndServe(":3000", nil)
}
go run .
If you visit localhost:3000, you will see a heading that says, “Hello, Templ!”
layout.templ
file in the root directory:// layout.templ
package main
templ layout(title string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{ title }</title>
</head>
<body>
{ children... }
</body>
</html>
}
All of our Templ page components will be wrapped by the Layout
component.
This is helpful because we would not have to type the HTML boilerplate code in every Templ file, and the layout file would serve as a single file where we can import HTMX. We are using the template composition syntax in Templ to be able to create a parent component.
We pass a title
and use it like { title }
this syntax allows us to pass variables in the components and render them in the markup.
<!-- layout.templ -->
<!-- ... -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Title</title>
<!-- HTMX -->
<script src="<https://unpkg.com/htmx.org@2.0.3>" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous"></script>
</head>
<!-- ... -->
npm install -D tailwindcss
npx tailwindcss init
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./*.templ"],
theme: {
extend: {},
},
plugins: [],
}
Make sure to specify the path where your Templ files are located, in this case we are doing everything in the root directory.
input.css
file in the root directory and add:@tailwind base;
@tailwind components;
@tailwind utilities;
yarn tailwindcss -i input.css -o static/css/tw.css --watch
layout.templ
<!-- Layout.templ -->
<!-- ... -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/css/tw.css"/>
<title>Title</title>
<!-- HTMX -->
<script src="../lib/htmx.min.js"></script>
</head>
<!-- ... -->
We now have Templ, HTMX, and tailwind setup ready to use.
tw.css
file from our go serverpackage main
import (
"fmt"
"github.com/a-h/templ"
"net/http"
)
func main() {
component := hello("Templ!")
http.Handle("/", templ.Handler(component))
// server static files
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", fs)
fmt.Println("Listening on :3000")
http.ListenAndServe(":3000", nil)
}
layout
component in the hello.templ
filepackage main
templ hello(name string) {
@layout("👋 Hello World!") {
<h1 class="text-xl text-violet-800">Hello, { name }</h1>
}
}
templ generate
go run .
Visit localhost:3000
to view the page! 🚀
The setup works but is quite tedious to use; it requires us to restart the Go server and re-run the templ generate
command every time we make any changes in the codebase. We also need to refresh the browser every time to see the change.
There are a few options for watching file changes and re-running Go automatically like air and gow. Both of them are excellent tools and work for us. Let's use gow
to watch for changes and restart Go server:
gow run . -e go,mod,html,css
Templ's command line tool comes with a built-in watcher which recompiles the templ
files to Go every time there is a change:
templ generate -watch --include-timestamp false --include-version false
But we can do better with Templ's proxy argument.
We don't need to re-run go
or templ
anymore but we still need to refresh the browser every time there is a change. We can use the templ
's command line tool's proxy for hot reloading the browser, let's add some more arguments to templ generate
:
templ generate -watch -include-timestamp false -include-version false -proxy="<http://localhost:3000>" -proxyport="8080" -open-browser=false -cmd="gow run ."
This command will start a proxy server to run on the port 8080
. You can visit localhost:8080 for development, and it will live reload every time a Templ or Go file changes.
JavaScript's ecosystem to develop websites is more mature than Go and provides a lot more features out of the box like -
Using Go and HTMX is still new to us and presents different challenges that we may want to explore.
Keep an eye out on our Twitter as we learn more about building static sites with Go.