Template
1
0
Fork 0

Updated to BunRouter

This commit is contained in:
Atridad Lahiji 2024-01-24 11:22:33 -07:00
parent ab639dec20
commit 8d3f803474
No known key found for this signature in database
17 changed files with 387 additions and 160 deletions

View file

@ -2,7 +2,7 @@
Go + Templates + HTMX Go + Templates + HTMX
## Stack: ## Stack:
- Backend: Golang + Echo - Backend: Golang + BunRouter
- Rendering: Golang templates - Rendering: Golang templates
- Style: TailwindCSS + DaisyUI - Style: TailwindCSS + DaisyUI
- Content format: Markdown - Content format: Markdown
@ -19,5 +19,5 @@ Go + Templates + HTMX
5. Run ```air``` to start the dev server 5. Run ```air``` to start the dev server
## Tests ## Tests
Without Coverage: `go test atri.dad/lib` Without Coverage: `go test goth.stack/lib`
With Coverage: `go test atri.dad/lib -cover` With Coverage: `go test goth.stack/lib -cover`

View file

@ -3,9 +3,11 @@ package api
import ( import (
"net/http" "net/http"
"github.com/labstack/echo/v4" "github.com/uptrace/bunrouter"
) )
func Ping(c echo.Context) error { func Ping(w http.ResponseWriter, req bunrouter.Request) error {
return c.String(http.StatusOK, "Pong!") w.WriteHeader(http.StatusOK)
w.Write([]byte("Pong!"))
return nil
} }

View file

@ -3,26 +3,28 @@ package api
import ( import (
"fmt" "fmt"
"log" "log"
"net/http"
"time" "time"
"github.com/labstack/echo/v4" "github.com/uptrace/bunrouter"
"goth.stack/lib" "goth.stack/lib"
) )
func SSEDemo(c echo.Context) error { func SSE(w http.ResponseWriter, req bunrouter.Request) error {
channel := c.QueryParam("channel") queryParams := req.URL.Query()
channel := queryParams.Get("channel")
if channel == "" { if channel == "" {
channel = "default" channel = "default"
} }
// Use the request context, which is cancelled when the client disconnects // Use the request context, which is cancelled when the client disconnects
ctx := c.Request().Context() ctx := req.Context()
pubsub, _ := lib.Subscribe(lib.RedisClient, channel) pubsub, _ := lib.Subscribe(lib.RedisClient, channel)
c.Response().Header().Set(echo.HeaderContentType, "text/event-stream") w.Header().Set("Content-Type", "text/event-stream")
c.Response().Header().Set(echo.HeaderConnection, "keep-alive") w.Header().Set("Connection", "keep-alive")
c.Response().Header().Set(echo.HeaderCacheControl, "no-cache") w.Header().Set("Cache-Control", "no-cache")
// Create a ticker that fires every 15 seconds // Create a ticker that fires every 15 seconds
ticker := time.NewTicker(30 * time.Second) ticker := time.NewTicker(30 * time.Second)
@ -35,10 +37,10 @@ func SSEDemo(c echo.Context) error {
return nil return nil
case <-ticker.C: case <-ticker.C:
// Every 30 seconds, send a comment to keep the connection alive // Every 30 seconds, send a comment to keep the connection alive
if _, err := c.Response().Write([]byte(": keep-alive\n\n")); err != nil { if _, err := w.Write([]byte(": keep-alive\n\n")); err != nil {
return err return err
} }
c.Response().Flush() w.(http.Flusher).Flush()
default: default:
// Handle incoming messages as before // Handle incoming messages as before
msg, err := pubsub.ReceiveMessage(ctx) msg, err := pubsub.ReceiveMessage(ctx)
@ -48,11 +50,11 @@ func SSEDemo(c echo.Context) error {
} }
data := fmt.Sprintf("data: %s\n\n", msg.Payload) data := fmt.Sprintf("data: %s\n\n", msg.Payload)
if _, err := c.Response().Write([]byte(data)); err != nil { if _, err := w.Write([]byte(data)); err != nil {
return err return err
} }
c.Response().Flush() w.(http.Flusher).Flush()
} }
} }
} }

View file

