How to Embed a Single Page App Inside a Go Web Executable
You're probalby here because you searched how to embed an SPA in a go executable
(or, more than likely, you're from my community and I posted a link to this post in our forum and you're checking it out. I see you). Either way, let me tell you how to get it done.
Follow These Steps
- Build your single page app (SPA) in whatever tool you like, they all basically work the same way anyway
- Put it inside of your Go project. Something like
web
would be nice - Add a go file in there
web/embed.go
- That file should export an
embed.FS
instance that points to your SPA's generated output - Add a handler that will point to the SPA's resources directory
- Add a handler that will capture all routes, but always return the SPA's index.html (or 200.html for those weird webservers)
Thats it!
Let's See Some Code
I got you.
Step 1
Put this file in the directory that your SPA's source code is in.
package web
import (
"embed"
)
//go:embed build/**
var BuildFS embed.FS
In this example the folder is web
and my SPA puts its files in the build
directory. That go:embed $somePattern
coupled with the variable will basically create a virutal filesystem that will be included in your executable. It's just like having actual files to navigate.
Step 2
Create some HTTP handlers -- one to serve files from that virual filesystem we were just talking about and the other to pass everything to the entry file for your SPA.
func staticFileHandler() http.HandlerFunc {
f, err := fs.Sub(web.BuildFS, "build")
return http.FileServer(http.FS(f)).ServeHTTP
}
func websiteHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/favicon.ico" {
rawFile, err := web.BuildFS.ReadFile("build/favicon.ico")
w.Write(rawFile)
return
}
rawFile, _ := web.BuildFS.ReadFile("build/index.html")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(rawFile)
}
error handling omitted
In the staticFileHandler
I create a new filesystem by trimming the leading build
directory from the path. This will allow the browser to request a resource and correctly find it.
The websiteHandler
does a bit more work. It will first check to see if the browser is requesting the favicon, because that typically lives in th site root (this is where you'd add more of these if
checks if you have other resources in the root dir). Then it will read the index.html from the virutal file system and write that out. Your SPA will get rendered and look a the url path and react accordingly.
Step 3
Update your routes to point to those handlers. You want these to be last as they are bascially catch-alls.
// other routes up here, you can call them from the SPA
g.router.PathPrefix("_/app").Handler(staticFileHandler)
g.router.PathPrefix("/").Handler(websiteHandler)
In this exmaple
g
is an instance of a Gorilla router and my SPA (svelte) writes its stuff to an _app directory
Now when you run go build
, you'll create a single executable that will be able handle http requests and serve your SPA at the same time. Shit, you probably should make your SPA call those other routes your app serves. Imagine have an API and a front end to consueme it in one package, pretty neat. Your api and SPA will be on the same domain (wihout the need to proxy via nginx or something fancy) so you can share cookies and wont have to worry about CORS.