package main import ( "embed" "flag" "fmt" "html/template" "io/fs" "log/slog" "net/http" "os" "strings" "time" ) //go:embed web/static/* var staticFS embed.FS //go:embed web/html/* var htmlTemplates embed.FS func main() { var ( address string profiles links services links ) flag.StringVar(&address, "address", "0.0.0.0:8080", "The address that the web server will listen on") flag.Var(&services, "service", "A list of service links") flag.Var(&profiles, "profile", "A list of profile links") flag.Parse() setupLogging() mux, err := routes(services, profiles) if err != nil { slog.Error("Unable to create the Mux", "error", err) os.Exit(1) } server := http.Server{ Addr: address, Handler: mux, ReadHeaderTimeout: 1 * time.Second, ReadTimeout: 5 * time.Second, } slog.Info("Starting web application.", "address", address) err = server.ListenAndServe() if err != nil { slog.Error("Failed to run the web application", "error", err) } } func routes(services, profiles links) (*http.ServeMux, error) { mux := http.NewServeMux() staticRootFS, err := fs.Sub(staticFS, "web/static") if err != nil { return nil, fmt.Errorf("unable to create the static root file system; %w", err) } fileServer := http.FileServer(http.FS(staticRootFS)) mux.Handle("/static/", http.StripPrefix("/static", neuter(fileServer))) mux.HandleFunc("/", landing(services, profiles)) return mux, nil } func neuter(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if strings.HasSuffix(request.URL.Path, "/") { notFound(writer) return } next.ServeHTTP(writer, request) }) } func landing(services, profiles links) func(http.ResponseWriter, *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { if request.URL.Path != "/" { notFound(writer) return } if request.Method != http.MethodGet { writer.Header().Set("Allow", http.MethodGet) clientError(writer, http.StatusMethodNotAllowed) return } tmpl, err := template.New("base").ParseFS(htmlTemplates, "web/html/base.tmpl.html") if err != nil { serverError(writer, fmt.Errorf("error parsing the HTML template; %w", err)) return } links := struct { Services links Profiles links }{ Services: services, Profiles: profiles, } if err = tmpl.Execute(writer, &links); err != nil { serverError(writer, fmt.Errorf("error rendering the HTML templates; %w", err)) } } } func notFound(w http.ResponseWriter) { clientError(w, http.StatusNotFound) } func clientError(w http.ResponseWriter, status int) { http.Error(w, http.StatusText(status), status) } func serverError(w http.ResponseWriter, err error) { slog.Error(err.Error()) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } func setupLogging() { opts := slog.HandlerOptions{ AddSource: false, } logger := slog.New(slog.NewJSONHandler(os.Stdout, &opts)) slog.SetDefault(logger) }