@ -1,25 +1,32 @@
package api package api
import ( import (
"encoding/json"
"net/http" "net/http"
"github.com/labstack/echo/v4" "github.com/uptrace/bunrouter"
"goth.stack/lib" "goth.stack/lib"
) )
func SSEDemoSend(c echo.Context) error { func SSEDemoSend(w http.ResponseWriter, req bunrouter.Request) error {
channel := c.QueryParam("channel") // Get query parameters
queryParams := req.URL.Query()
// Get channel from query parameters
channel := queryParams.Get("channel")
if channel == "" { if channel == "" {
channel = "default" channel = "default"
} }
// Get message from query parameters, form value, or request body // Get message from query parameters, form value, or request body
message := c.QueryParam("message") message := queryParams.Get("message")
if message == "" { if message == "" {
message = c.FormValue("message") message = req.PostFormValue("message")
if message == "" { if message == "" {
var body map[string]string var body map[string]string
if err := c.Bind(&body); err != nil { err := json.NewDecoder(req.Body).Decode(&body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return err return err
} }
message = body["message"] message = body["message"]
@ -27,11 +34,18 @@ func SSEDemoSend(c echo.Context) error {
} }
if message == "" { if message == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "message parameter is required"}) errMsg := map[string]string{"error": "message parameter is required"}
errMsgBytes, _ := json.Marshal(errMsg)
http.Error(w, string(errMsgBytes), http.StatusBadRequest)
return nil
} }
// Send message // Send message
lib.SendSSE("default", message) lib.SendSSE("default", message)
return c.JSON(http.StatusOK, map[string]string{"status": "message sent"}) statusMsg := map[string]string{"status": "message sent"}
statusMsgBytes, _ := json.Marshal(statusMsg)
w.Write(statusMsgBytes)
return nil
} }

16
go.mod
View file

