diff --git a/.gitignore b/.gitignore index 8e27bf6..57e57de 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ Session.vim .vercel *_templ.go dist +tmp +bin diff --git a/api/hello.go b/api/hello.go new file mode 100644 index 0000000..7095201 --- /dev/null +++ b/api/hello.go @@ -0,0 +1,45 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "math/rand/v2" + "net/http" +) + +type helloObj struct { + Language string + Hello string +} + +func getHelloList() ([]helloObj, error) { + res, err := http.Get("https://raw.githubusercontent.com/novellac/multilanguage-hello-json/master/hello.json") + if err != nil { + return nil, err + } + bytes, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + var hellos []helloObj + err = json.Unmarshal(bytes, &hellos) + if err != nil { + return nil, err + } + + return hellos, nil +} + +func Hello(w http.ResponseWriter, r *http.Request) { + hellos, err := getHelloList() + var hello string + if err != nil { + hello = "Welcome!" + } else { + hello = hellos[rand.IntN(len(hellos)-1)].Hello + } + + fmt.Fprint(w, hello) +} diff --git a/cmd/build/main.go b/cmd/build/main.go new file mode 100644 index 0000000..e8d73aa --- /dev/null +++ b/cmd/build/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "flag" + "log" + + "www/config" + "www/internals" +) + +func main() { + dir := flag.String("d", "./dist", "the directory to write the files") + staticDir := flag.String("s", "./static", "the directory to copy static files from") + + w := internals.StaticWriter{ + DistDir: dir, + StaticDir: staticDir, + Pages: config.ROUTES, + Context: context.Background(), + Logger: *log.Default(), + } + + err := w.WriteAll() + if err != nil { + log.Fatal(err) + } +} diff --git a/cmd/vercel/main.go b/cmd/vercel/main.go new file mode 100644 index 0000000..e321e6e --- /dev/null +++ b/cmd/vercel/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + "os" + + "www/config" + "www/internals" +) + +type VercelConfig struct { + OutputDirectory string `json:"outputDirectory"` +} + +var logger = log.Default() + +func main() { + configPath := flag.String("c", "./vercel.json", "the path to the vercel.json file") + staticDir := flag.String("s", "./static", "the directory to copy static files from") + port := flag.Int("p", 8080, "the port to run the server") + + configFile, err := os.ReadFile(*configPath) + if err != nil { + logger.Fatalf("Unable to read vercel.json file due to:\n%s", err) + } + + var c VercelConfig + err = json.Unmarshal(configFile, &c) + if err != nil { + logger.Fatalf("Unable to parse vercel.json file due to:\n%s", err) + } + + w := internals.StaticWriter{ + DistDir: &c.OutputDirectory, + StaticDir: staticDir, + Pages: config.ROUTES, + Context: context.Background(), + Logger: *log.Default(), + } + + logger.Print("Writing static files") + err = w.WriteAll() + if err != nil { + logger.Fatal(err) + } + + logger.Print("Starting server") + mux := http.NewServeMux() + + config.APIROUTES(mux) + mux.Handle("/", http.FileServer(http.Dir(c.OutputDirectory))) + + logger.Printf("Running server at port: %v", *port) + err = http.ListenAndServe(fmt.Sprintf(":%v", *port), mux) + if err != nil { + logger.Fatalf("Server crashed due to:\n%s", err) + } +} diff --git a/config/routes.go b/config/routes.go new file mode 100644 index 0000000..bbce360 --- /dev/null +++ b/config/routes.go @@ -0,0 +1,17 @@ +package config + +import ( + "net/http" + + "www/api" + "www/internals" + "www/pages" +) + +var ROUTES = []internals.Page{ + {Path: "index.html", Component: pages.Homepage()}, +} + +func APIROUTES(mux *http.ServeMux) { + mux.HandleFunc("/api/hello", api.Hello) +} diff --git a/flake.lock b/flake.lock index c70a9ff..24f3c81 100644 --- a/flake.lock +++ b/flake.lock @@ -96,11 +96,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1716137900, - "narHash": "sha256-sowPU+tLQv8GlqtVtsXioTKeaQvlMz/pefcdwg8MvfM=", + "lastModified": 1716293225, + "narHash": "sha256-pU9ViBVE3XYb70xZx+jK6SEVphvt7xMTbm6yDIF4xPs=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6c0b7a92c30122196a761b440ac0d46d3d9954f1", + "rev": "3eaeaeb6b1e08a016380c279f8846e0bd8808916", "type": "github" }, "original": { @@ -171,15 +171,16 @@ "xc": "xc" }, "locked": { - "lastModified": 1716195313, - "narHash": "sha256-/7UL2Oqpp9FPYVF0SJ32/q7inHJIwZDnoSmJN/323ck=", + "lastModified": 1716034419, + "narHash": "sha256-z/sb4AlFOU20sBEAu12VSXqhHQuqvj3mUu7JTvyc1pI=", "owner": "a-h", "repo": "templ", - "rev": "e369eaf5ca569e50ae3a17931d84de9d335f6db0", + "rev": "0c14a899236d115a790b5a960b5d2b50c277c77e", "type": "github" }, "original": { "owner": "a-h", + "ref": "tags/v0.2.697", "repo": "templ", "type": "github" } diff --git a/flake.nix b/flake.nix index b5cbe15..fe8ea67 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,7 @@ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; - templ.url = "github:a-h/templ"; + templ.url = "github:a-h/templ?ref=tags/v0.2.697"; }; outputs = { self @@ -29,9 +29,6 @@ templ nodePackages_latest.vercel ]; - shellHook = " - export GOOS=linux - "; }; }); } diff --git a/go.mod b/go.mod index f970146..23152ec 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module guz.one +module www go 1.22.2 diff --git a/internals/static_writer.go b/internals/static_writer.go new file mode 100644 index 0000000..976a1a7 --- /dev/null +++ b/internals/static_writer.go @@ -0,0 +1,107 @@ +package internals + +import ( + "context" + "io" + "io/fs" + "log" + "os" + "path/filepath" + "strings" + + "github.com/a-h/templ" +) + +const PERMISSIONS = 0755 + +type Page struct { + Path string + Component templ.Component +} + +type StaticWriter struct { + DistDir *string + StaticDir *string + Pages []Page + Context context.Context + Logger log.Logger +} + +func (w *StaticWriter) WritePage(path string, writer func(ctx context.Context, w io.Writer) error) error { + directory := filepath.Dir(path) + err := os.MkdirAll(directory, PERMISSIONS) + if err != nil { + return err + } + + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + err = writer(w.Context, f) + return err +} + +func (w *StaticWriter) WriteAll() error { + for _, page := range w.Pages { + p := filepath.Join(*w.DistDir, page.Path) + w.Logger.Printf("Writing page %s", p) + err := w.WritePage(p, page.Component.Render) + if err != nil { + return err + } + } + + err := filepath.WalkDir(*w.StaticDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } else if d.IsDir() || path == *w.StaticDir { + return nil + } + + f, err := filepath.Abs(path) + if err != nil { + return err + } + s, err := filepath.Abs(*w.StaticDir) + if err != nil { + return err + } + + err = w.CopyStatic(strings.TrimPrefix(f, s)) + if err != nil { + return err + } + return nil + }) + return err +} + +func (w *StaticWriter) CopyStatic(path string) error { + c, err := os.ReadFile(filepath.Join(*w.StaticDir, path)) + if err != nil { + return err + } + + p := filepath.Join(*w.DistDir, path) + err = os.MkdirAll(filepath.Dir(p), PERMISSIONS) + if err != nil { + return err + } + + f, err := os.Create(p) + if err != nil { + return err + } + defer f.Close() + + b, err := f.Write(c) + if err != nil { + return err + } + w.Logger.Printf("Wrote %v bytes in %s", b, p) + + return nil +} diff --git a/layouts/page.templ b/layouts/page.templ new file mode 100644 index 0000000..d216377 --- /dev/null +++ b/layouts/page.templ @@ -0,0 +1,12 @@ +package layouts + +templ Page(title string) { + + + { title } + + + { children... } + + +} diff --git a/main.go b/main.go index 7d9c12e..1d3228a 100644 --- a/main.go +++ b/main.go @@ -1,22 +1,69 @@ package main import ( - "context" + "flag" + "fmt" "log" "net/http" + "slices" + "strings" - "guz.one/api" - "guz.one/pages" + "www/config" + "www/internals" ) +var logger = log.Default() + func main() { + staticDir := flag.String("s", "./static", "the directory to copy static files from") + port := flag.Int("p", 8080, "the port to run the server") + mux := http.NewServeMux() - mux.HandleFunc("/api/hello", api.Hello) + config.APIROUTES(mux) + for _, route := range config.ROUTES { + path := "/" + strings.TrimSuffix(route.Path, ".html") + if path == "/index" { + continue + } + logger.Printf("Registering page route. page=%s route=%s", route.Path, path) + + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + logger.Printf("Handling request. path=%s", r.URL.Path) + + w.Header().Add("Content-Type", "text/html") + + err := route.Component.Render(r.Context(), w) + if err != nil { + logger.Fatalf("Unable to render route %s due to %s", route.Path, err) + } + }) + } mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - err := pages.Homepage().Render(context.Background(), w) - _ = err + if r.URL.Path != "/" { + logger.Printf("Handling file server request. path=%s", r.URL.Path) + http.FileServer(http.Dir(*staticDir)).ServeHTTP(w, r) + return + } + + logger.Printf("Handling request. path=%s", r.URL.Path) + + w.Header().Add("Content-Type", "text/html") + + index := slices.IndexFunc(config.ROUTES, func(route internals.Page) bool { + return route.Path == "index.html" + }) + indexPage := config.ROUTES[index] + + err := indexPage.Component.Render(r.Context(), w) + if err != nil { + log.Fatalf("Unable to render index page due to %s", err) + } }) - log.Fatal(http.ListenAndServe(":5432", mux)) + logger.Printf("Running server at port: %v", *port) + err := http.ListenAndServe(fmt.Sprintf(":%v", *port), mux) + if err != nil { + logger.Fatalf("Server crashed due to:\n%s", err) + } } diff --git a/makefile b/makefile new file mode 100644 index 0000000..f661f8b --- /dev/null +++ b/makefile @@ -0,0 +1,47 @@ +PORT?=8080 + +all: run + +dev: + air -build.pre_cmd 'make templ' \ + -build.include_ext 'templ' \ + -proxy.enabled true \ + -proxy.app_port $(PORT) \ + -proxy.proxy_port $$(($(PORT) + 1)) \ + -- -p $(PORT) + +dev-vercel: + air -build.pre_cmd 'make build-vercel' \ + -build.include_ext 'templ' \ + -build.cmd 'make build-vercel' \ + -build.bin './bin/vercel' \ + -proxy.enabled true \ + -proxy.app_port $(PORT) \ + -proxy.proxy_port $$(($(PORT) + 1)) \ + -- -p $(PORT) + +run: bin/www + ./bin/www + +run-vercel: bin/vercel + ./bin/vercel + +build-static: templ + go run ./cmd/build/main.go + +build-vercel: bin/vercel build-static + +bin/www: main.go templ + go build -o ./bin/www ./main.go + +bin/vercel: cmd/vercel/main.go templ build-vercel + go build -o ./bin/vercel ./cmd/vercel/main.go + +templ: + templ generate + +clean: + if [[ -d "dist" ]]; then rm -r ./dist; fi + if [[ -d "tmp" ]]; then rm -r ./tmp; fi + if [[ -d "bin" ]]; then rm -r ./bin; fi + rm $(TEMPL_FILES) diff --git a/pages/homepage.templ b/pages/homepage.templ new file mode 100644 index 0000000..55f47fa --- /dev/null +++ b/pages/homepage.templ @@ -0,0 +1,10 @@ +package pages + +import ( + "www/layouts" +) + +templ Homepage() { + + @layouts.Page("Hello world") +} diff --git a/static/logo-013-dark.svg b/static/logo-013-dark.svg new file mode 100644 index 0000000..37aea5f --- /dev/null +++ b/static/logo-013-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/static/robots.txt @@ -0,0 +1 @@ +test diff --git a/vercel.json b/vercel.json index 89db77a..1a28888 100644 --- a/vercel.json +++ b/vercel.json @@ -2,3 +2,4 @@ "$schema": "https://openapi.vercel.sh/vercel.json", "outputDirectory": "dist" } +