diff --git a/Dockerfile b/Dockerfile index b69279b..cfddf17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,44 @@ -FROM golang:1.22 AS build +FROM golang:1-alpine AS builder + +RUN apk add --no-cache build-base + +ENV USER=appuser +ENV UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + "${USER}" WORKDIR /build COPY go.mod go.sum ./ RUN go mod download -COPY . ./ - -ENV GOFLAGS='-tags="sqlite_json"' -ENV CGO_ENABLED=1 -ENV GOOS=linux -RUN go build -o xtemplate ./cmd +COPY . . +RUN CGO_ENABLED=1 \ + GOFLAGS='-tags="sqlite_json"' \ + GOOS=linux \ + GOARCH=amd64 \ + go build -ldflags="-w -s" -o /dist/xtemplate ./cmd +RUN ldd /dist/xtemplate | tr -s [:blank:] '\n' | grep ^/ | xargs -I % install -D % /dist/% +RUN ln -s ld-musl-x86_64.so.1 /dist/lib/libc.musl-x86_64.so.1 ### FROM scratch -WORKDIR /app - -COPY --from=build /build/xtemplate /app/xtemplate +COPY --from=builder /etc/passwd /etc/group /etc/ +COPY --from=builder /dist/lib /lib/ +COPY --from=builder /dist/xtemplate /app/xtemplate +WORKDIR /app VOLUME /app/data +USER appuser:appuser EXPOSE 80 ENTRYPOINT ["/app/xtemplate"] -CMD ["-template-root", "/app/templates", "-watch-template", "false", "-log", "0"] +CMD ["-template-path", "/app/templates", "-watch-template", "false", "-log", "0", "-listen", ":80"] diff --git a/README.md b/README.md index 88322cc..b78b38b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,393 @@ # xtemplate +`xtemplate` is a html/template-based hypertext preprocessor and rapid +application development web server written in Go. It streamlines construction of +hypermedia-exchange-oriented web sites by efficiently handling basic server +tasks, enabling authors to focus on defining routes and responding to them using +templates and configurable data sources. + +> [!IMPORTANT] +> +> xtemplate is somewhat unstable + +- [💡 Why?](#-why) +- [🎇 Features](#-features) +- [👨‍🏫 How it works](#-how-it-works) +- [👨‍🏭 How to use](#-how-to-use) + - [📦 Deployment modes](#-deployment-modes) + - [🧰 Template semantics](#-template-semantics) + - [📝 Dynamic Values in dot-context](#-context) + - [📐 Call custom Go functions and pure builtins](#-functions) +- [🏆 Users](#-users) +- [👷‍♀️ Development](#%EF%B8%8F-development) +- [✅ License](#-project-history-and-license) + +## 💡 Why? + +After bulding some sites with [htmx](https://htmx.org) and Go, I wished that +everything would just get out of the way of the absolute fundamentals: + +- URLs and path patterns +- Access to a backing data source +- Executing a template to return HTML + +**The hypothesis of `xtemplate` is that *templates* can be the nexus of these +fundamentals.** + +The Go stdlib `html/template` package is a good starting point, but it requires +some acute customization and a decent surrounding implementation to realize this +vision. + +## 🎇 Features + +*Click a feature to expand and show details:* + +
⚡ Efficient design + +> All template files are read and parsed *once*, at startup, and kept in memory +> during the life of an xtemplate *instance*. Requests are routed to a handler +> that immediately starts executing a template reference in response. No slow +> cascading disk accesses or parsing overhead before you even begin crafting the +> response. +
+ +
🔄 Live reload + +> Template files are loaded into a new instance and validated milliseconds after +> they are modified, no need to restart the server. If an error occurs during +> load the previous instance remains intact and continues to serve while the +> loading error is printed to the logs. A successful reload atomically swaps the +> handler so new requests are served by the new instance; pending requests are +> allowed to complete *gracefully*. +> +> Add this template definition and one-line script to your page, then +> clients will automatically reload when the server does: +> +> ```html +> {{- define "SSE /reload"}}{{.Block}}data: reload{{printf "\n\n"}}{{end}} +> +> +> ``` +
+ +
🗃️ Simple file-based routing + +> `GET` requests are handled by invoking a matching template file at that path. +> (Hidden files that start with `.` are loaded but not routed by default.) +> +> ``` +> File path: HTTP path: +> . +> ├── index.html GET / +> ├── todos.html GET /todos +> ├── admin +> │ └── settings.html GET /admin/settings +> └── shared +> └── .head.html (not routed because it starts with '.') +> ``` +
+ +
🔱 Add custom routes to handle any method and path pattern + +> Handle any [Go 1.22 ServeMux](servemux) pattern by **defining a template with +> that pattern as its name**. Path placeholders are available during template +> execution with the `.Req.PathValue` method. +> +> ```html +> +> {{define "GET /contact/{id}"}} +> {{$contact := .QueryRow `SELECT name,phone FROM contacts WHERE id=?` (.Req.PathValue "id")}} +>
+> Name: {{$contact.name}} +> Phone: {{$contact.phone}} +>
+> {{end}} +> +> +> {{define "DELETE /contact/{id}"}} +> {{$_ := .Exec `DELETE from contacts WHERE id=?` (.Req.PathValue "id")}} +> {{.RespStatus 204}} +> {{end}} +> ``` + +[servemux]: https://tip.golang.org/doc/go1.22#enhanced_routing_patterns + +
+ +
👨‍💻 Define and invoke custom templates + +> All html files under the template root directory are available to invoke by +> their full path relative to the template root dir starting with `/`: +> +> ```html +> +> Home +> +> {{template "/shared/.head.html" .}} +> +> +> +> {{template "navbar" .}} +> ... +> +> +> ``` +
+ +
🛡️ XSS safe by default + +> The html/template library automatically escapes user content, so you can rest +> easy from basic XSS attacks. The defacto standard html sanitizer for Go, +> BlueMonday, is available for cases where you need finer grained control. +> +> If you have some html string that you do trust, it's easy to inject if that's +> your intention with the `trustHtml` func. +
+ +
🎨 Customize the context to provide selected data sources + +> Configure xtemplate to get access to built-in and custom data sources like +> running SQL queries against a database, sending and receiving messages using a +> message streaming client like NATS, read and list files from a local +> directory, reading static config from a key-value store, **or perform any +> action you can define by writing a Go API**, like the common "repository" +> design pattern for example. +> +> Modify `Config` to add built-in or custom `ContextProvider` implementations, +> and they will be made available in the dot context. +> +> Some built-in context providers are listed next: +
+ +
💽 Database context provider: Execute queries + +> Add the built-in Database Context Provider to run queries using the configured +> Go driver and connection string for your database. (Supports the `sqlite3` +> driver by default, compile with your desired driver to use it.) +> +> ```html +> +> ``` +
+ +
🗄️ Filesystem context provider: List and read local files + +> Add the built-in Filesystem Context Provider to List and read +> files from the configured directory. +> +> ```html +>

Here are the files: +>

    +> {{range .ListFiles "dir/"}} +>
  1. {{.Name}}
  2. +> {{end}} +>
+> ``` +
+ +
💬 NATS context provider: Send and receive messages + +> Add and configure the NATS Context Provider to send messages, use the +> Request-Response pattern, and even send live updates to a client. +> +> ```html +> +> ``` +
+ +
📤 Optimal static file serving + +> Non-template files in the templates directory are served directly from disk +> with appropriate caching responses and negotiates with the client to serve +> compressed versions. Efficient access to the content hash is available to +> templates for efficient SRI and perfect cache behavior. +> +> If a static file also has .gz, .br, .zip, or .zst copies, they are decoded and +> hashed for consistency on startup, and use the `Accept-Encoding` header to +> negotiate an appropriate `Content-Encoding` with the client and served +> directly from disk. +> +> Templates can efficiently access the static file's precalculated content hash +> to build a `