From d38fc83df3824b840f0b3930e4cb7236bdab84b2 Mon Sep 17 00:00:00 2001 From: a73x Date: Tue, 27 Aug 2024 19:43:56 +0100 Subject: feat(content): support templating in content this is tired person code don't write tired person code --- README | 4 +- content/index.md | 38 ++++++ content/posts.md | 6 + content/posts/001.md | 36 ++++++ content/posts/002.md | 117 +++++++++++++++++++ content/posts/003.md | 20 ++++ content/posts/004.md | 251 ++++++++++++++++++++++++++++++++++++++++ embed.go | 4 +- go.mod | 6 +- go.sum | 11 +- html/html.go | 19 +++ markdown/markdown.go | 76 ++++++++++++ pages/pages.go | 107 +++++++++++++++++ posts/001.md | 34 ------ posts/002.md | 116 ------------------- posts/003.md | 19 --- posts/004.md | 250 --------------------------------------- proxy/Dockerfile | 16 --- proxy/fly.toml | 22 ---- proxy/nginx.conf | 14 --- proxy/start.sh | 6 - templates/default.html | 5 + templates/index.html | 54 --------- templates/layouts/_default.html | 75 ++++++++++++ templates/layouts/default.html | 75 ------------ templates/post.html | 6 - templates/posts.html | 9 -- web/web.go | 138 ++-------------------- 28 files changed, 773 insertions(+), 761 deletions(-) create mode 100644 content/index.md create mode 100644 content/posts.md create mode 100644 content/posts/001.md create mode 100644 content/posts/002.md create mode 100644 content/posts/003.md create mode 100644 content/posts/004.md create mode 100644 html/html.go create mode 100644 markdown/markdown.go create mode 100644 pages/pages.go delete mode 100644 posts/001.md delete mode 100644 posts/002.md delete mode 100644 posts/003.md delete mode 100644 posts/004.md delete mode 100644 proxy/Dockerfile delete mode 100644 proxy/fly.toml delete mode 100644 proxy/nginx.conf delete mode 100755 proxy/start.sh create mode 100644 templates/default.html delete mode 100644 templates/index.html create mode 100644 templates/layouts/_default.html delete mode 100644 templates/layouts/default.html delete mode 100644 templates/post.html delete mode 100644 templates/posts.html diff --git a/README b/README index ae8fe58..de4eab4 100644 --- a/README +++ b/README @@ -1 +1,3 @@ -just enough framework for a site \ No newline at end of file +just enough framework for a site + +my website in a binary. \ No newline at end of file diff --git a/content/index.md b/content/index.md new file mode 100644 index 0000000..e42c4d5 --- /dev/null +++ b/content/index.md @@ -0,0 +1,38 @@ +--- +title: me +--- +* backend cloud software engineer +* lang: go +* infra: kubernetes + +## tidbits + +### #go + +* layout packages by what they do, not by their abstract type +* use channels sparingly. write synchronous methods and allow the caller to make it async +* `append` modifies the underlying slice, you'll only make this mistake once +* define interfaces where you use them +* `make([]int, 5)` has a length and capacity of 5. `([]int, 0,5)` has a length of 0 and capacity of 5. + `append()` will only do what you want with the latter +* don't use `init()` +* TFBO (test, fix, benchmark, optimise) +* more CPU != more performance + more CPU == more Contention + +### #git + +* `git reflog` +* `git commit --fixup=` + `git rebase origin/main --autosquash` + +## resources + +* [`proc.go`](https://cs.opensource.google/go/go/+/refs/tags/go1.23.0:src/runtime/proc.go) +* [proposal: runtime/metrics: define a recommended set of metrics](https://github.com/golang/go/issues/67120) + +## books + +* [Designing Data Intensive Applications](https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063) +* [Database Internals](https://www.oreilly.com/library/view/database-internals/9781492040330) +* [Efficient Go](https://www.oreilly.com/library/view/efficient-go/9781098105709) \ No newline at end of file diff --git a/content/posts.md b/content/posts.md new file mode 100644 index 0000000..020e1cc --- /dev/null +++ b/content/posts.md @@ -0,0 +1,6 @@ +--- +title: posts +--- +{{range .Collections.posts}} +- [{{.Meta.title}}]({{.Path}}) +{{end}} \ No newline at end of file diff --git a/content/posts/001.md b/content/posts/001.md new file mode 100644 index 0000000..48ea720 --- /dev/null +++ b/content/posts/001.md @@ -0,0 +1,36 @@ +--- +title: "Go Benchmarking" +tags: posts +--- +1. write a benchmark +2. run a benchmark +3. get a profile +4. optimise +5. run your tests +6. goto 2. + +## cpuprofile +`go test -test=XXX -bench -cpuprofile ` + +## memprofile +`go test -test=XXX -bench -memprofile -benchmem` + +## pprof +[pprof usage](https://github.com/google/pprof/blob/main/doc/README.md) + +`go pprof -http=:8080 profile.pb.gz` +will show a web UI for analysing the profile. + +### views: +- flame graph: `localhost:8080/ui/flamegraph` + - shows percentage breakdown of how much resource each "call" made. + - clicking a box will make it "100%" allowing for deep diving + - right click "show source code" to view +- top: `localhost:8080/ui/top` + - shows top functions + - `flat`: profile samples in this function + - `cum`: (cumulative) profile samples in this function and its callees +- source: `localhost:8080/ui/source` + - each source line is annotated with the time spent in that source line + - the first number does not count time spent in functions called from the source line + - the second number does \ No newline at end of file diff --git a/content/posts/002.md b/content/posts/002.md new file mode 100644 index 0000000..8e22144 --- /dev/null +++ b/content/posts/002.md @@ -0,0 +1,117 @@ +--- +title: Go Project Layouts +tags: posts +--- + +Do you lay awake at night and consider how to optimally layout your Go project? +No...? what about recommending Windows to a friend or colleague?? +yeah me either... + +I've seen a lot online that shows what I can only describe as endgame enterprise Go project layouts. These layouts are heavily folder-based and only make sense when your project has grown large enough to warrant the verbosity these folders provide. My only problem is that people often try to start there. + +A lot of design tells you to think about your project in layers. +- api +- domain +- storage + +If you read [The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) +you get told the layers should be, + +1. entities +2. use cases +3. interface adapters +4. frameworks and drivers. + +and that all dependencies should point in (yeah I know, I didn't do a circle so "in" doesn't make sense but I'm sure you can follow). + +This is an excellent idea; separation of concerns is good. + +So you make your folders. +``` +. +├── drivers +├── entities +├── interfaces +└── usecases +``` +aaand this is an awful idea. I don't even want to go further into this hypothetical layout because it hurts too much. + +Find me a project that actually creates these folders, and I'll find you the medium article titled "Clean Code in Go" which spawned it. + +The important parts of clean code, are the ideas presented, and how you apply them to a package orientated language. Creating a folder to represent each layer, doesn't really carry much weight here. + +As a package orientated language, we want to think and reason about things in terms of packages. Yes there will be a point where you may want to group your packages into some group, but that is mostly ceremonial. +Go doesn't care if you're accessing `domain/bar` or `domain/foo/bar`. Either will simply be accessed as `bar`. This means that what matters what's in that package `bar`. Since everything will be read as `bar.Thing` i.e `import bytes` and `bytes.Buffer`. + +So, the package name sets context and expectations. If I grab the `json` package, I expect that package to do things around `json`. I'd feel a bit confused if I was able to configure an smtp server. + +If you cannot come up with a package name that’s a meaningful prefix for the package’s contents, the package abstraction boundary may be wrong + +"but you've still not provided a good example?" +well +yes + +I think the project should grow organically to some degree. What we want to do is write code, and refactoring in Go is fairly cheap. + +Start with a `main.go` and make a `Run` function or some equivalent which it calls. + +```go + +func Run() error { + // actual important stuff here +} +func main() { + if err := Run(); err != nil { + log.Fatal(err) + } +} +``` + +This allows you to test your run function in a unit test, and keeps your `main` func minimal. + +As your project grows, you can keep it flat inside the root directory + +``` +├── api.go +├── go.mod +├── go.sum +├── main.go +├── rss.go +└── sqlite.go +``` +Even just glancing at that, you can guess that this might be an RSS server, that uses sqlite to back it. + +Who knows what +``` +├── drivers +├── entities +├── interfaces +└── usecases +``` + +does. + +As things evolve you might want to put them in `internal` to hide them from being imported by other packages, or `cmd` as you develop multiple binaries. Placing things in `internal` means you're free to mess around with it, without breaking any public contracts other users rely on. +I can't be bothered rewriting my example, so here's a random one I found online; it's probably all right. +[Server Project](https://go.dev/doc/modules/layout#server-project) + +``` +project-root-directory/ + go.mod + internal/ + auth/ + ... + metrics/ + ... + model/ + ... + cmd/ + api-server/ + main.go + metrics-analyzer/ + main.go + ... + ... the project's other directories with non-Go code +``` + +My vague summary is that clean code gives you a north star to follow, an idea of how you want to separate and reason about the packages you create. You don't need to create the entities of abstraction that are also presented. Think about what things do or relate to and create packages for them. You should allow your project to grow organically but don't expect architecture to appear without following a north star. \ No newline at end of file diff --git a/content/posts/003.md b/content/posts/003.md new file mode 100644 index 0000000..e19e891 --- /dev/null +++ b/content/posts/003.md @@ -0,0 +1,20 @@ +--- +title: "Levels of Optimisation" +tags: posts +--- +This probably isn't strictly true, but it makes sense to me. +We've got three levels of "optimisation" (assuming your actual design doesn't suck and needs optimising). + +## Benchmark Optimisation +To begin with, we have benchmark optimisation; you create a benchmark locally, dump a profile of it, and optimise it. Then, you run your tests because the most optimal solution is "return nil" and make sure you didn't break your tests. +This is the first and easiest optimisation because it only requires a function, nothing else, and can be done in isolation. You don't need a working "application" here, just the function you're trying to benchmark. There are different types of benchmarks, micro, macro, etc., but I'm leaving them out of scope for this conversation. Go read [Efficient Go](https://learning.oreilly.com/library/view/efficient-go/9781098105709/). + +## Profile guided optimisation +This is a mild step up from benchmark optimisation only because you need a live server load from which you use to pull a profile, but it is probably the most hands-off step. You import the `net/http/pprof` package into your service, call the `debug/profile?seconds=30` to get a profile, and compile your binary with `go build -pgo=profile.pgo`. The compiler will make optimisations for you, and even if your profile is garbage, it shouldn't cause any regressions. + +You probably want to get a few profiles and merge them using `go tool pprof -proto a.out b.out > merged`. This will help provide optimisations that are more relevant to your overall system; instead of just a single 30s slice. +Also, if you have long-running calls that are chained together, a 30-second snapshot might not be enough, so try a sample with a longer window. + +## Runtime optimisation +This is where you expose `/runtime/metrics` and monitor them continuously. There's a list of metrics that you might be interested in, a recommended set of metrics, and generally, you are looking to optimise your interactions with the go runtime. There are a few stats here: goroutine counts, goroutines waiting to run, heap size, how often garbage collection runs, how long garbage collection takes, etc. All useful information to use when optimising - when garbage collection is running, your program ain't. It's also useful for finding memory leaks; it becomes pretty obvious you are leaking goroutines when you graph the count and just watch it go up and never down. +It's also just lowkey fun to look at the exposed data and understand what your system is doing. \ No newline at end of file diff --git a/content/posts/004.md b/content/posts/004.md new file mode 100644 index 0000000..ebfaad2 --- /dev/null +++ b/content/posts/004.md @@ -0,0 +1,251 @@ +--- +title: "Writing HTTP Handlers" +tags: posts +--- + +I'm sharing how I write handlers in Go. + +I write them like this for reasons that are probably fairly contextual. I've written a few applications and had to swap REST libraries or even swapped REST for GRPC, so things that make that easier speak to me a great deal. + +I've used `ints` instead of the `http.StatusXXXX` and omitted `JSON` tags in an attempt to try save up screen space. + +To begin with, you might have something like this: +``` +package main + +import ( + "fmt" + "log" + "net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, World!") +} + +func main() { + http.HandleFunc("/", handler) + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +``` + +Then you might get told off because you've just registered routes with the default mux, which isn't very testable. + +So you tweak it a little bit. +``` +package main + +import ( + "fmt" + "log" + "net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, World!") +} + +func newMux() *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/", handler) + + return mux +} + +func Run() error { + mux := newMux() + return http.ListenAndServe(":8080", mux) +} + +func main() { + if err := Run(); err != nil { + log.Fatal(err) + } +} +``` + +`newMux()` gives you a `mux` to use when testing. + +`Run` keeps `main` nice and clean, so you can just return errors as needed instead of going `log.Fatal` and just generally being messy. + +But now you need to do something real, you want to store and fetch data. + +``` +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" +) + +func NewMux() *http.ServeMux { + mux := http.NewServeMux() + s := Server{ + data: make(map[int]Content), + } + + s.Register(mux) + return mux +} + +func Run() error { + mux := NewMux() + return http.ListenAndServe(":8080", mux) +} + +type Server struct { + data map[int]Content +} + +func (s *Server) Register(mux *http.ServeMux) { + mux.HandleFunc("GET /{id}", s.Get) + mux.HandleFunc("POST /", s.Post) +} + +func (s *Server) Get(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + if err != nil { + w.WriteHeader(400) + w.Write([]byte(fmt.Sprintf("failed to parse id: %v", err))) + return + } + data, ok := s.data[id] + if !ok { + w.WriteHeader(404) + w.Write([]byte("not found")) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + json.NewEncoder(w).Encode(data) +} + +type ContentPostReq struct { + Foo string +} + +func (s *Server) Post(w http.ResponseWriter, r *http.Request) { + req := ContentPostReq{} + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(400) + w.Write([]byte(fmt.Sprintf("failed to parse request: %v", err))) + return + } + id := len(s.data) + content := Content{ + ID: id, + Foo: req.Foo, + } + s.data[id] = content + + w.WriteHeader(200) + json.NewEncoder(w).Encode(content) +} + +type Content struct { + ID int + Foo string +} + +func main() { + if err := Run(); err != nil { + log.Fatal(err) + } +} +``` + +``` +❯ curl -X POST localhost:8080 --header "Content-Type: application/json" -d '{"foo":"bar"}' +{"ID":0,"Foo":"bar"} +❯ curl -X GET localhost:8080/0 +{"ID":0,"Foo":"bar"} +``` + +Erm, well, okay. Quite a bit has changed here, but I'm sure you can read it. We now save and fetch very, very safely from a map and return the response as `JSON`. I've done some things for brevity because I want to get to the main point. + +This API is inconsistent. It sometimes returns `JSON`, and the others return strings. Overall, it's just a mess. + +So let's try to standardise things. +First, let's design some form of REST spec. + +``` +type JSONResp[T any] struct { + Resources []T + Errs []ErrorResp +} + +type ErrorResp struct { + Status int + Msg string +} +``` +We want to be able to support fetching multiple resources at once, if we can only fetch some resources, let's return them under `resources` and show the errors under `errs` + +Now, add some helpful functions to handle things. +``` +func Post[In any, Out any](successCode int, fn func(context.Context, In) ([]Out, []ErrorResp)) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var v In + + if err := json.NewDecoder(r.Body).Decode(&v); err != nil { + writeJSONResp[Out](w, http.StatusBadRequest, nil, []ErrorResp{ + { + Status: http.StatusBadRequest, + Msg: fmt.Sprintf("failed to parse request: %v", err), + }, + }) + + return + } + + res, errs := fn(r.Context(), v) + writeJSONResp(w, successCode, res, errs) + } +} + +func writeJSONResp[T any](w http.ResponseWriter, successCode int, res []T, errs []ErrorResp) { + body := JSONResp[T]{ + Resources: res, + Errs: errs, + } + + status := successCode + for _, e := range errs { + if e.Status > status { + status = e.Status + } + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(body) +} +``` +And we've standardised all `POST` requests! + +This function can be used by all `POST` requests, ensuring they adhere to the spec. It also removes the repetitive code around marshalling and unmarshalling to `JSON` and handles errors in a consistent manner. +The handler functions accept a `context` param and their expected struct input. +``` +func (s *Server) Register(mux *http.ServeMux) { +... + mux.HandleFunc("POST /", Post(201, s.Post)) +} + +func (s *Server) Post(ctx context.Context, req ContentPostReq) ([]Content, []ErrorResp) { + id := len(s.data) + content := Content{ + ID: id, + Foo: req.Foo, + } + s.data[id] = content + + return []Content{content}, nil +} +``` +As you can see, the post function is fairly cleaner now. + +You can extend this to all the other request types. If you have query or path parameters, you could either pass in the request, write a custom struct tag parser, or find someone else who has already done it: [https://github.com/gorilla/schema](https://github.com/gorilla/schema). diff --git a/embed.go b/embed.go index 006db50..e3e27c2 100644 --- a/embed.go +++ b/embed.go @@ -6,5 +6,7 @@ import "embed" //go:embed public/static/*.woff2 //go:embed templates/*.html //go:embed templates/layouts/*.html -//go:embed posts/*.md +//go:embed content/*.md +//go:embed content/posts/*.md + var Content embed.FS diff --git a/go.mod b/go.mod index cbefc2e..3cccf13 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module git.sr.ht/~a73x/home go 1.22.5 require ( - github.com/yuin/goldmark v1.7.4 - go.abhg.dev/goldmark/frontmatter v0.2.0 + github.com/adrg/frontmatter v0.2.0 + github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6 go.uber.org/zap v1.27.0 ) @@ -12,5 +12,5 @@ require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/stretchr/testify v1.9.0 // indirect go.uber.org/multierr v1.10.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index 18c7fcf..fb52c7c 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,16 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/adrg/frontmatter v0.2.0 h1:/DgnNe82o03riBd1S+ZDjd43wAmC6W35q67NHeLkPd4= +github.com/adrg/frontmatter v0.2.0/go.mod h1:93rQCj3z3ZlwyxxpQioRKC1wDLto4aXHrbqIsnH9wmE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6 h1:ZPy+2XJ8u0bB3sNFi+I72gMEMS7MTg7aZCCXPOjV8iw= +github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= -github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= -go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= @@ -18,5 +19,7 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 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/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/html/html.go b/html/html.go new file mode 100644 index 0000000..fe1bbee --- /dev/null +++ b/html/html.go @@ -0,0 +1,19 @@ +package html + +import ( + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" +) + +func MDToHTML(md []byte) []byte { + extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock + p := parser.NewWithExtensions(extensions) + doc := p.Parse(md) + + htmlFlags := html.CommonFlags + opts := html.RendererOptions{Flags: htmlFlags} + renderer := html.NewRenderer(opts) + + return markdown.Render(doc, renderer) +} diff --git a/markdown/markdown.go b/markdown/markdown.go new file mode 100644 index 0000000..0924208 --- /dev/null +++ b/markdown/markdown.go @@ -0,0 +1,76 @@ +package markdown + +import ( + "bytes" + "fmt" + "io/fs" + "path/filepath" + "strings" + + "git.sr.ht/~a73x/home" + "github.com/adrg/frontmatter" +) + +type Content struct { + Meta map[string]any + Body string + Path string +} + +func ParseContents() ([]Content, error) { + contentFS, err := fs.Sub(home.Content, "content") + if err != nil { + return nil, fmt.Errorf("no content found: %v", err) + } + contentFiles, err := glob(contentFS, ".", ".md") + if err != nil { + return nil, fmt.Errorf("failed to glob: %v", err) + } + + res := make([]Content, 0, len(contentFiles)) + for _, contentFile := range contentFiles { + c, err := parseMarkdownFile(contentFS, contentFile) + if err != nil { + return nil, fmt.Errorf("failed to read markdown file: %v", err) + } + + res = append(res, c) + } + + return res, nil +} + +func glob(embedded fs.FS, dir string, ext string) ([]string, error) { + files := []string{} + err := fs.WalkDir(embedded, dir, func(path string, d fs.DirEntry, err error) error { + if filepath.Ext(path) == ext { + files = append(files, path) + } + return nil + }) + + return files, err +} + +func parseMarkdownFile(embedded fs.FS, path string) (Content, error) { + input, err := fs.ReadFile(embedded, path) + if err != nil { + return Content{}, fmt.Errorf("failed to read post: %v", err) + } + + matter := map[string]any{} + matter["template"] = "default" + rest, err := frontmatter.Parse(bytes.NewBuffer(input), &matter) + if err != nil { + return Content{}, fmt.Errorf("failed to parse frontmatter: %v", err) + } + path = strings.Replace(path, ".md", "", 1) + + mc := Content{ + Body: string(rest), + Path: path, + Meta: matter, + } + + return mc, nil +} diff --git a/pages/pages.go b/pages/pages.go new file mode 100644 index 0000000..fbd179a --- /dev/null +++ b/pages/pages.go @@ -0,0 +1,107 @@ +package pages + +import ( + "bytes" + "fmt" + "text/template" + + "git.sr.ht/~a73x/home" + "git.sr.ht/~a73x/home/html" + "git.sr.ht/~a73x/home/markdown" +) + +type GlobalState struct { + Collections map[string][]markdown.Content +} + +type ParserPair struct { + GlobalState + markdown.Content +} + +func renderTemplate(config GlobalState, content markdown.Content) (string, error) { + tmpl := content.Meta["template"] + chosenTemplate := fmt.Sprintf("templates/%s.html", tmpl) + t, err := template.ParseFS(home.Content, chosenTemplate, "templates/layouts/*.html") + if err != nil { + return "", fmt.Errorf("failed to parse layouts: %v", err) + } + + contentParser, err := template.New("content").Parse(string(content.Body)) + if err != nil { + return "", fmt.Errorf("failed parsing content: %v", err) + } + + data := ParserPair{ + config, + content, + } + + newContent := &bytes.Buffer{} + if err := contentParser.Execute(newContent, data); err != nil { + return "", fmt.Errorf("failed to execute content template: %v", err) + } + + data.Body = string(html.MDToHTML(newContent.Bytes())) + + b := &bytes.Buffer{} + if err := t.Execute(b, data); err != nil { + return "", err + } + + return b.String(), nil +} + +type Page struct { + Path string + Content string +} + +func Collect() ([]Page, error) { + contents, err := markdown.ParseContents() + if err != nil { + return nil, err + } + + gs := GlobalState{ + Collections: map[string][]markdown.Content{ + "all": contents, + }, + } + + for _, content := range contents { + tags, ok := content.Meta["tags"] + if !ok { + continue + } + + switch tags := tags.(type) { + case string: + gs.Collections[tags] = append(gs.Collections[tags], content) + case []string: + for _, tag := range tags { + gs.Collections[tag] = append(gs.Collections[tag], content) + } + } + } + + pages := []Page{} + for _, content := range contents { + page, err := renderTemplate(gs, content) + if err != nil { + return nil, fmt.Errorf("failed to build site: %v", err) + } + + path := content.Path + if path == "index" { + path = "" + } + + pages = append(pages, Page{ + Path: path, + Content: page, + }) + } + + return pages, nil +} diff --git a/posts/001.md b/posts/001.md deleted file mode 100644 index 71ece5b..0000000 --- a/posts/001.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: "Go Benchmarking" ---- -1. write a benchmark -2. run a benchmark -3. get a profile -4. optimise -5. run your tests -6. goto 2. -## cpuprofile -`go test -test=XXX -bench -cpuprofile ` - -## memprofile -`go test -test=XXX -bench -memprofile -benchmem` - -## pprof -[pprof usage](https://github.com/google/pprof/blob/main/doc/README.md) - -`go pprof -http=:8080 profile.pb.gz` -will show a web UI for analysing the profile. - -### views: -- flame graph: `localhost:8080/ui/flamegraph` - - shows percentage breakdown of how much resource each "call" made. - - clicking a box will make it "100%" allowing for deep diving - - right click "show source code" to view -- top: `localhost:8080/ui/top` - - shows top functions - - `flat`: profile samples in this function - - `cum`: (cumulative) profile samples in this function and its callees -- source: `localhost:8080/ui/source` - - each source line is annotated with the time spent in that source line - - the first number does not count time spent in functions called from the source line - - the second number does \ No newline at end of file diff --git a/posts/002.md b/posts/002.md deleted file mode 100644 index 57ac355..0000000 --- a/posts/002.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: Go Project Layouts ---- - -Do you lay awake at night and consider how to optimally layout your Go project? -No...? what about recommending Windows to a friend or colleague?? -yeah me either... - -I've seen a lot online that shows what I can only describe as endgame enterprise Go project layouts. These layouts are heavily folder-based and only make sense when your project has grown large enough to warrant the verbosity these folders provide. My only problem is that people often try to start there. - -A lot of design tells you to think about your project in layers. -- api -- domain -- storage - -If you read [The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) -you get told the layers should be, - -1. entities -2. use cases -3. interface adapters -4. frameworks and drivers. - -and that all dependencies should point in (yeah I know, I didn't do a circle so "in" doesn't make sense but I'm sure you can follow). - -This is an excellent idea; separation of concerns is good. - -So you make your folders. -``` -. -├── drivers -├── entities -├── interfaces -└── usecases -``` -aaand this is an awful idea. I don't even want to go further into this hypothetical layout because it hurts too much. - -Find me a project that actually creates these folders, and I'll find you the medium article titled "Clean Code in Go" which spawned it. - -The important parts of clean code, are the ideas presented, and how you apply them to a package orientated language. Creating a folder to represent each layer, doesn't really carry much weight here. - -As a package orientated language, we want to think and reason about things in terms of packages. Yes there will be a point where you may want to group your packages into some group, but that is mostly ceremonial. -Go doesn't care if you're accessing `domain/bar` or `domain/foo/bar`. Either will simply be accessed as `bar`. This means that what matters what's in that package `bar`. Since everything will be read as `bar.Thing` i.e `import bytes` and `bytes.Buffer`. - -So, the package name sets context and expectations. If I grab the `json` package, I expect that package to do things around `json`. I'd feel a bit confused if I was able to configure an smtp server. - -If you cannot come up with a package name that’s a meaningful prefix for the package’s contents, the package abstraction boundary may be wrong - -"but you've still not provided a good example?" -well -yes - -I think the project should grow organically to some degree. What we want to do is write code, and refactoring in Go is fairly cheap. - -Start with a `main.go` and make a `Run` function or some equivalent which it calls. - -```go - -func Run() error { - // actual important stuff here -} -func main() { - if err := Run(); err != nil { - log.Fatal(err) - } -} -``` - -This allows you to test your run function in a unit test, and keeps your `main` func minimal. - -As your project grows, you can keep it flat inside the root directory - -``` -├── api.go -├── go.mod -├── go.sum -├── main.go -├── rss.go -└── sqlite.go -``` -Even just glancing at that, you can guess that this might be an RSS server, that uses sqlite to back it. - -Who knows what -``` -├── drivers -├── entities -├── interfaces -└── usecases -``` - -does. - -As things evolve you might want to put them in `internal` to hide them from being imported by other packages, or `cmd` as you develop multiple binaries. Placing things in `internal` means you're free to mess around with it, without breaking any public contracts other users rely on. -I can't be bothered rewriting my example, so here's a random one I found online; it's probably all right. -[Server Project](https://go.dev/doc/modules/layout#server-project) - -``` -project-root-directory/ - go.mod - internal/ - auth/ - ... - metrics/ - ... - model/ - ... - cmd/ - api-server/ - main.go - metrics-analyzer/ - main.go - ... - ... the project's other directories with non-Go code -``` - -My vague summary is that clean code gives you a north star to follow, an idea of how you want to separate and reason about the packages you create. You don't need to create the entities of abstraction that are also presented. Think about what things do or relate to and create packages for them. You should allow your project to grow organically but don't expect architecture to appear without following a north star. \ No newline at end of file diff --git a/posts/003.md b/posts/003.md deleted file mode 100644 index 690b215..0000000 --- a/posts/003.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: "Levels of Optimisation" ---- -This probably isn't strictly true, but it makes sense to me. -We've got three levels of "optimisation" (assuming your actual design doesn't suck and needs optimising). - -## Benchmark Optimisation -To begin with, we have benchmark optimisation; you create a benchmark locally, dump a profile of it, and optimise it. Then, you run your tests because the most optimal solution is "return nil" and make sure you didn't break your tests. -This is the first and easiest optimisation because it only requires a function, nothing else, and can be done in isolation. You don't need a working "application" here, just the function you're trying to benchmark. There are different types of benchmarks, micro, macro, etc., but I'm leaving them out of scope for this conversation. Go read [Efficient Go](https://learning.oreilly.com/library/view/efficient-go/9781098105709/). - -## Profile guided optimisation -This is a mild step up from benchmark optimisation only because you need a live server load from which you use to pull a profile, but it is probably the most hands-off step. You import the `net/http/pprof` package into your service, call the `debug/profile?seconds=30` to get a profile, and compile your binary with `go build -pgo=profile.pgo`. The compiler will make optimisations for you, and even if your profile is garbage, it shouldn't cause any regressions. - -You probably want to get a few profiles and merge them using `go tool pprof -proto a.out b.out > merged`. This will help provide optimisations that are more relevant to your overall system; instead of just a single 30s slice. -Also, if you have long-running calls that are chained together, a 30-second snapshot might not be enough, so try a sample with a longer window. - -## Runtime optimisation -This is where you expose `/runtime/metrics` and monitor them continuously. There's a list of metrics that you might be interested in, a recommended set of metrics, and generally, you are looking to optimise your interactions with the go runtime. There are a few stats here: goroutine counts, goroutines waiting to run, heap size, how often garbage collection runs, how long garbage collection takes, etc. All useful information to use when optimising - when garbage collection is running, your program ain't. It's also useful for finding memory leaks; it becomes pretty obvious you are leaking goroutines when you graph the count and just watch it go up and never down. -It's also just lowkey fun to look at the exposed data and understand what your system is doing. \ No newline at end of file diff --git a/posts/004.md b/posts/004.md deleted file mode 100644 index 3ce6a82..0000000 --- a/posts/004.md +++ /dev/null @@ -1,250 +0,0 @@ ---- -title: "Writing HTTP Handlers" ---- - -I'm sharing how I write handlers in Go. - -I write them like this for reasons that are probably fairly contextual. I've written a few applications and had to swap REST libraries or even swapped REST for GRPC, so things that make that easier speak to me a great deal. - -I've used `ints` instead of the `http.StatusXXXX` and omitted `JSON` tags in an attempt to try save up screen space. - -To begin with, you might have something like this: -``` -package main - -import ( - "fmt" - "log" - "net/http" -) - -func handler(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello, World!") -} - -func main() { - http.HandleFunc("/", handler) - log.Fatal(http.ListenAndServe(":8080", nil)) -} - -``` - -Then you might get told off because you've just registered routes with the default mux, which isn't very testable. - -So you tweak it a little bit. -``` -package main - -import ( - "fmt" - "log" - "net/http" -) - -func handler(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello, World!") -} - -func newMux() *http.ServeMux { - mux := http.NewServeMux() - mux.HandleFunc("/", handler) - - return mux -} - -func Run() error { - mux := newMux() - return http.ListenAndServe(":8080", mux) -} - -func main() { - if err := Run(); err != nil { - log.Fatal(err) - } -} -``` - -`newMux()` gives you a `mux` to use when testing. - -`Run` keeps `main` nice and clean, so you can just return errors as needed instead of going `log.Fatal` and just generally being messy. - -But now you need to do something real, you want to store and fetch data. - -``` -package main - -import ( - "encoding/json" - "fmt" - "log" - "net/http" - "strconv" -) - -func NewMux() *http.ServeMux { - mux := http.NewServeMux() - s := Server{ - data: make(map[int]Content), - } - - s.Register(mux) - return mux -} - -func Run() error { - mux := NewMux() - return http.ListenAndServe(":8080", mux) -} - -type Server struct { - data map[int]Content -} - -func (s *Server) Register(mux *http.ServeMux) { - mux.HandleFunc("GET /{id}", s.Get) - mux.HandleFunc("POST /", s.Post) -} - -func (s *Server) Get(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") - id, err := strconv.Atoi(idStr) - if err != nil { - w.WriteHeader(400) - w.Write([]byte(fmt.Sprintf("failed to parse id: %v", err))) - return - } - data, ok := s.data[id] - if !ok { - w.WriteHeader(404) - w.Write([]byte("not found")) - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - json.NewEncoder(w).Encode(data) -} - -type ContentPostReq struct { - Foo string -} - -func (s *Server) Post(w http.ResponseWriter, r *http.Request) { - req := ContentPostReq{} - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(400) - w.Write([]byte(fmt.Sprintf("failed to parse request: %v", err))) - return - } - id := len(s.data) - content := Content{ - ID: id, - Foo: req.Foo, - } - s.data[id] = content - - w.WriteHeader(200) - json.NewEncoder(w).Encode(content) -} - -type Content struct { - ID int - Foo string -} - -func main() { - if err := Run(); err != nil { - log.Fatal(err) - } -} -``` - -``` -❯ curl -X POST localhost:8080 --header "Content-Type: application/json" -d '{"foo":"bar"}' -{"ID":0,"Foo":"bar"} -❯ curl -X GET localhost:8080/0 -{"ID":0,"Foo":"bar"} -``` - -Erm, well, okay. Quite a bit has changed here, but I'm sure you can read it. We now save and fetch very, very safely from a map and return the response as `JSON`. I've done some things for brevity because I want to get to the main point. - -This API is inconsistent. It sometimes returns `JSON`, and the others return strings. Overall, it's just a mess. - -So let's try to standardise things. -First, let's design some form of REST spec. - -``` -type JSONResp[T any] struct { - Resources []T - Errs []ErrorResp -} - -type ErrorResp struct { - Status int - Msg string -} -``` -We want to be able to support fetching multiple resources at once, if we can only fetch some resources, let's return them under `resources` and show the errors under `errs` - -Now, add some helpful functions to handle things. -``` -func Post[In any, Out any](successCode int, fn func(context.Context, In) ([]Out, []ErrorResp)) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - var v In - - if err := json.NewDecoder(r.Body).Decode(&v); err != nil { - writeJSONResp[Out](w, http.StatusBadRequest, nil, []ErrorResp{ - { - Status: http.StatusBadRequest, - Msg: fmt.Sprintf("failed to parse request: %v", err), - }, - }) - - return - } - - res, errs := fn(r.Context(), v) - writeJSONResp(w, successCode, res, errs) - } -} - -func writeJSONResp[T any](w http.ResponseWriter, successCode int, res []T, errs []ErrorResp) { - body := JSONResp[T]{ - Resources: res, - Errs: errs, - } - - status := successCode - for _, e := range errs { - if e.Status > status { - status = e.Status - } - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - json.NewEncoder(w).Encode(body) -} -``` -And we've standardised all `POST` requests! - -This function can be used by all `POST` requests, ensuring they adhere to the spec. It also removes the repetitive code around marshalling and unmarshalling to `JSON` and handles errors in a consistent manner. -The handler functions accept a `context` param and their expected struct input. -``` -func (s *Server) Register(mux *http.ServeMux) { -... - mux.HandleFunc("POST /", Post(201, s.Post)) -} - -func (s *Server) Post(ctx context.Context, req ContentPostReq) ([]Content, []ErrorResp) { - id := len(s.data) - content := Content{ - ID: id, - Foo: req.Foo, - } - s.data[id] = content - - return []Content{content}, nil -} -``` -As you can see, the post function is fairly cleaner now. - -You can extend this to all the other request types. If you have query or path parameters, you could either pass in the request, write a custom struct tag parser, or find someone else who has already done it: [https://github.com/gorilla/schema](https://github.com/gorilla/schema). diff --git a/proxy/Dockerfile b/proxy/Dockerfile deleted file mode 100644 index 012092d..0000000 --- a/proxy/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM nginx:1.27.0-alpine - -RUN apk update && apk add ca-certificates iptables ip6tables && rm -rf /var/cache/apk/* - -COPY --from=docker.io/tailscale/tailscale:stable /usr/local/bin/tailscaled /app/tailscaled -COPY --from=docker.io/tailscale/tailscale:stable /usr/local/bin/tailscale /app/tailscale -RUN mkdir -p /var/run/tailscale /var/cache/tailscale /var/lib/tailscale - -WORKDIR /app/ - -COPY nginx.conf /etc/nginx/conf.d/nginx.conf -COPY /start.sh /app/start.sh - -RUN chmod +x /app/start.sh - -CMD ["/app/start.sh"] diff --git a/proxy/fly.toml b/proxy/fly.toml deleted file mode 100644 index 97bf7b5..0000000 --- a/proxy/fly.toml +++ /dev/null @@ -1,22 +0,0 @@ -# fly.toml app configuration file generated for home-divine-bush-1861 on 2024-08-11T17:34:11+01:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# - -app = 'home-divine-bush-1861' -primary_region = 'lhr' - -[build] - -[http_service] - internal_port = 8080 - force_https = true - auto_stop_machines = 'stop' - auto_start_machines = true - min_machines_running = 0 - processes = ['app'] - -[[vm]] - memory = '1gb' - cpu_kind = 'shared' - cpus = 1 diff --git a/proxy/nginx.conf b/proxy/nginx.conf deleted file mode 100644 index 4095372..0000000 --- a/proxy/nginx.conf +++ /dev/null @@ -1,14 +0,0 @@ -server { - listen 8080; - listen [::]:8080; - - server_name a73x.sh; - - - location / { - proxy_ssl_session_reuse off; - proxy_ssl_server_name on; - proxy_pass https://home.folk-amberjack.ts.net/; - proxy_set_header X-Forwarded-Host $http_host; - } -} diff --git a/proxy/start.sh b/proxy/start.sh deleted file mode 100755 index c2c73d6..0000000 --- a/proxy/start.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -/app/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/var/run/tailscale/tailscaled.sock & -/app/tailscale up --authkey=${TAILSCALE_AUTHKEY} --hostname=fly-app - -nginx -g "daemon off;" diff --git a/templates/default.html b/templates/default.html new file mode 100644 index 0000000..3d68d2c --- /dev/null +++ b/templates/default.html @@ -0,0 +1,5 @@ +{{template "base" .}} +{{define "content"}} +

{{.Meta.title}}

+{{ .Body }} +{{end}} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index e84a09e..0000000 --- a/templates/index.html +++ /dev/null @@ -1,54 +0,0 @@ -{{template "base" .}} -{{define "content"}} -
    -
  • backend cloud software engineer
  • -
  • lang: go
  • -
  • infra: kubernetes
  • -
-

tidbits

-

#go

-
    -
  • layout packages by what they do, not by their abstract type
  • -
  • use channels sparingly. write synchronous methods and allow the caller to make it async
  • -
  • append modifies the underlying slice, you'll only make this mistake once
  • -
  • define interfaces where you use them
  • -
  • make([]int, 5) has a length and capacity of 5. ([]int, 0,5) has a length - of 0 and capacity of 5. -
    - append() will only do what you want with the latter -
  • -
  • don't use init()
  • -
  • TFBO (test, fix, benchmark, optimise)
  • -
  • more CPU != more performance -
    - more CPU == more Contention -
  • -
-

#git

-
    -
  • git reflog
  • -
  • git commit --fixup=<COMMITISH> -
    - git rebase origin/main --autosquash -
  • -
- -

resources

- -

books

- -{{end}} diff --git a/templates/layouts/_default.html b/templates/layouts/_default.html new file mode 100644 index 0000000..beab01e --- /dev/null +++ b/templates/layouts/_default.html @@ -0,0 +1,75 @@ +{{define "base"}} + + + + + + + + + a73x + + + + +

a73x

+ high effort, low reward + + {{ template "content" . }} +
+

​​​​​​​​​​​​​​​​​​​
​​​​​ +

see something you disagree with? Email: yourewrong@a73x.sh

+
+ + + +{{end}} \ No newline at end of file diff --git a/templates/layouts/default.html b/templates/layouts/default.html deleted file mode 100644 index 14868cf..0000000 --- a/templates/layouts/default.html +++ /dev/null @@ -1,75 +0,0 @@ -{{define "base"}} - - - - - - - - - a73x - - - - -

a73x

- high effort, low reward - - {{ template "content" . }} -
-

​​​​​​​​​​​​​​​​​​​
​​​​​ -

see something you disagree with? Email: yourewrong@a73x.sh

-
- - - -{{end}} \ No newline at end of file diff --git a/templates/post.html b/templates/post.html deleted file mode 100644 index 0a6cc6e..0000000 --- a/templates/post.html +++ /dev/null @@ -1,6 +0,0 @@ -{{template "base" .}} -{{define "content"}} -

{{.Title}}

-{{ .Post }} -{{end}} - diff --git a/templates/posts.html b/templates/posts.html deleted file mode 100644 index c68fb73..0000000 --- a/templates/posts.html +++ /dev/null @@ -1,9 +0,0 @@ -{{template "base" .}} -{{define "content"}} -

posts

- -{{end}} diff --git a/web/web.go b/web/web.go index 487dd81..e421d3c 100644 --- a/web/web.go +++ b/web/web.go @@ -1,138 +1,16 @@ package web import ( - "bytes" "fmt" - "io" "io/fs" "net/http" - "path/filepath" - "strings" - "text/template" "time" "git.sr.ht/~a73x/home" - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/parser" - "go.abhg.dev/goldmark/frontmatter" + "git.sr.ht/~a73x/home/pages" "go.uber.org/zap" ) -type CommonData struct { - Posts []PostData -} - -type PostData struct { - Title string - Path string -} - -func GeneratePosts(mux *http.ServeMux) ([]PostData, error) { - converter := goldmark.New( - goldmark.WithExtensions( - &frontmatter.Extender{}, - )) - - t, err := template.ParseFS(home.Content, "templates/layouts/*.html") - if err != nil { - return nil, fmt.Errorf("failed to parse layouts: %v", err) - } - - final := []PostData{} - posts, err := fs.Glob(home.Content, "posts/*.md") - if err != nil { - return nil, fmt.Errorf("failed to glob posts: %v", err) - } - - for _, post := range posts { - postName := filepath.Base(post) - postName = strings.Split(postName, ".")[0] + ".html" - - // parse markdown - input, err := fs.ReadFile(home.Content, post) - if err != nil { - return nil, fmt.Errorf("failed to read post: %v", err) - } - - var b bytes.Buffer - ctx := parser.NewContext() - if err := converter.Convert(input, &b, parser.WithContext(ctx)); err != nil { - return nil, err - } - - d := frontmatter.Get(ctx) - var meta map[string]string - if err := d.Decode(&meta); err != nil { - return nil, err - } - - foo, err := t.ParseFS(home.Content, "templates/post.html") - if err != nil { - return nil, fmt.Errorf("failed to parse post template: %v", err) - } - - content, err := io.ReadAll(&b) - if err != nil { - return nil, fmt.Errorf("failed to read post template: %v", err) - } - - type IPostData struct { - Title string - Post string - } - - mux.HandleFunc("/posts/"+postName, func(w http.ResponseWriter, r *http.Request) { - if err := foo.ExecuteTemplate(w, "post.html", IPostData{ - Title: meta["title"], - Post: string(content), - }); err != nil { - fmt.Println(err) - } - }) - - final = append(final, PostData{ - Title: meta["title"], - Path: "posts/" + postName, - }) - } - - return final, nil -} - -func loadTemplates(mux *http.ServeMux, data any) error { - tmplFiles, err := fs.ReadDir(home.Content, "templates") - if err != nil { - return fmt.Errorf("failed to parse template layouts") - } - - for _, tmpl := range tmplFiles { - if tmpl.IsDir() { - continue - } - - pt, err := template.ParseFS(home.Content, "templates/"+tmpl.Name(), "templates/layouts/*.html") - if err != nil { - return fmt.Errorf("failed to parse template "+tmpl.Name(), err) - } - - if tmpl.Name() == "index.html" { - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - pt.ExecuteTemplate(w, tmpl.Name(), data) - }) - } else if tmpl.Name() == "page.html" { - continue - } else { - mux.HandleFunc("/"+tmpl.Name(), func(w http.ResponseWriter, r *http.Request) { - pt, err := template.ParseFS(home.Content, "templates/"+tmpl.Name(), "templates/layouts/*.html") - if err != nil { - fmt.Printf("failed to parse template "+tmpl.Name(), err) - } - pt.ExecuteTemplate(w, tmpl.Name(), data) - }) - } - } - return nil -} func New(logger *zap.Logger) (*http.Server, error) { loggingMiddleware := func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -148,17 +26,15 @@ func New(logger *zap.Logger) (*http.Server, error) { } mux := http.NewServeMux() - postData, err := GeneratePosts(mux) + pages, err := pages.Collect() if err != nil { - return nil, fmt.Errorf("failed to generate posts: %v", err) - } - - data := CommonData{ - Posts: postData, + return nil, err } - if err := loadTemplates(mux, data); err != nil { - return nil, fmt.Errorf("failed to parse templates: %v", err) + for _, page := range pages { + mux.HandleFunc("GET /"+page.Path, func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(page.Content)) + }) } staticFs, err := fs.Sub(home.Content, "public/static") -- cgit v1.2.3