/caddy-xtemplate

A html/template-oriented hypertext preprocessor as a Caddy module. Think PHP but the syntax looks like Go templates.

Primary LanguageGoApache License 2.0Apache-2.0

caddy-xtemplate is a Caddy module that extends Go's html/template library to be capable enough to host an entire server-side application in it. Designed with the htmx.org js library in mind, which makes server side rendered sites feel as interactive as a Single Page Apps.

⚠️ This project is in active development, expect regular breaking changes. ⚠️


Table of contents


Features

Query the database directly within template definitions

<ul>
  {{range .Query `SELECT id,name FROM contacts`}}
  <li><a href="/contact/{{.id}}">{{.name}}</a></li>
  {{end}}
</ul>

Note: The html/template library automatically sanitizes inputs, so you can rest easy from basic XSS attacks. Note: if you generate some html that you do trust, it's easy to inject if you intend to.

Define templates and import content from other files

<html>
  <title>Home</title>
  <!-- import the contents of a file -->
  {{template "/shared/_head.html" .}}

  <body>
    <!-- invoke a custom template defined anywhere -->
    {{template "navbar" .}}
    ...
  </body>
</html>

File-based routing & custom routes

GET requests for any file will invoke the template file at that path. Except files that start with _ which are not routed, this lets you define templates that only other templates can invoke.

.
├── index.html          GET /
├── todos.html          GET /todos
├── admin
│   └── settings.html   GET /admin/settings
└── shared
    └── _head.html      (not routed)

Create custom route handlers by defining a template with a name matching the pattern <method> <path>. Use httprouter syntax for path parameters and wildcards, which are made available in the template as values in the .Param key while serving a request.

<!-- match on path parameters -->
{{define "GET /contact/:id"}}
{{$contact := .QueryRow `SELECT name,phone FROM contacts WHERE id=?`
(.Params.ByName "id")}}
<div>
  <span>Name: {{.name}}</span>
  <span>Phone: {{.phone}}</span>
</div>
<!-- match on any http method -->
{{end}} {{define "DELETE /contact/:id"}}
{{$_ := .Exec `DELETE from contacts WHERE id=?` (.Params.ByName "id")}} OK
{{end}}

Automatic reload

Templates are reloaded and validated automatically as soon as they are modified, no need to restart the server. If there's a syntax error it continues to serve the old version and prints the loading error out in Caddy's logs.

Ctrl+S > Alt+Tab > F5

Showcase

Quickstart

Download caddy with all standard modules, plus the xtemplate module (!important) from Caddy's build and download server:

https://caddyserver.com/download?package=github.com%2Finfogulch%2Fcaddy-xtemplate

Write your caddy config and use the xtemplate http handler:

:8080

route {
    xtemplate {
        template_root templates
    }
}

Write .html files in the root directory specified in your Caddy config.

Run caddy with your config: caddy run --config Caddyfile

Remember Caddy is a super http server, check out the caddy docs for features you may want to layer on top. Examples: serving static files (css/js libs), set up an auth proxy, caching, rate limiting, automatic https, and more!

Config

xtemplate is configured through caddy's configuration system. Here are the config fields available to a Caddyfile:

xtemplate {
    template_root <root directory where template files are loaded>
    context_root <root directory that template funcs have access to>
    delimiters <left> <right>         # defaults: {{ and }}
    database {                        # default empty, no db available
        driver <driver>               # driver and connstr are passed directly to sql.Open
        connstr <connection string>   # check your sql driver for connstr details
    }
    config {                          # a map of configs, accessible in the template as .Config
      key1 value1
      key2 value2
    }
    funcs_modules <mod1> <mod2>       # a list of caddy modules under the `xtemplate.funcs.*`
                                      # namespace that implement the FuncsProvider interface,
                                      # to add custom funcs to the Template FuncMap.
}

These sql drivers are available by default. Modify db.go and recompile xtemplate to include your preferred drivers.

  • mattn/sqlite3 (requires building with CGO_ENABLED=1, not available from the caddy build server)
  • cznic/sqlite (available without CGo, including from the caddy build server)

