From f9c5ccc378b0a9bdba53a134fe885ad662a306f4 Mon Sep 17 00:00:00 2001 From: luxick Date: Wed, 15 Oct 2025 09:32:32 +0200 Subject: [PATCH] Add templating --- .github/go.instructions.md | 373 +++++++++++++++++++++++++++++++ internal/server/server.go | 27 ++- internal/server/template_data.go | 86 +++++++ web/embed.go | 2 +- web/templates/admin.html | 28 +++ web/templates/index.html | 96 ++++---- web/templates/layout.html | 19 ++ web/templates/menubar.html | 25 +++ 8 files changed, 596 insertions(+), 60 deletions(-) create mode 100644 .github/go.instructions.md create mode 100644 internal/server/template_data.go create mode 100644 web/templates/admin.html create mode 100644 web/templates/layout.html create mode 100644 web/templates/menubar.html diff --git a/.github/go.instructions.md b/.github/go.instructions.md new file mode 100644 index 0000000..a956d62 --- /dev/null +++ b/.github/go.instructions.md @@ -0,0 +1,373 @@ +--- +description: 'Instructions for writing Go code following idiomatic Go practices and community standards' +applyTo: '**/*.go,**/go.mod,**/go.sum' +--- + +# Go Development Instructions + +Follow idiomatic Go practices and community standards when writing Go code. These instructions are based on [Effective Go](https://go.dev/doc/effective_go), [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments), and [Google's Go Style Guide](https://google.github.io/styleguide/go/). + +## General Instructions + +- Write simple, clear, and idiomatic Go code +- Favor clarity and simplicity over cleverness +- Follow the principle of least surprise +- Keep the happy path left-aligned (minimize indentation) +- Return early to reduce nesting +- Prefer early return over if-else chains; use `if condition { return }` pattern to avoid else blocks +- Make the zero value useful +- Write self-documenting code with clear, descriptive names +- Document exported types, functions, methods, and packages +- Use Go modules for dependency management +- Leverage the Go standard library instead of reinventing the wheel (e.g., use `strings.Builder` for string concatenation, `filepath.Join` for path construction) +- Prefer standard library solutions over custom implementations when functionality exists +- Write comments in English by default; translate only upon user request +- Avoid using emoji in code and comments + +## Naming Conventions + +### Packages + +- Use lowercase, single-word package names +- Avoid underscores, hyphens, or mixedCaps +- Choose names that describe what the package provides, not what it contains +- Avoid generic names like `util`, `common`, or `base` +- Package names should be singular, not plural + +#### Package Declaration Rules (CRITICAL): +- **NEVER duplicate `package` declarations** - each Go file must have exactly ONE `package` line +- When editing an existing `.go` file: + - **PRESERVE** the existing `package` declaration - do not add another one + - If you need to replace the entire file content, start with the existing package name +- When creating a new `.go` file: + - **BEFORE writing any code**, check what package name other `.go` files in the same directory use + - Use the SAME package name as existing files in that directory + - If it's a new directory, use the directory name as the package name + - Write **exactly one** `package ` line at the very top of the file +- When using file creation or replacement tools: + - **ALWAYS verify** the target file doesn't already have a `package` declaration before adding one + - If replacing file content, include only ONE `package` declaration in the new content + - **NEVER** create files with multiple `package` lines or duplicate declarations + +### Variables and Functions + +- Use mixedCaps or MixedCaps (camelCase) rather than underscores +- Keep names short but descriptive +- Use single-letter variables only for very short scopes (like loop indices) +- Exported names start with a capital letter +- Unexported names start with a lowercase letter +- Avoid stuttering (e.g., avoid `http.HTTPServer`, prefer `http.Server`) + +### Interfaces + +- Name interfaces with -er suffix when possible (e.g., `Reader`, `Writer`, `Formatter`) +- Single-method interfaces should be named after the method (e.g., `Read` → `Reader`) +- Keep interfaces small and focused + +### Constants + +- Use MixedCaps for exported constants +- Use mixedCaps for unexported constants +- Group related constants using `const` blocks +- Consider using typed constants for better type safety + +## Code Style and Formatting + +### Formatting + +- Always use `gofmt` to format code +- Use `goimports` to manage imports automatically +- Keep line length reasonable (no hard limit, but consider readability) +- Add blank lines to separate logical groups of code + +### Comments + +- Strive for self-documenting code; prefer clear variable names, function names, and code structure over comments +- Write comments only when necessary to explain complex logic, business rules, or non-obvious behavior +- Write comments in complete sentences in English by default +- Translate comments to other languages only upon specific user request +- Start sentences with the name of the thing being described +- Package comments should start with "Package [name]" +- Use line comments (`//`) for most comments +- Use block comments (`/* */`) sparingly, mainly for package documentation +- Document why, not what, unless the what is complex +- Avoid emoji in comments and code + +### Error Handling + +- Check errors immediately after the function call +- Don't ignore errors using `_` unless you have a good reason (document why) +- Wrap errors with context using `fmt.Errorf` with `%w` verb +- Create custom error types when you need to check for specific errors +- Place error returns as the last return value +- Name error variables `err` +- Keep error messages lowercase and don't end with punctuation + +## Architecture and Project Structure + +### Package Organization + +- Follow standard Go project layout conventions +- Keep `main` packages in `cmd/` directory +- Put reusable packages in `pkg/` or `internal/` +- Use `internal/` for packages that shouldn't be imported by external projects +- Group related functionality into packages +- Avoid circular dependencies + +### Dependency Management + +- Use Go modules (`go.mod` and `go.sum`) +- Keep dependencies minimal +- Regularly update dependencies for security patches +- Use `go mod tidy` to clean up unused dependencies +- Vendor dependencies only when necessary + +## Type Safety and Language Features + +### Type Definitions + +- Define types to add meaning and type safety +- Use struct tags for JSON, XML, database mappings +- Prefer explicit type conversions +- Use type assertions carefully and check the second return value +- Prefer generics over unconstrained types; when an unconstrained type is truly needed, use the predeclared alias `any` instead of `interface{}` (Go 1.18+) + +### Pointers vs Values + +- Use pointer receivers for large structs or when you need to modify the receiver +- Use value receivers for small structs and when immutability is desired +- Use pointer parameters when you need to modify the argument or for large structs +- Use value parameters for small structs and when you want to prevent modification +- Be consistent within a type's method set +- Consider the zero value when choosing pointer vs value receivers + +### Interfaces and Composition + +- Accept interfaces, return concrete types +- Keep interfaces small (1-3 methods is ideal) +- Use embedding for composition +- Define interfaces close to where they're used, not where they're implemented +- Don't export interfaces unless necessary + +## Concurrency + +### Goroutines + +- Be cautious about creating goroutines in libraries; prefer letting the caller control concurrency +- If you must create goroutines in libraries, provide clear documentation and cleanup mechanisms +- Always know how a goroutine will exit +- Use `sync.WaitGroup` or channels to wait for goroutines +- Avoid goroutine leaks by ensuring cleanup + +### Channels + +- Use channels to communicate between goroutines +- Don't communicate by sharing memory; share memory by communicating +- Close channels from the sender side, not the receiver +- Use buffered channels when you know the capacity +- Use `select` for non-blocking operations + +### Synchronization + +- Use `sync.Mutex` for protecting shared state +- Keep critical sections small +- Use `sync.RWMutex` when you have many readers +- Choose between channels and mutexes based on the use case: use channels for communication, mutexes for protecting state +- Use `sync.Once` for one-time initialization +- WaitGroup usage by Go version: + - If `go >= 1.25` in `go.mod`, use the new `WaitGroup.Go` method ([documentation](https://pkg.go.dev/sync#WaitGroup)): + ```go + var wg sync.WaitGroup + wg.Go(task1) + wg.Go(task2) + wg.Wait() + ``` + - If `go < 1.25`, use the classic `Add`/`Done` pattern + +## Error Handling Patterns + +### Creating Errors + +- Use `errors.New` for simple static errors +- Use `fmt.Errorf` for dynamic errors +- Create custom error types for domain-specific errors +- Export error variables for sentinel errors +- Use `errors.Is` and `errors.As` for error checking + +### Error Propagation + +- Add context when propagating errors up the stack +- Don't log and return errors (choose one) +- Handle errors at the appropriate level +- Consider using structured errors for better debugging + +## API Design + +### HTTP Handlers + +- Use `http.HandlerFunc` for simple handlers +- Implement `http.Handler` for handlers that need state +- Use middleware for cross-cutting concerns +- Set appropriate status codes and headers +- Handle errors gracefully and return appropriate error responses +- Router usage by Go version: + - If `go >= 1.22`, prefer the enhanced `net/http` `ServeMux` with pattern-based routing and method matching + - If `go < 1.22`, use the classic `ServeMux` and handle methods/paths manually (or use a third-party router when justified) + +### JSON APIs + +- Use struct tags to control JSON marshaling +- Validate input data +- Use pointers for optional fields +- Consider using `json.RawMessage` for delayed parsing +- Handle JSON errors appropriately + +### HTTP Clients + +- Keep the client struct focused on configuration and dependencies only (e.g., base URL, `*http.Client`, auth, default headers). It must not store per-request state +- Do not store or cache `*http.Request` inside the client struct, and do not persist request-specific state across calls; instead, construct a fresh request per method invocation +- Methods should accept `context.Context` and input parameters, assemble the `*http.Request` locally (or via a short-lived builder/helper created per call), then call `c.httpClient.Do(req)` +- If request-building logic is reused, factor it into unexported helper functions or a per-call builder type; never keep `http.Request` (URL params, body, headers) as fields on the long-lived client +- Ensure the underlying `*http.Client` is configured (timeouts, transport) and is safe for concurrent use; avoid mutating `Transport` after first use +- Always set headers on the request instance you’re sending, and close response bodies (`defer resp.Body.Close()`), handling errors appropriately + +## Performance Optimization + +### Memory Management + +- Minimize allocations in hot paths +- Reuse objects when possible (consider `sync.Pool`) +- Use value receivers for small structs +- Preallocate slices when size is known +- Avoid unnecessary string conversions + +### I/O: Readers and Buffers + +- Most `io.Reader` streams are consumable once; reading advances state. Do not assume a reader can be re-read without special handling +- If you must read data multiple times, buffer it once and recreate readers on demand: + - Use `io.ReadAll` (or a limited read) to obtain `[]byte`, then create fresh readers via `bytes.NewReader(buf)` or `bytes.NewBuffer(buf)` for each reuse + - For strings, use `strings.NewReader(s)`; you can `Seek(0, io.SeekStart)` on `*bytes.Reader` to rewind +- For HTTP requests, do not reuse a consumed `req.Body`. Instead: + - Keep the original payload as `[]byte` and set `req.Body = io.NopCloser(bytes.NewReader(buf))` before each send + - Prefer configuring `req.GetBody` so the transport can recreate the body for redirects/retries: `req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(buf)), nil }` +- To duplicate a stream while reading, use `io.TeeReader` (copy to a buffer while passing through) or write to multiple sinks with `io.MultiWriter` +- Reusing buffered readers: call `(*bufio.Reader).Reset(r)` to attach to a new underlying reader; do not expect it to “rewind” unless the source supports seeking +- For large payloads, avoid unbounded buffering; consider streaming, `io.LimitReader`, or on-disk temporary storage to control memory + +- Use `io.Pipe` to stream without buffering the whole payload: + - Write to `*io.PipeWriter` in a separate goroutine while the reader consumes + - Always close the writer; use `CloseWithError(err)` on failures + - `io.Pipe` is for streaming, not rewinding or making readers reusable + +- **Warning:** When using `io.Pipe` (especially with multipart writers), all writes must be performed in strict, sequential order. Do not write concurrently or out of order—multipart boundaries and chunk order must be preserved. Out-of-order or parallel writes can corrupt the stream and result in errors. + +- Streaming multipart/form-data with `io.Pipe`: + - `pr, pw := io.Pipe()`; `mw := multipart.NewWriter(pw)`; use `pr` as the HTTP request body + - Set `Content-Type` to `mw.FormDataContentType()` + - In a goroutine: write all parts to `mw` in the correct order; on error `pw.CloseWithError(err)`; on success `mw.Close()` then `pw.Close()` + - Do not store request/in-flight form state on a long-lived client; build per call + - Streamed bodies are not rewindable; for retries/redirects, buffer small payloads or provide `GetBody` + +### Profiling + +- Use built-in profiling tools (`pprof`) +- Benchmark critical code paths +- Profile before optimizing +- Focus on algorithmic improvements first +- Consider using `testing.B` for benchmarks + +## Testing + +### Test Organization + +- Keep tests in the same package (white-box testing) +- Use `_test` package suffix for black-box testing +- Name test files with `_test.go` suffix +- Place test files next to the code they test + +### Writing Tests + +- Use table-driven tests for multiple test cases +- Name tests descriptively using `Test_functionName_scenario` +- Use subtests with `t.Run` for better organization +- Test both success and error cases +- Consider using `testify` or similar libraries when they add value, but don't over-complicate simple tests + +### Test Helpers + +- Mark helper functions with `t.Helper()` +- Create test fixtures for complex setup +- Use `testing.TB` interface for functions used in tests and benchmarks +- Clean up resources using `t.Cleanup()` + +## Security Best Practices + +### Input Validation + +- Validate all external input +- Use strong typing to prevent invalid states +- Sanitize data before using in SQL queries +- Be careful with file paths from user input +- Validate and escape data for different contexts (HTML, SQL, shell) + +### Cryptography + +- Use standard library crypto packages +- Don't implement your own cryptography +- Use crypto/rand for random number generation +- Store passwords using bcrypt, scrypt, or argon2 (consider golang.org/x/crypto for additional options) +- Use TLS for network communication + +## Documentation + +### Code Documentation + +- Prioritize self-documenting code through clear naming and structure +- Document all exported symbols with clear, concise explanations +- Start documentation with the symbol name +- Write documentation in English by default +- Use examples in documentation when helpful +- Keep documentation close to code +- Update documentation when code changes +- Avoid emoji in documentation and comments + +### README and Documentation Files + +- Include clear setup instructions +- Document dependencies and requirements +- Provide usage examples +- Document configuration options +- Include troubleshooting section + +## Tools and Development Workflow + +### Essential Tools + +- `go fmt`: Format code +- `go vet`: Find suspicious constructs +- `golangci-lint`: Additional linting (golint is deprecated) +- `go test`: Run tests +- `go mod`: Manage dependencies +- `go generate`: Code generation + +### Development Practices + +- Run tests before committing +- Use pre-commit hooks for formatting and linting +- Keep commits focused and atomic +- Write meaningful commit messages +- Review diffs before committing + +## Common Pitfalls to Avoid + +- Not checking errors +- Ignoring race conditions +- Creating goroutine leaks +- Not using defer for cleanup +- Modifying maps concurrently +- Not understanding nil interfaces vs nil pointers +- Forgetting to close resources (files, connections) +- Using global variables unnecessarily +- Over-using unconstrained types (e.g., `any`); prefer specific types or generic type parameters with constraints. If an unconstrained type is required, use `any` rather than `interface{}` +- Not considering the zero value of types +- **Creating duplicate `package` declarations** - this is a compile error; always check existing files before adding package declarations diff --git a/internal/server/server.go b/internal/server/server.go index 418d4e9..960dcf7 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,6 +3,7 @@ package server import ( "fmt" "html" + "html/template" "io/fs" "log" "net/http" @@ -36,7 +37,7 @@ type Config struct { // Server hosts the Luxtools HTTP handlers. type Server struct { mux *http.ServeMux - indexPage []byte + templates *template.Template staticFS fs.FS mediaFS http.FileSystem @@ -60,9 +61,10 @@ func New(cfg Config) (*Server, error) { return nil, fmt.Errorf("media directory must be a directory: %s", mediaDir) } - indexPage, err := webbundle.Content.ReadFile("templates/index.html") + // Parse all templates + tmpl, err := template.ParseFS(webbundle.Content, "templates/*.html") if err != nil { - return nil, fmt.Errorf("failed to read index template: %w", err) + return nil, fmt.Errorf("failed to parse templates: %w", err) } staticFS, err := fs.Sub(webbundle.Content, "static") @@ -72,7 +74,7 @@ func New(cfg Config) (*Server, error) { s := &Server{ mux: http.NewServeMux(), - indexPage: indexPage, + templates: tmpl, staticFS: staticFS, mediaFS: http.Dir(mediaDir), } @@ -103,7 +105,12 @@ func (s *Server) registerRoutes() { func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": - s.sendHTML(w, http.StatusOK, s.indexPage) + data := DefaultMenuBar() + s.renderTemplate(w, http.StatusOK, "index.html", data) + case "/admin": + data := AdminMenuBar() + data.Title = "Admin Panel" + s.renderTemplate(w, http.StatusOK, "admin.html", data) case "/counter": s.handleCounter(w, r) case "/time": @@ -127,6 +134,16 @@ func (s *Server) handleCounter(w http.ResponseWriter, r *http.Request) { s.sendHTMLString(w, http.StatusOK, renderCounter(count)) } +func (s *Server) renderTemplate(w http.ResponseWriter, status int, name string, data interface{}) { + w.Header().Set("Content-Type", htmlContentType) + w.WriteHeader(status) + + if err := s.templates.ExecuteTemplate(w, name, data); err != nil { + log.Printf("template execution failed: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} + func (s *Server) sendHTML(w http.ResponseWriter, status int, body []byte) { w.Header().Set("Content-Type", htmlContentType) w.WriteHeader(status) diff --git a/internal/server/template_data.go b/internal/server/template_data.go new file mode 100644 index 0000000..02e6e76 --- /dev/null +++ b/internal/server/template_data.go @@ -0,0 +1,86 @@ +package server + +import "html/template" + +// This file defines the data structures and helper functions for the template system. +// +// The template system uses Go's html/template package to provide: +// - Dynamic menu bars that can be customized per page +// - Template inheritance via layout.html +// - Type-safe data passing via structs +// +// To create a new page with a custom menu: +// 1. Create a PageData struct with your menu configuration +// 2. Call s.renderTemplate(w, status, "yourpage.html", data) +// 3. In yourpage.html, use {{template "layout" .}} and define content blocks +// +// See TEMPLATE_GUIDE.md for detailed examples and usage patterns. + +// PageData holds the data passed to page templates. +type PageData struct { + Title string + MenuGroups []MenuGroup + ShowClock bool +} + +// MenuGroup represents a dropdown menu in the navbar. +type MenuGroup struct { + LabelHTML template.HTML + Items []MenuItem +} + +// MenuItem represents a single menu item. +type MenuItem struct { + LabelHTML template.HTML + URL string + IsDivider bool +} + +// DefaultMenuBar returns the standard menu configuration. +func DefaultMenuBar() PageData { + return PageData{ + ShowClock: true, + MenuGroups: []MenuGroup{ + { + LabelHTML: template.HTML(`File`), + Items: []MenuItem{ + {LabelHTML: template.HTML(`New`), URL: "#!"}, + {LabelHTML: template.HTML(`Open`), URL: "#!"}, + {LabelHTML: template.HTML(`Save`), URL: "#!"}, + {LabelHTML: template.HTML(`Save As`), URL: "#!"}, + {IsDivider: true}, + {LabelHTML: template.HTML(`Exit`), URL: "#!"}, + }, + }, + { + LabelHTML: template.HTML(`Edit`), + Items: []MenuItem{ + {LabelHTML: template.HTML(`Cut`), URL: "#!"}, + {LabelHTML: template.HTML(`Copy`), URL: "#!"}, + {LabelHTML: template.HTML(`Paste`), URL: "#!"}, + }, + }, + { + LabelHTML: template.HTML(`Help`), + Items: []MenuItem{ + {LabelHTML: template.HTML(`Documentation`), URL: "#!"}, + {LabelHTML: template.HTML(`About`), URL: "#!"}, + }, + }, + }, + } +} + +// AdminMenuBar returns a menu configuration with admin options. +func AdminMenuBar() PageData { + data := DefaultMenuBar() + data.MenuGroups = append(data.MenuGroups, MenuGroup{ + LabelHTML: template.HTML(`Admin`), + Items: []MenuItem{ + {LabelHTML: template.HTML(`Users`), URL: "/admin/users"}, + {LabelHTML: template.HTML(`Settings`), URL: "/admin/settings"}, + {LabelHTML: template.HTML(`Logs`), URL: "/admin/logs"}, + }, + }) + return data +} diff --git a/web/embed.go b/web/embed.go index 175dc0a..ffc6adf 100644 --- a/web/embed.go +++ b/web/embed.go @@ -4,5 +4,5 @@ import "embed" // Content holds the HTML templates and static assets for the web UI. // -//go:embed templates/index.html static +//go:embed templates/* static var Content embed.FS diff --git a/web/templates/admin.html b/web/templates/admin.html new file mode 100644 index 0000000..112c333 --- /dev/null +++ b/web/templates/admin.html @@ -0,0 +1,28 @@ +{{template "layout" .}} + +{{define "title"}}Admin Panel · Luxtools{{end}} + +{{define "content"}} +
+ Admin Panel +

