From 2e8befc55dbaf21d057cdb1943e0fe2d4d446ab7 Mon Sep 17 00:00:00 2001 From: Adnaan Badr Date: Mon, 4 May 2026 22:19:25 +0000 Subject: [PATCH 1/5] feat(landing): replace static todo with live tinkerdown lvt block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the iframe direction in favor of a tinkerdown ```lvt fenced block bound to a markdown source — same livetemplate primitives, no iframe chrome, multi-tab WebSocket sync demonstrated by sharing the source across all visitors. Lede rewritten to describe what LiveTemplate is in plain positive terms (Go library, template + controller, diff-based DOM patches over form-POST/fetch/WebSocket) instead of a "no X. no Y. no Z." negation list. Adds chromedp e2e test asserting the lvt block populates and a click toggles state, plus a staging deploy workflow so changes can be verified at livetemplate-docs-staging.fly.dev before promoting to prod. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/deploy-staging.yml | 36 +++++++++++++ content/_data/landing-tasks.md | 6 +++ content/index.md | 65 +++++++++++++++++----- e2e/landing_lvt_demo_test.go | 81 ++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/deploy-staging.yml create mode 100644 content/_data/landing-tasks.md create mode 100644 e2e/landing_lvt_demo_test.go diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 0000000..c4d2661 --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,36 @@ +name: deploy-staging + +on: + workflow_dispatch: + push: + branches: + - "staging-**" + +concurrency: + group: deploy-staging + cancel-in-progress: true + +jobs: + deploy: + name: Deploy to livetemplate-docs-staging.fly.dev + runs-on: ubuntu-latest + environment: staging + steps: + - uses: actions/checkout@v4 + + - uses: superfly/flyctl-actions/setup-flyctl@master + + - name: Deploy + run: flyctl deploy --config fly.toml --remote-only + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + + - uses: actions/setup-go@v5 + with: + go-version: '1.26' + + - name: Verify responsive layout against staging + working-directory: e2e + env: + E2E_BASE_URL: https://livetemplate-docs-staging.fly.dev + run: go test -run TestResponsiveLayoutAcrossViewports -v -timeout 300s diff --git a/content/_data/landing-tasks.md b/content/_data/landing-tasks.md new file mode 100644 index 0000000..2343b85 --- /dev/null +++ b/content/_data/landing-tasks.md @@ -0,0 +1,6 @@ +# Tasks + +- [x] Click me — I'm interactive +- [ ] Open this page in another tab +- [ ] Toggle me there, watch this tab update +- [x] That's LiveTemplate diff --git a/content/index.md b/content/index.md index 7644db6..bac97ce 100644 --- a/content/index.md +++ b/content/index.md @@ -1,39 +1,76 @@ --- title: "LiveTemplate" -description: "Reactive web UIs in standard HTML and Go. No custom template language. No client-side framework. No persistent connection required." +description: "A Go library for reactive web UIs. Write a Go template and a controller struct; when state changes, only the diff is sent to the browser. The same code runs over a plain form POST, fetch, or WebSocket." +sources: + landing_tasks: + type: markdown + file: "./_data/landing-tasks.md" + anchor: "#tasks" + readonly: false --- # Reactive web UIs in standard HTML and Go -No custom template language. No client-side framework. No persistent connection required. Forms POST. Buttons submit. Add the JS client and the DOM patches in place. Add a WebSocket and other tabs sync automatically. +LiveTemplate is a Go library for building reactive web UIs from standard `html/template` templates. You write a template and a controller struct; when state changes, the template re-renders on the server and only the diff is sent to the browser. The same code runs three ways: a plain `
` POST that reloads the page, a `fetch()` request that patches the DOM in place, or a WebSocket session where other tabs sync automatically. > **Alpha** — core features work and are tested, but the API may change before v1.0. -## A todo list, end to end +## Try it + +```lvt +
+{{if .Error}} +

{{.Error}}

+{{else}} +
    +{{range .Data}} +
  • + + {{.Text}} +
  • +{{end}} +
+Open this page in another tab and toggle a checkbox — both tabs stay in sync. +{{end}} +
+``` + +Click any checkbox above. The server diffs the new render against the previous one, sends only the changed attributes to the browser, and the DOM is patched in place — the input you just clicked never re-renders, so focus and scroll position are preserved. Open the page in a second tab and toggle there: both tabs update over WebSocket. + +## The same pattern, written directly in LiveTemplate + +The block above is rendered through tinkerdown, which wraps LiveTemplate. Without that wrapper, the same demo in raw LiveTemplate is a controller struct and a template: -The HTML: +The template: ```html - - - -
``` -The Go: +The controller: ```go -func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoState, error) { - state.Items = append(state.Items, Todo{Title: ctx.GetString("title")}) - ctx.BroadcastAction("Refresh", nil) // pushes the update to other WS-connected tabs +type TaskController struct{} + +func (c *TaskController) Toggle(state TaskState, ctx *livetemplate.Context) (TaskState, error) { + id := ctx.GetString("id") + for i, t := range state.Tasks { + if t.ID == id { + state.Tasks[i].Done = !state.Tasks[i].Done + } + } return state, nil } ``` -The button's `name` IS the action — ` + + + + ``` -The controller: +**The wire-up** (the rest of `main.go`): ```go -type TaskController struct{} - -func (c *TaskController) Toggle(state TaskState, ctx *livetemplate.Context) (TaskState, error) { - id := ctx.GetString("id") - for i, t := range state.Tasks { - if t.ID == id { - state.Tasks[i].Done = !state.Tasks[i].Done - } - } - return state, nil -} +tmpl := livetemplate.Must(livetemplate.New("counter", + livetemplate.WithParseFiles("counter.tmpl"), +)) +handler := tmpl.Handle(&CounterController{}, livetemplate.AsState(&CounterState{})) +http.ListenAndServe(":8080", handler) ``` -The `lvt-on:click="Toggle"` attribute names the routing key; LiveTemplate dispatches to the controller's `Toggle` method using the form data and `data-*` attributes the browser already sends. The wire is HTTP and the message format is HTML. +A button's `name` attribute IS the routing key — `