@ -4,7 +4,6 @@ go 1.21.6
require ( require (
github.com/alecthomas/chroma/v2 v2.12.0 github.com/alecthomas/chroma/v2 v2.12.0
github.com/labstack/echo/v4 v4.11.4
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
) )
@ -14,29 +13,30 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
require ( require (
github.com/alecthomas/assert/v2 v2.4.1 github.com/alecthomas/assert/v2 v2.4.1
github.com/go-redis/redismock/v9 v9.2.0 github.com/go-redis/redismock/v9 v9.2.0
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/redis/go-redis/v9 v9.4.0 github.com/redis/go-redis/v9 v9.4.0
github.com/resendlabs/resend-go v1.7.0 github.com/resendlabs/resend-go v1.7.0
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/unrolled/secure v1.14.0
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/uptrace/bunrouter v1.0.21
github.com/uptrace/bunrouter/extra/reqlog v1.0.21
github.com/yuin/goldmark v1.6.0 github.com/yuin/goldmark v1.6.0
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/net v0.20.0 // indirect golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )

38
go.sum
View file

@ -21,22 +21,22 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw= github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0= github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@ -58,17 +58,25 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/unrolled/secure v1.14.0 h1:u9vJTU/pR4Bny0ntLUMxdfLtmIRGvQf2sEFuA0TG9AE=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/unrolled/secure v1.14.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/uptrace/bunrouter v1.0.21 h1:HXarvX+N834sXyHpl+I/TuE11m19kLW/qG5u3YpHUag=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/uptrace/bunrouter v1.0.21/go.mod h1:TwT7Bc0ztF2Z2q/ZzMuSVkcb/Ig/d3MQeP2cxn3e1hI=
github.com/uptrace/bunrouter/extra/reqlog v1.0.21 h1:k9ebZATe9NOBUrPcMYJAQ2OMb62ERRN7qfwNwoa6Kxs=
github.com/uptrace/bunrouter/extra/reqlog v1.0.21/go.mod h1:j+pXrYzYe3OxTzFb4f/f2JL+CVoGVi1QJJiU0YKSUlw=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -77,8 +85,6 @@ golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=

41
lib/templates.go Normal file
View file

@ -0,0 +1,41 @@
package lib
import (
"html/template"
"log"
"net/http"
"path/filepath"
"runtime"
)
func RenderTemplate(w http.ResponseWriter, layout string, partials []string, props interface{}) error {
// Get the name of the current file
_, filename, _, _ := runtime.Caller(1)
page := filepath.Base(filename)
page = page[:len(page)-len(filepath.Ext(page))] // remove the file extension
// Build the list of templates
templates := []string{
"./pages/templates/layouts/" + layout + ".html",
"./pages/templates/" + page + ".html",
}
for _, partial := range partials {
templates = append(templates, "./pages/templates/partials/"+partial+".html")
}
// Parse the templates
ts, err := template.ParseFiles(templates...)
if err != nil {
log.Print(err.Error())
return err
}
// Execute the layout template
err = ts.ExecuteTemplate(w, layout, props)
if err != nil {
log.Print(err.Error())
return err
}
return nil
}

56
main.go
View file

@ -2,14 +2,17 @@ package main
import ( import (
"io" "io"
"log"
"net/http"
"text/template" "text/template"
"goth.stack/api" "goth.stack/api"
"goth.stack/middleware"
"goth.stack/pages" "goth.stack/pages"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/labstack/echo/v4" "github.com/uptrace/bunrouter"
"github.com/labstack/echo/v4/middleware" "github.com/uptrace/bunrouter/extra/reqlog"
) )
// Template Type // Template Type
@ -18,7 +21,7 @@ type Template struct {
} }
// Template Render function // Template Render function
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { func (t *Template) Render(w io.Writer, name string, data interface{}) error {
return t.templates.ExecuteTemplate(w, name, data) return t.templates.ExecuteTemplate(w, name, data)
} }
@ -26,38 +29,31 @@ func main() {
godotenv.Load(".env") godotenv.Load(".env")
// Initialize router // Initialize router
e := echo.New() router := bunrouter.New(
bunrouter.Use(reqlog.NewMiddleware(), middleware.RequestID, middleware.SecureHeaders),
)
// Middlewares // Static server
e.Pre(middleware.RemoveTrailingSlash()) fs := http.FileServer(http.Dir("public"))
e.Use(middleware.Logger())
e.Use(middleware.RequestID())
e.Use(middleware.Secure())
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Level: 5,
}))
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(69)))
// Template Parsing router.GET("/public/*filepath", func(w http.ResponseWriter, req bunrouter.Request) error {
t := &Template{ http.StripPrefix("/public", fs).ServeHTTP(w, req.Request)
templates: template.Must(template.ParseGlob("pages/**/*.html")), return nil
} })
e.Renderer = t
// // Static server
e.Static("/public", "public")
// Page routes // Page routes
e.GET("/", pages.Home) pageGroup := router.NewGroup("", bunrouter.Use(middleware.NewRateLimiter(50).RateLimit))
e.GET("/blog", pages.Blog) pageGroup.GET("/", pages.Home)
e.GET("/post/:post", pages.Post) pageGroup.GET("/blog", pages.Blog)
e.GET("/ssedemo", pages.SSEDemo) pageGroup.GET("/post/:post", pages.Post)
pageGroup.GET("/sse", pages.SSEDemo)
// API Routes: // API Routes:
e.GET("/api/ping", api.Ping) apiGroup := router.NewGroup("/api")
e.GET("/api/ssedemo", api.SSEDemo) apiGroup.GET("/ping", api.Ping)
e.POST("/api/sendsse", api.SSEDemoSend)
e.Logger.Fatal(e.Start(":3000")) apiGroup.GET("/sse", api.SSE)
apiGroup.POST("/sendsse", api.SSEDemoSend)
log.Fatal(http.ListenAndServe(":3000", router))
} }

60
middleware/ratelimit.go Normal file
View file

@ -0,0 +1,60 @@
package middleware
import (
"net"
"net/http"
"sync"
"time"
"github.com/uptrace/bunrouter"
)
type rateLimiter struct {
visitors map[string]*visitor
mu sync.Mutex
rps int
}
type visitor struct {
firstSeen time.Time
requests int
}
func NewRateLimiter(rps int) *rateLimiter {
return &rateLimiter{
visitors: make(map[string]*visitor),
rps: rps,
}
}
func (rl *rateLimiter) RateLimit(next bunrouter.HandlerFunc) bunrouter.HandlerFunc {
return func(w http.ResponseWriter, req bunrouter.Request) error {
rl.mu.Lock()
defer rl.mu.Unlock()
ip, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
// handle error, e.g., return an HTTP 500 error
http.Error(w, "Internal server error", http.StatusInternalServerError)
return nil
}
v, exists := rl.visitors[ip]
if !exists || time.Since(v.firstSeen) > 1*time.Minute {
v = &visitor{
firstSeen: time.Now(),
}
rl.visitors[ip] = v
}
v.requests++
// Limit each IP to rps requests per minute
if v.requests > rl.rps {
http.Error(w, "Too many requests", http.StatusTooManyRequests)
return nil
}
return next(w, req)
}
}