+ This is an example admin page with a different menu bar that includes admin-specific options. +

+ +
+

System Status

+
+

Server: Running

+

Users: 42 active

+

Uptime: 7 days, 3 hours

+
+
+ +
+

Quick Actions

+ + + +
+
+{{end}} diff --git a/web/templates/index.html b/web/templates/index.html index 97ddc52..4a7cd3c 100755 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1,57 +1,45 @@ - - - - - - Luxtools · Nim + HTMX demo - - - - - -
-
+{{template "layout" .}} + +{{define "title"}}Luxtools · Go + HTMX demo{{end}} + +{{define "content"}} +
Luxtools control panel -

- Luxtools demonstrates a Nim backend compiled into a single binary. The UI - uses TUI.CSS for a retro feel and HTMX for - partial page updates without JavaScript glue code. -

+

+ Luxtools demonstrates a Go backend compiled into a single binary. The UI + uses TUI.CSS for a retro feel and HTMX for + partial page updates without JavaScript glue code. +

-
-

Interactive counter

-

- Click the button to trigger an hx-post request. The response - replaces only the counter panel. -

-
-
Loading…
-
-
+
+

Interactive counter

+

+ Click the button to trigger an hx-post request. The response + replaces only the counter panel. +

+
+
Loading…
+
+
-
-

Server time

-

- A periodic hx-get refresh keeps this panel in sync with the - server clock. -

-
-
Loading…
-
-
-
-
- - +
+

Server time

+

+ A periodic hx-get refresh keeps this panel in sync with the + server clock. +

+
+
Loading…
+
+
+ +{{end}} diff --git a/web/templates/layout.html b/web/templates/layout.html new file mode 100644 index 0000000..3024273 --- /dev/null +++ b/web/templates/layout.html @@ -0,0 +1,19 @@ +{{define "layout"}} + + + + + + {{block "title" .}}Luxtools{{end}} + + + + + + {{block "menubar" .}}{{end}} +
+ {{block "content" .}}{{end}} +
+ + +{{end}} diff --git a/web/templates/menubar.html b/web/templates/menubar.html new file mode 100644 index 0000000..f6edcb4 --- /dev/null +++ b/web/templates/menubar.html @@ -0,0 +1,25 @@ +{{define "menubar"}} + +{{end}}