chore: merge pull request #1 from dot013/vercel-static-experiment

Go + Templ + HTMX + Vercel deployment experiment
This commit is contained in:
Guz
2024-05-23 11:23:31 -03:00
committed by GitHub
16 changed files with 397 additions and 18 deletions

2
.gitignore vendored
View File

@@ -3,3 +3,5 @@ Session.vim
.vercel
*_templ.go
dist
tmp
bin

45
api/hello.go Normal file
View File

@@ -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)
}

28
cmd/build/main.go Normal file
View File

@@ -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)
}
}

63
cmd/vercel/main.go Normal file
View File

@@ -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)
}
}

17
config/routes.go Normal file
View File

@@ -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)
}

13
flake.lock generated
View File

@@ -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"
}

View File

@@ -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
";
};
});
}

2
go.mod
View File

@@ -1,4 +1,4 @@
module guz.one
module www
go 1.22.2

107
internals/static_writer.go Normal file
View File

@@ -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
}

12
layouts/page.templ Normal file
View File

@@ -0,0 +1,12 @@
package layouts
templ Page(title string) {
<html>
<head>
<title>{ title }</title>
</head>
<body>
{ children... }
</body>
</html>
}

61
main.go
View File

@@ -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)
}
}

47
makefile Normal file
View File

@@ -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)

10
pages/homepage.templ Normal file
View File

@@ -0,0 +1,10 @@
package pages
import (
"www/layouts"
)
templ Homepage() {
<img src="/logo-013-dark.svg" alt="" width="100" height="100"/>
@layouts.Page("Hello world")
}

1
static/logo-013-dark.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 186.23 93.51"><defs><style>.cls-1{fill:#111;}</style></defs><g id="logos"><g id="dark"><polygon class="cls-1" points="68.37 51.8 22.71 2.1 5.59 17.14 51.25 66.85 68.37 51.8"/><rect class="cls-1" x="69.45" width="23.01" height="61.64"/><polygon class="cls-1" points="145.09 24.19 185.73 24.19 185.73 1.68 105.09 1.68 146.25 70.92 0 71 0.01 93.51 186.23 93.41 145.09 24.19"/></g></g></svg>

After

Width:  |  Height:  |  Size: 440 B

1
static/robots.txt Normal file
View File

@@ -0,0 +1 @@
test

View File

@@ -2,3 +2,4 @@
"$schema": "https://openapi.vercel.sh/vercel.json",
"outputDirectory": "dist"
}