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

  1. Build your single page app (SPA) in whatever tool you like, they all basically work the same way anyway
  2. Put it inside of your Go project. Something like web would be nice
  3. Add a go file in there web/embed.go
  4. That file should export an embed.FS instance that points to your SPA's generated output
  5. Add a handler that will point to the SPA's resources directory
  6. 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.

@emehrkay