Embedding assets in the binary

Hey everyone!

I use nspawn instead of docker and prefer one binary deploys. Traefik is used for the reverse proxy without a CDN. This assumes the api and frontend directories are next to each other. For example:

~/code.vikunja.io/api/
~/code.vikunja.io/frontend/

In the frontend, add your domain to window.API_URL in public/index.html. Then build it.

yarn run build

The heart of embedding assets lies in pkg/routes/routes.go in the api. Add this to the end of the RegisterRoutes() function. Build and run it. This will pull the assets from the frontend and serve it up from the same server (same hostname/port).

	// Static files
	e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
		// Root directory from where the static content is served.
		// Required.
		Root: "../frontend/dist",
		// Enable HTML5 mode by forwarding all not-found requests to root so that
		// SPA (single-page application) can handle the routing.
		// Optional. Default value false.
		HTML5: true,
	}))

However, the assets live outside the binary. Let’s add them to the binary. Vikunja uses the Echo framework and their Embed Resources Recipe uses go.rice. Replace the above snippet with this.

	// The file server for rice. "../../../frontend/dist" contains the frontend built for production.
	staticBox := rice.MustFindBox("../../../frontend/dist")
	// Serves static files from the rice box
	e.Any("/*", echo.WrapHandler(http.FileServer(staticBox.HTTPBox())))

Add the imports at the top.

import (
...
	"github.com/GeertJohan/go.rice"
	"net/http"
)

Go ahead and build it but it’s not ready just yet. We need to add the assets to the binary. If you haven’t already, you’ll need to install go.rice.

go get github.com/GeertJohan/go.rice/rice

Now run these commands to append the frontend/dist files into the binary. This only works if you’re in the same directory as routes.go.

cd pkg/routes
rice append --exec ../../vikunja

Run the binary and it serve up the assets. But if you refresh the page, you’ll discover a 404 for routes like /login and /register. That’s because those are routes, not files and the api backend doesn’t know how to handle the frontend routes. Let’s fix that. Add this to after the code above in routes.go. This will redirect /login and /register to the home page which then loads the application.

	// Handle specific paths so that a browser refresh doesn't end up with a 404.
	e.GET("/login", func(c echo.Context) error {
		return c.Redirect(http.StatusSeeOther, "/")
	})

	e.GET("/register", func(c echo.Context) error {
		return c.Redirect(http.StatusSeeOther, "/")
	})

The down side to this is the assets are not gzipped. That’s not a big deal for me because Traefik handles the compression. Also, go.rice offers other ways to embed the assets that may be better in certain circumstances.

Enjoy!

Welcome!

Thank you for the contribution.

I have a couple of notes:

  1. Vikunja already uses vfstemplate to embed the mail templates into the binary - maybe it could be use to also serve the assets.
  2. You should redirect (or better yet, serve the frontend assets) all requests not starting with /api*/ to avoid having to manually go through the hoops of finding a task again which was opened when restarting the browser. Your current solution with redirects break stuff like that but also password resets or share links.

vfstemplate can definitely be used. I’m not familiar with it and didn’t know how the package names made it to the route.

Re: routing and redirects: I saw one post about a catch all route needing to use a custom HTTPErrorHandler to send back the index.html. I tried but could not get it to work. Echo#Match does not support regex and I could not see where it supports boolean NOT. It seems the better solution is a lot more in depth because it combines the frontend routing in the backend rather than just “serve up these files”. I’m not fluent enough in Go to dive that deep. Perhaps another day.

Cheers!

1 Like