chi
chi copied to clipboard
Fileserver example for an SPA (Single Page Application)
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 like images
?
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. ❤️
I found this: https://github.com/gorilla/mux#serving-single-page-applications.
Maybe we can update our example using some code from there.
I also found: http://kefblog.com/2017-04-07/How-to-serve-static-files-with-custom-not-found-handler.
go-chi & go:embed
https://github.com/shibukawa/spa-go-1.16
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
https://github.com/shibukawa/spa-go-1.16
This is amazing! Thanks!
go-chi & go:embed
https://github.com/shibukawa/spa-go-1.16
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
.