Template syntax

Template syntax uses Go's html/template module, and extends it with custom functions and useful context.

Context values

The dot context {{.}} set on the main template handler provides request-specific data and stateful actions. See tplcontext.go for details.

  • Request and response related fields and fields
    • .Req is the current HTTP request struct, http.Request, which has various fields, including:
      • .Method - the method
      • .URL - the URL, which in turn has component fields (Scheme, Host, Path, etc.)
      • .Header - the header fields
      • .Host - the Host or :authority header of the request
    • .OriginalReq is the original, unmodified, un-rewritten request as it originally came in over the wire. Has the same fields as .Req.
    • .Params is a list of path parameters extracted from the url based on the current route, see custom routes. {{.Params.ByName "id"}}
    • .RemoteIP is the client's IP address. {{.RemoteIP}}
    • .Host is the hostname portion (no port) of the Host header of the HTTP request.
    • .Cookie Gets the value of a cookie by name. {{.Cookie "cookiename"}}
    • .Placeholder gets a caddy "placeholder variable". The braces ({}) have to be omitted.
    • .RespStatus Set the status code of the current response. {{.RespStatus 201}}
    • .RespHeader.Add Adds a header field to the HTTP response. {{.RespHeader.Add "Field-Name" "val"}}
    • .RespHeader.Set Sets a header field on the HTTP response, replacing any existing value.
    • .RespHeader.Del Deletes a header field on the HTTP response.
  • File related funcs. File operations are rooted at the directory specified by the root config option.
    • .ReadFile reads and returns the contents of another file, as-is. Note that the contents are NOT escaped, so you should only read trusted files.
    • .ListFiles returns a list of the files in the given directory, which is relative to the template context's file root.
    • .FileExists returns true if filename can be opened successfully
    • .StatFile returns Stat of a filename.
  • Database related funcs. All funcs accept a query string and any number of parameters. Prefer using parameters over building the query string dynamically.
    • .Exec executes a statment
    • .QueryRows executes a query and returns all rows in a big []map[string]any.
    • .QueryRow executes a query, which must return one row, and returns the map[string]any.
    • .QueryVal executes a query, which must return one row and one column, and returns the value of the column.
  • Other
    • .Template evaluate the template name with the given context and return the result as a string.
    • .Funcs returns a list of all the custom FuncMap funcs that are available to call. Useful in combination with the try func.
    • .Config is a map of config strings set in the Caddyfile. See Config.

Functions

There are built-in functions that perform actions that are unrelated to a specific request. See funcs.go for details.

Go stdlib template functions

See text/template#Functions.

Expand for a stdlib funcs documentation.
  • and Returns the boolean AND of its arguments by returning the first empty argument or the last argument. That is, "and x y" behaves as "if x then y else x." Evaluation proceeds through the arguments left to right and returns when the result is determined.
  • call Returns the result of calling the first argument, which must be a function, with the remaining arguments as parameters. Thus "call .X.Y 1 2" is, in Go notation, dot.X.Y(1, 2) where Y is a func-valued field, map entry, or the like. The first argument must be the result of an evaluation that yields a value of function type (as distinct from a predefined function such as print). The function must return either one or two result values, the second of which is of type error. If the arguments don't match the function or the returned error value is non-nil, execution stops.
  • html Returns the escaped HTML equivalent of the textual representation of its arguments. This function is unavailable in html/template, with a few exceptions.
  • index Returns the result of indexing its first argument by the following arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each indexed item must be a map, slice, or array.
  • slice slice returns the result of slicing its first argument by the remaining arguments. Thus "slice x 1 2" is, in Go syntax, x[1:2], while "slice x" is x[:], "slice x 1" is x[1:], and "slice x 1 2 3" is x[1:2:3]. The first argument must be a string, slice, or array.
  • js Returns the escaped JavaScript equivalent of the textual representation of its arguments.
  • len Returns the integer length of its argument.
  • not Returns the boolean negation of its single argument.
  • or Returns the boolean OR of its arguments by returning the first non-empty argument or the last argument, that is, "or x y" behaves as "if x then x else y". Evaluation proceeds through the arguments left to right and returns when the result is determined.
  • print An alias for fmt.Sprint
  • printf An alias for fmt.Sprintf
  • println An alias for fmt.Sprintln
  • urlquery Returns the escaped value of the textual representation of its arguments in a form suitable for embedding in a URL query. This function is unavailable in html/template, with a few exceptions.