41
middleware/requestid.go Normal file
View file

@ -0,0 +1,41 @@
package middleware
import (
"context"
"net/http"
"github.com/google/uuid"
"github.com/uptrace/bunrouter"
)
type contextKey string
func (c contextKey) String() string {
return string(c)
}
var (
HeaderXRequestID = "X-Request-ID"
requestIDKey = contextKey("requestID")
)
func RequestID(next bunrouter.HandlerFunc) bunrouter.HandlerFunc {
return func(w http.ResponseWriter, req bunrouter.Request) error {
reqID := req.Header.Get(HeaderXRequestID)
if reqID == "" {
reqID = uuid.New().String()
}
ctx := context.WithValue(req.Context(), requestIDKey, reqID)
w.Header().Set(HeaderXRequestID, reqID)
return next(w, req.WithContext(ctx))
}
}
func GetRequestID(ctx context.Context) string {
if reqID, ok := ctx.Value(requestIDKey).(string); ok {
return reqID
}
return ""
}

21
middleware/secure.go Normal file
View file

@ -0,0 +1,21 @@
package middleware
import (
"net/http"
"github.com/unrolled/secure"
"github.com/uptrace/bunrouter"
)
func SecureHeaders(next bunrouter.HandlerFunc) bunrouter.HandlerFunc {
secureMiddleware := secure.New(secure.Options{
FrameDeny: true,
ContentTypeNosniff: true,
BrowserXssFilter: true,
})
return func(w http.ResponseWriter, req bunrouter.Request) error {
secureMiddleware.HandlerFuncWithNext(w, req.Request, nil)
return next(w, req)
}
}

View file

@ -1,7 +1,6 @@
package pages package pages
import ( import (
"html/template"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -9,7 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/labstack/echo/v4" "github.com/uptrace/bunrouter"
"goth.stack/lib" "goth.stack/lib"
) )
@ -17,18 +16,20 @@ type BlogProps struct {
Posts []lib.CardLink Posts []lib.CardLink
} }
func Blog(c echo.Context) error { func Blog(w http.ResponseWriter, req bunrouter.Request) error {
var posts []lib.CardLink var posts []lib.CardLink
files, err := os.ReadDir("./content/") files, err := os.ReadDir("./content/")
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "There was an finding posts!") http.Error(w, "There was an issue finding posts!", http.StatusInternalServerError)
return nil
} }
for _, file := range files { for _, file := range files {
frontMatter, err := lib.ExtractFrontMatter(file, "./content/") frontMatter, err := lib.ExtractFrontMatter(file, "./content/")
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering the posts!") http.Error(w, "There was an issue rendering the posts!", http.StatusInternalServerError)
return nil
} }
frontMatter.Href = "post/" + strings.TrimSuffix(file.Name(), ".md") frontMatter.Href = "post/" + strings.TrimSuffix(file.Name(), ".md")
@ -57,19 +58,9 @@ func Blog(c echo.Context) error {
Posts: posts, Posts: posts,
} }
templates := []string{ // Specify the partials used by this page
"./pages/templates/layouts/base.html", partials := []string{"header", "navitems", "cardlinks"}
"./pages/templates/partials/header.html",
"./pages/templates/partials/navitems.html",
"./pages/templates/partials/cardlinks.html",
"./pages/templates/blog.html",
}
ts, err := template.ParseFiles(templates...) // Render the template
if err != nil { return lib.RenderTemplate(w, "base", partials, props)
log.Print(err.Error())
return err
}
return ts.ExecuteTemplate(c.Response().Writer, "base", props)
} }

File diff suppressed because one or more lines are too long

View file

