generated from templates/go-generic
Dan Anglin
a0e3ee8a6f
Add a landing page for the Flow Platform. The landing page is a linktree-styled static page made with Go, HTML and CSS.
144 lines
3 KiB
Go
144 lines
3 KiB
Go
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)
|
|
}
|