GoX lets you write HTML templates as typed Go expressions that compile to plain Go on the go.
- Seamless editor support: near-native language server experience across
.goxand.gofiles. - Full templating toolbox: conditionals, loops, composition, and reusable components.
- Extensible rendering pipeline: templates compile to a stream of render jobs, processable with custom printers.
templcompatible:gox.ElemimplementsRender(ctx, w)and drops in wherever atemplcomponent is expected.
Syntax guide: doors.dev/docs/template-syntax
For practical extensions on top of GoX, see
github.com/doors-dev/goxx.
This README focuses on installation, workflow, editor integration, and the rendering API behind GoX.
Working with an LLM agent? Point it at
llms.md— the condensed agent-facing reference (syntax, workflow, pitfalls). See Using with LLM agents below for setup.
Built for Doors
GoX is the template engine powering doors — a server-driven framework for building reactive web applications entirely in Go.
Doors lets you write full interactive UIs in Go without JavaScript, with reactive state, type-safe routing, and real-time sync under the hood. The browser acts as a remote renderer while your Go server is the UI runtime.
If you are looking for a complete framework on top of GoX, check out doors.
Not required, VS Code or Neovim extensions is enough to get started.
The easiest path is the prebuilt binary from GitHub Releases.
To install from source:
make installThat builds the bundled Rust formatter and installs gox. Building from source requires Go, Cargo, and a working native toolchain.
Recommended: use the official VS Code or Neovim extension.
The bare gox command starts the GoX language server. gox srv is the explicit form of the same command.
If you are wiring an editor manually:
- Run
goxorgox srv. - Attach it to both
.goand.goxbuffers. - Disable a separate workspace
goplsclient. GoX launches and proxies agoplsinstance. - Make sure
goplsis onPATH, or pass-gopls /path/to/gopls. - Install the tree-sitter-gox grammar.
Useful server flags:
-goplsto point at a specificgoplsbinary-listento expose the server over TCP or a Unix socket instead of stdio-listen.timeoutto stop an idle socket server after a timeout
GoX sits in front of gopls. It parses .gox, generates .x.go, keeps source and target positions mapped both ways, and forwards normal Go language features through a gopls instance.
go get github.com/doors-dev/goxKeep the installed gox tool and the Go module version reasonably in sync. Generated .x.go files carry a GoX version marker, and newer generated files require a tooling upgrade.
Most of the time, rendering starts with Elem.Render(ctx, w).
func Badge(label string) gox.Elem {
return <span class="badge">~(label)</span>
}
func handleBadge(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := Badge("New").Render(r.Context(), w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}Outside HTTP, you can render to any io.Writer:
func Example() error {
return Badge("New").Render(context.Background(), os.Stdout)
}If you need the HTML as a string, render into a buffer:
func RenderBadge(label string) (string, error) {
var buf bytes.Buffer
err := Badge(label).Render(context.Background(), &buf)
return buf.String(), err
}Use Elem.Print(ctx, printer) instead when you want to render through a custom Printer instead of the default HTML writer.
A typical package can contain all three kinds of files:
main.go # regular Go
page.gox # source template
page.x.go # generated Go
What you edit:
- edit
.gox - treat
.x.goas generated output - keep normal
.gofiles alongside both
What the tooling does:
- the language server regenerates
.x.goon save gox gendoes the same in batch mode- orphaned
.x.gofiles are removed automatically
Rules worth following:
- do not edit
.x.gomanually - do not use the
.x.gosuffix for hand-written files - if a generated file was produced by a newer GoX version, upgrade the tooling before continuing
gox # start the language server
gox gen # generate .x.go files for the current directory
gox gen ./pkg # generate a specific file or directory
gox fmt # format .gox and .go files in the current directory
gox fmt ./internal # format a specific directory
gox fmt ./main.go # format a specific file
gox ver # print the GoX version
By default, gox gen and gox fmt use the current directory and respect .gitignore. Both commands also accept an optional positional file or directory path. -no-ignore disables ignore handling, and -force skips target-file safety checks during generation.
The .gox parser produces a syntax tree. The assembler walks that tree and lowers template nodes into plain Go built around gox.Elem(func(cur gox.Cursor) error { ... }).
Alongside generation, GoX keeps a source-to-target translation map between .gox and .x.go. That mapping is used for the language server features.
gox fmt formats .gox with the bundled Rust formatter and regular .go files with gofmt. Embedded <script> and <style> blocks are reformatted too.
Generated .gox code ultimately works with four things:
- render values such as
Elem,Comp, and templ-compatible components - a
Cursorthat builds structure and emits rendering operations - an
Attrsset attached to an open head before it is submitted and exposed via cursor methods - a stream of
Jobvalues consumed by aPrinter
That is the useful mental model for GoX. Once you understand those pieces, generated .x.go files are easy to read and the advanced hooks stop feeling magical.
These are the core renderable types:
type Comp interface {
Main() Elem
}
type Elem func(cur Cursor) errorElem is the main render value in GoX. It is a function that renders through a Cursor.
It also:
- implements
Compby returning itself fromMain() - renders directly with
Render(ctx, w) - renders through custom pipelines with
Print(ctx, printer)
GoX also defines a minimal Templ interface for values that render with Render(ctx, w), and Cursor.Any knows how to emit those too.
So in normal Go code, a template is just a value you can return, store, pass around, and render.
Cursor is the low-level rendering state machine. It streams operations to a Printer and tracks active element heads so it can enforce correct ordering.
There are three head lifecycles:
- Regular element
Init(tag)-> optionalSet/Modify->Submit()-> child content ->Close() - Void element
InitVoid(tag)-> optionalSet/Modify->Submit() - Container
InitContainer()-> child content ->Close()
Regular and void heads become HTML tags. Containers do not emit a tag, but they still create open and close jobs in the stream, which makes them useful for grouping and for render-time transformations.
The important state rule is:
- before
Submit(), you are still building a head and may mutate attributes - after
Submit(), you may emit child content, but you may no longer mutate that head
cur.Context() returns the default context for jobs emitted through that cursor. cur.Printer() exposes the underlying printer for direct job emission. cur.Send() remains as a deprecated shortcut for cur.Printer().Send() and bypasses cursor state validation in the same way.
Generated .x.go files are mostly straightforward cursor code, similar to:
func Badge(label string) gox.Elem {
return gox.Elem(func(cur gox.Cursor) error {
if err := cur.Init("span"); err != nil {
return err
}
if err := cur.Set("class", "badge"); err != nil {
return err
}
if err := cur.Submit(); err != nil {
return err
}
if err := cur.Text(label); err != nil {
return err
}
return cur.Close()
})
}Most placeholder rendering ends up calling Cursor.Any or Cursor.Many.
Cursor.Any understands:
stringand[]stringElemand[]ElemCompand[]CompJoband[]JobEditorTempl[]interface{}
Anything else falls back to escaped fmt.Fprint.
Attrs is the mutable attribute set attached to a head while that head is being built.
You mainly encounter attributes in three places:
- when generated code or hand-written cursor code calls
Set - when code calls
Modifyto attach one or more render-time modifiers - when custom printers or proxies inspect
JobHeadOpen.Attrs
AttrSet and AttrMod remain as deprecated compatibility aliases for Set
and Modify.
Important details from the API:
- attributes are stored sorted by name
- names are case-sensitive
nilmeans "unset"falsemeans "unset"- any other non-nil value means "set"
The attribute system is also an extension point:
type Modify interface {
Modify(ctx context.Context, tag string, attrs Attrs) error
}
type Mutate interface {
Mutate(name string, value any) any
}
type Output interface {
Output(w io.Writer) error
}Modify is head-level, not value-level. It runs right before a head is rendered, receives the full Attrs set, and can inspect or change the final attributes for that element. This is the hook used by render-time attribute transformations.
Mutate is value-level. It lets a new attribute value depend on the previous value already stored under the same name.
Output lets a value control its own escaped output.
That makes attributes more than plain HTML metadata. They are also part of the rendering pipeline.
GoX exposes three main render-time extension points:
Editorfor code that needs direct cursor accessProxyfor wrapping or rebasing an element subtree before it rendersPrinterfor consuming and transforming the emitted job stream
Editor is the escape hatch for render-time behavior that needs direct cursor access.
Use it when rendering needs to:
- emit low-level jobs manually
- work directly with
cur.Context() - integrate with a larger rendering runtime
- do something more specific than "return another subtree"
A Proxy wraps an Elem subtree before it renders.
You can do any type of transofrmation with proxy, for example:
- change attributes
- convert tags
- render the subtree through a custom printer before forwarding it
- basically any transormation
A common implementation pattern is a proxy printer: call elem.Print(cur.Context(), customPrinter), inspect the first *gox.JobHeadOpen or *gox.JobComp, adjust it, then forward the rest into the current cursor.
Rendering is a job stream.
Useful concrete job types include:
*gox.JobHeadOpen*gox.JobHeadClose*gox.JobText*gox.JobRaw*gox.JobBytes*gox.JobComp*gox.JobTempl*gox.JobFprint*gox.JobError
Important behavior from the API:
- open and close jobs for the same head share an
ID - container head jobs emit no HTML, but still exist in the stream
- the default printer from
gox.NewPrinterchecksj.Context().Err()before callingOutput - jobs are pooled and single-use
Custom printers are where GoX opens up the most. They can buffer, transform, route, inspect, or reinterpret the stream instead of just writing HTML sequentially.
The helpers in helpers.go keep the API lightweight when you want one-off implementations:
gox.EditorCompfor values that should be bothEditorandCompgox.EditorCompFuncgox.EditorFuncgox.ProxyFuncgox.ModifyFuncgox.PrinterFuncgox.NewEscapedWriter
gox.NewEscapedWriter is useful when custom rendering code needs the same escaping rules as GoX text and attribute output.
llms.md is a condensed, agent-facing reference: syntax essentials, workflow rules, common pitfalls, and the goxx defaults. Wire it into whichever agent you use:
-
Claude Code — add a line to your project's
CLAUDE.md:@https://raw.githubusercontent.com/doors-dev/gox/main/llms.mdOr vendor a local copy and reference it:
@docs/gox-llms.md. -
Cursor — drop the file into
.cursor/rules/gox.mdand add a frontmatter glob so it activates only on GoX files:--- globs: ["**/*.gox", "**/*.x.go"] ---
-
Generic
AGENTS.md/ Copilot instructions / other tools — copy the contents into the relevant section, or link to the raw file above.
The doc assumes the agent will run gox gen itself, so make sure the gox CLI is installed and on PATH in the agent's environment (see Install).
Disclaimer: GoX is an independent, third-party project and is not affiliated with, endorsed by, or sponsored by The Go Project, Google, or any official Go tooling.