Open frederikhors opened 3 years ago
I found this: https://github.com/gorilla/mux#serving-single-page-applications.
Maybe we can update our example using some code from there.
go-chi & go:embed
Nice! I’ll update when I can.. it’ll be nice to finish the docs PR too and include a section on file serving too, embedding, etc. PRs are welcome :)
go-chi & go:embed
This is amazing! Thanks!
go-chi & go:embed
Thank you @utamori.
Is there another way of using embed with chi
than diverting NotFound as demonstrated here?
With net/http
ServerMux I can do something like this:
//go:embed static
var embeddedFS embed.FS
r := http.NewServeMux()
r.Handle("/", http.FileServer(http.FS(embeddedFS)))
err = http.ListenAndServe(":8080", r)
...
This is not possible with chi
by simply replacing http.NewServeMux()
with chi.NewRouter()
. Has anyone a working example?
Is there another way of using embed with
chi
than diverting NotFound as demonstrated here?With
net/http
ServerMux I can do something like this://go:embed static var embeddedFS embed.FS r := http.NewServeMux() r.Handle("/", http.FileServer(http.FS(embeddedFS))) err = http.ListenAndServe(":8080", r) ...
This is not possible with
chi
by simply replacinghttp.NewServeMux()
withchi.NewRouter()
. Has anyone a working example?
It works, but the standard library mux has its quirks that you are not aware of, namely it will treat /
path as everything that starts with /
. With chi you need to be explicit and use /*
to achieve the same:
package main
import (
"embed"
"log"
"net/http"
"github.com/go-chi/chi/v5"
)
//go:embed assets
var embeddedFS embed.FS
func main() {
r := chi.NewRouter()
r.Handle("/*", http.FileServer(http.FS(embeddedFS)))
log.Fatal(http.ListenAndServe(":8080", r))
}
Is there another way of using embed with
chi
than diverting NotFound as demonstrated here? Withnet/http
ServerMux I can do something like this://go:embed static var embeddedFS embed.FS r := http.NewServeMux() r.Handle("/", http.FileServer(http.FS(embeddedFS))) err = http.ListenAndServe(":8080", r) ...
This is not possible with
chi
by simply replacinghttp.NewServeMux()
withchi.NewRouter()
. Has anyone a working example?It works, but the standard library mux has its quirks that you are not aware of, namely it will treat
/
path as everything that starts with/
. With chi you need to be explicit and use/*
to achieve the same:package main import ( "embed" "log" "net/http" "github.com/go-chi/chi/v5" ) //go:embed assets var embeddedFS embed.FS func main() { r := chi.NewRouter() r.Handle("/*", http.FileServer(http.FS(embeddedFS))) log.Fatal(http.ListenAndServe(":8080", r)) }
@ivanduka Awesome, thank you so much!
@ivanduka how do you redirect to index.html
to implement client side routing?
@ivanduka how do you redirect to
index.html
to implement client side routing?
I assume that you are trying to serve your single-page application.
I declare all my API endpoints and the last thing in my list of routes is a call to NotFound:
router := chi.NewRouter()
// Here I declare all my API endpoints. If none of them are hit, then:
router.NotFound(spa.Handle(app.Assets))
All my static assets are preloaded to a map with content type, etag, etc. precalculated:
type Asset struct {
Name string
FileContent []byte
ContentType string
Etag string
ContentLength string
LongCache bool
}
Then I serve it like this:
package spa
import (
"net/http"
)
const indexHTML = "index.html"
func Handle(assets map[string]Asset) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
cleanPath := r.URL.Path
if len(cleanPath) > 0 && cleanPath[0] == '/' {
cleanPath = r.URL.Path[1:]
}
file, ok := assets[cleanPath]
if !ok {
file = assets[indexHTML]
}
sendFile(w, r.Header.Get("If-None-Match"), file)
}
}
func sendFile(w http.ResponseWriter, match string, file Asset) {
if match != "" && match == file.Etag {
w.WriteHeader(http.StatusNotModified)
return
}
if file.LongCache {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
}
w.Header().Set("Etag", file.Etag)
w.Header().Set("Content-Type", file.ContentType)
w.Header().Set("Content-Length", file.ContentLength)
_, _ = w.Write(file.FileContent)
}
Basically, the idea is to serve a file if it exists, and if not - serve index.html
Adopted from mux:
package shared
import (
"net/http"
"os"
"path/filepath"
)
// SPAHandler serves a single page application.
func SPAHandler(staticPath string) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Join internally call path.Clean to prevent directory traversal
path := filepath.Join(staticPath, r.URL.Path)
// check whether a file exists or is a directory at the given path
fi, err := os.Stat(path)
if os.IsNotExist(err) || fi.IsDir() {
// set cache control header to prevent caching
// this is to prevent the browser from caching the index.html
// and serving old build of SPA App
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
// file does not exist or path is a directory, serve index.html
http.ServeFile(w, r, filepath.Join(staticPath, "index.html"))
return
}
if err != nil {
// if we got an error (that wasn't that the file doesn't exist) stating the
// file, return a 500 internal server error and stop
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// set cache control header to serve file for a year
// static files in this case need to be cache busted
// (usualy by appending a hash to the filename)
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
// otherwise, use http.FileServer to serve the static file
http.FileServer(http.Dir(staticPath)).ServeHTTP(w, r)
})
}
Usage
router := chi.NewRouter()
// Here I declare all my API endpoints. If none of them are hit, then:
router.NotFound(shared.SPAHandler("./dist"))
Here is a solution I use for go embed:
package site
import (
"embed"
"fmt"
"io/fs"
"net/http"
"os"
"path"
"strings"
)
//go:embed dist/*
var spaFiles embed.FS
func SPAHandler() http.HandlerFunc {
spaFS, err := fs.Sub(spaFiles, "dist")
if err != nil {
panic(fmt.Errorf("failed getting the sub tree for the site files: %w", err))
}
return func(w http.ResponseWriter, r *http.Request) {
f, err := spaFS.Open(strings.TrimPrefix(path.Clean(r.URL.Path), "/"))
if err == nil {
defer f.Close()
}
if os.IsNotExist(err) {
r.URL.Path = "/"
}
http.FileServer(http.FS(spaFS)).ServeHTTP(w, r)
}
}
You can register it with r.Handle("/*", site.SPAHandler())
. You can also register it with r.NotFound(site.SPAHandler)
but I'm not sure what advantage that would give over just using r.Handle
.
Can I ask you to update the fileserver example for an SPA (Single Page Application) with redirect to
index.html
avoiding the listing of directories likeimages
?Chi is amazing but for newbies like me it helps to have updated examples and recipes, like https://github.com/gofiber/recipes.
Thank you with all my heart. ❤️