Sprig library template functions

See the Sprig documentation for details: Sprig Function Documentation.

Expand for a listing of Sprig funcs.

xtemplate functions

  • markdown Renders the given Markdown text as HTML and returns it. This uses the Goldmark library, which is CommonMark compliant. It also has these extensions enabled: Github Flavored Markdown, Footnote, and syntax highlighting provided by Chroma.
  • splitFrontMatter Splits front matter out from the body. Front matter is metadata that appears at the very beginning of a file or string. Front matter can be in YAML, TOML, or JSON formats.
    • .Meta to access the metadata fields, for example: {{$parsed.Meta.title}}
    • .Body to access the body after the front matter, for example: {{markdown $parsed.Body}}
  • sanitizeHtml Uses bluemonday to sanitize strings with html content. {{sanitizeHtml "strict" "Shows <b>only</b> text content"}}
    • First parameter is the name of the chosen sanitization policy. "strict" = StrictPolicy(), "ugc" = UGCPolicy() for 'user generated content', "externalugc" = UGCPolicy() + disallow relative urls + add target=_blank to urls.
    • Second parameter is the content to sanitize.
    • Returns the string as a template.HTML type which can be output directly into the document without trustHtml.
  • humanize Transforms size and time inputs to a human readable format using the go-humanize library. Call with two parameters, the format type and the value to format. Format types are:
    • size which turns an integer amount of bytes into a string like 2.3 MB, for example: {{humanize "size" "2048000"}}
    • time which turns a time string into a relative time string like 2 weeks ago, for example: {{humanize "time" "Fri, 05 May 2022 15:04:05 +0200"}}
  • ksuid returns a 'K-Sortable Globally Unique ID' using segmentio/ksuid
  • idx gets an item from a list, similar to the built-in index, but with reversed args: index first, then array. This is useful to use index in a pipeline, for example: {{generate-list | idx 5}}
  • try takes a function that returns an error in the first argument and calls it with the values from the remaining arguments, and returns the result including any error as struct fields. This enables template authors to handle funcs that return errors within the template definition. Example: {{ $result := try .QueryVal "SELECT 'oops' WHERE 1=0" }}{{if $result.OK}}{{$result.Value}}{{else}}QueryVal requires exactly one row. Error: {{$result.Error}}{{end}}

Development

To work on this project, install xcaddy, then build from the repo root:

# build a caddy executable with the latest version of caddy-xtemplate from github:
xcaddy build --with github.com/infogulch/caddy-xtemplate

# build a caddy executable and override the xtemplate module with your
# modifications in the current directory:
xcaddy build --with github.com/infogulch/caddy-xtemplate=.

# build with CGO in order to use the sqlite3 db driver
CGO_ENABLED=1 xcaddy build --with github.com/infogulch/caddy-xtemplate

# build enable the sqlite_json build tag to get json funcs
GOFLAGS='-tags="sqlite_json"' CGO_ENABLED=1 xcaddy build --with github.com/infogulch/caddy-xtemplate

TZ=UTC git --no-pager show --quiet --abbrev=12 --date='format-local:%Y%m%d%H%M%S' --format="%cd-%h"

Project lineage and license

The idea for this project started in infogulch/go-htmx (now archived), which included the first implementations of template-name-based routing, exposing sql db functions to templates, and a persistent templates instance shared across requests and reloaded when template files changed.

go-htmx was refactored and rebased on top of the templates module from the Caddy server to create caddy-xtemplate in order to get a jump start on broader web server features without having to implement them from scratch.

caddy-xtemplate is licensed under the Apache 2.0 license. See LICENSE