chore: merge pull request #1 from dot013/vercel-static-experiment
Go + Templ + HTMX + Vercel deployment experiment
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@ Session.vim
|
||||
.vercel
|
||||
*_templ.go
|
||||
dist
|
||||
tmp
|
||||
bin
|
||||
|
||||
45
api/hello.go
Normal file
45
api/hello.go
Normal 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
28
cmd/build/main.go
Normal 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
63
cmd/vercel/main.go
Normal 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
17
config/routes.go
Normal 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
13
flake.lock
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
";
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
107
internals/static_writer.go
Normal file
107
internals/static_writer.go
Normal 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
12
layouts/page.templ
Normal file
@@ -0,0 +1,12 @@
|
||||
package layouts
|
||||
|
||||
templ Page(title string) {
|
||||
<html>
|
||||
<head>
|
||||
<title>{ title }</title>
|
||||
</head>
|
||||
<body>
|
||||
{ children... }
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
61
main.go
61
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)
|
||||
}
|
||||
}
|
||||
|
||||
47
makefile
Normal file
47
makefile
Normal 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
10
pages/homepage.templ
Normal 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
1
static/logo-013-dark.svg
Normal 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
1
static/robots.txt
Normal file
@@ -0,0 +1 @@
|
||||
test
|
||||
@@ -2,3 +2,4 @@
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"outputDirectory": "dist"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user