Mental Model

LiveTemplate is server-driven UI for Go applications. You write standard html/template markup and a Go controller. The browser sends ordinary form data. The server runs a controller method, re-renders the template, diffs the result, and updates the browser.

The important constraint is also the useful part: start with HTML that works as a normal form POST, then opt into richer behavior only where the workflow needs it.

The three files

A small LiveTemplate app usually starts with three files:

The Your First App tutorial uses this shape directly: main.go, counter.go, and counter.tmpl.

What happens on click

Buttons and forms provide the routing key. A button like this:

<button name="increment">+1</button>

dispatches to this controller method:

func (c *CounterController) Increment(state CounterState, ctx *livetemplate.Context) (CounterState, error) {
    state.Count++
    return state, nil
}

The method receives the current state and returns the next state. If it returns an error, the state is not committed and the error can be rendered back into the template.

Where state lives

The controller is where dependencies live: databases, loggers, clients, and other long-lived services. State is per session group and should contain serializable UI state.

By default, anonymous visitors get a stable session group through a cookie. Tabs from the same browser share that group; different browsers get isolated groups. Authenticated apps can define their own grouping through an authenticator.

Transport fallback

The same controller action can run in three modes:

Browser capability What happens
JavaScript disabled The form submits normally and the browser navigates to the server-rendered response.
JavaScript enabled, WebSocket disabled The client intercepts the form, sends HTTP, and patches the DOM in place.
JavaScript and WebSocket enabled The client sends the action over WebSocket and receives DOM patches on the same connection.

The app code does not need separate handlers for those modes. Progressive enhancement is a transport concern, not a different application model.

DOM updates

After an action changes state, LiveTemplate renders the template on the server and compares it to the previous render. The browser receives the changed parts and patches the current DOM instead of replacing the whole page.

That means templates remain the source of truth. The browser client is there to preserve focus, submit actions, apply patches, and handle optional client attributes; it is not a second application.

When to use lvt-* attributes

Use plain HTML first:

Reach for lvt-* attributes when HTML cannot express the interaction cleanly: debounced input, explicit loading states, client-side DOM effects, click-away behavior, or SPA-style navigation that should keep the current LiveTemplate session.

When to use pub/sub

Pub/sub is not needed for a single form updating a single tab. Add it when another connection needs to react to an action.

The smallest common case is same-user multi-tab sync:

func (c *Controller) Mount(state State, ctx *livetemplate.Context) (State, error) {
    _ = ctx.Subscribe(ctx.SelfTopic())
    return state, nil
}

func (c *Controller) Save(state State, ctx *livetemplate.Context) (State, error) {
    // mutate state or durable storage
    ctx.Publish(ctx.SelfTopic(), "Refresh", nil)
    return state, nil
}

Subscribe opts the current connection into a topic. Publish sends an action to subscribed peers after the current action succeeds. Without both parts, no peer update happens.

Next

source: livetemplate/docs · path: content/getting-started/mental-model.md