@ -7,7 +7,7 @@ import (
"os" "os"
chromahtml "github.com/alecthomas/chroma/v2/formatters/html" chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/labstack/echo/v4" "github.com/uptrace/bunrouter"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting/v2" highlighting "github.com/yuin/goldmark-highlighting/v2"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
@ -21,24 +21,27 @@ type PostProps struct {
Tags []string Tags []string
} }
func Post(c echo.Context) error { func Post(w http.ResponseWriter, req bunrouter.Request) error {
postName := c.ParamValues()[0] postName := req.Param("post")
filePath := "content/" + postName + ".md" filePath := "content/" + postName + ".md"
md, err := os.ReadFile(filePath) md, err := os.ReadFile(filePath)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "This post does not exist!") http.Error(w, "This post does not exist!", http.StatusNotFound)
return nil
} }
frontmatterBytes, content, err := lib.SplitFrontmatter(md) frontmatterBytes, content, err := lib.SplitFrontmatter(md)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering this post!") http.Error(w, "There was an issue rendering this post!", http.StatusInternalServerError)
return nil
} }
var frontmatter lib.FrontMatter var frontmatter lib.FrontMatter
if err := yaml.Unmarshal(frontmatterBytes, &frontmatter); err != nil { if err := yaml.Unmarshal(frontmatterBytes, &frontmatter); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering this post!") http.Error(w, "There was an issue rendering this post!", http.StatusInternalServerError)
return nil
} }
var buf bytes.Buffer var buf bytes.Buffer
@ -54,7 +57,8 @@ func Post(c echo.Context) error {
) )
if err := markdown.Convert(content, &buf); err != nil { if err := markdown.Convert(content, &buf); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering this post!") http.Error(w, "There was an issue rendering this post!", http.StatusInternalServerError)
return nil
} }
props := PostProps{ props := PostProps{
@ -64,17 +68,9 @@ func Post(c echo.Context) error {
Tags: frontmatter.Tags, Tags: frontmatter.Tags,
} }
templates := []string{ // Specify the partials used by this page
"./pages/templates/layouts/post.html", partials := []string{"header", "navitems"}
"./pages/templates/partials/header.html",
"./pages/templates/partials/navitems.html",
"./pages/templates/post.html",
}
ts, err := template.ParseFiles(templates...) // Render the template
if err != nil { return lib.RenderTemplate(w, "post", partials, props)
return echo.NewHTTPError(http.StatusInternalServerError, "There was an issue rendering this post!")
}
return ts.ExecuteTemplate(c.Response().Writer, "post", props)
} }

View file

@ -1,25 +1,16 @@
package pages package pages
import ( import (
"html/template" "net/http"
"log"
"github.com/labstack/echo/v4" "github.com/uptrace/bunrouter"
"goth.stack/lib"
) )
func SSEDemo(c echo.Context) error { func SSEDemo(w http.ResponseWriter, req bunrouter.Request) error {
templates := []string{ // Specify the partials used by this page
"./pages/templates/layouts/base.html", partials := []string{"header", "navitems"}
"./pages/templates/partials/header.html",
"./pages/templates/partials/navitems.html",
"./pages/templates/ssedemo.html",
}
ts, err := template.ParseFiles(templates...) // Render the template
if err != nil { return lib.RenderTemplate(w, "base", partials, nil)
log.Print(err.Error())
return err
}
return ts.ExecuteTemplate(c.Response().Writer, "base", nil)
} }

View file

@ -5,7 +5,7 @@
</a> </a>
</li> </li>
<li> <li>
<a class="no-underline" href="/ssedemo"> <a class="no-underline" href="/sse">
SSE Demo SSE Demo
</a> </a>
</li> </li>

View file

@ -13,7 +13,7 @@ GOTH // SSE <div class="badge badge-accent">DEMO</div>
<h1 class="text-4xl">Server Sent Events</h1> <h1 class="text-4xl">Server Sent Events</h1>
<h2 class="text-xl">This page demonstrates the use of the <a href="https://htmx.org/extensions/sse/">HTMX SSE Extention</a> to receive Server Sent Events on the "default" channel.</h2> <h2 class="text-xl">This page demonstrates the use of the <a href="https://htmx.org/extensions/sse/">HTMX SSE Extention</a> to receive Server Sent Events on the "default" channel.</h2>
<p class="text-lg">Any events received on the "default" channel will appear below:</p> <p class="text-lg">Any events received on the "default" channel will appear below:</p>
<div hx-ext="sse" sse-connect="/api/ssedemo" sse-swap="message"> <div hx-ext="sse" sse-connect="/api/sse" sse-swap="message">
Waiting for SSE Message... Waiting for SSE Message...
</div> </div>