diff --git a/go.mod b/go.mod
index 34825ea..4be6868 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/livetemplate/components v0.0.0-20251224004709-1f8c1de230b4
- github.com/livetemplate/livetemplate v0.8.0
+ github.com/livetemplate/livetemplate v0.8.1-0.20260118195628-f6a04e2a2d8b
github.com/livetemplate/lvt v0.0.0-20260110064539-b9afb9e6df26
modernc.org/sqlite v1.43.0
)
diff --git a/go.sum b/go.sum
index ab3e39b..131066b 100644
--- a/go.sum
+++ b/go.sum
@@ -126,8 +126,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/livetemplate/components v0.0.0-20251224004709-1f8c1de230b4 h1:wLfVleSSlcv4NPg5KN8pul0Rz9ub1CtI8OAcPlyBYlw=
github.com/livetemplate/components v0.0.0-20251224004709-1f8c1de230b4/go.mod h1:+C2iGZfdgjc6y6MsaDHBWzWGIbBHna4l+ygFYJfuyUo=
-github.com/livetemplate/livetemplate v0.8.0 h1:eWUhB6jwkTj4rfgRAZG+Eap0wGeaovmwXO3hGdQpf5M=
-github.com/livetemplate/livetemplate v0.8.0/go.mod h1:0jD5ccG/VQ/BmjbsZdOamAeFh+aO/f1yJeMQqhxPa68=
+github.com/livetemplate/livetemplate v0.8.1-0.20260118195628-f6a04e2a2d8b h1:jzwam9JEm/IQpwCNHLeKD33004pjmQDdbkpmLeL/s4U=
+github.com/livetemplate/livetemplate v0.8.1-0.20260118195628-f6a04e2a2d8b/go.mod h1:0jD5ccG/VQ/BmjbsZdOamAeFh+aO/f1yJeMQqhxPa68=
github.com/livetemplate/lvt v0.0.0-20260110064539-b9afb9e6df26 h1:RRYko8rFvHz8ad5ixw3ke1ZvJKMtVqJA6Ddxx92Wobw=
github.com/livetemplate/lvt v0.0.0-20260110064539-b9afb9e6df26/go.mod h1:b+qhiaDS5oURHjiCs+ZPOoJTtZ+5cCM8xyriVA97uQo=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
diff --git a/progressive-enhancement/README.md b/progressive-enhancement/README.md
new file mode 100644
index 0000000..53391d4
--- /dev/null
+++ b/progressive-enhancement/README.md
@@ -0,0 +1,91 @@
+# Progressive Enhancement Example
+
+This example demonstrates how LiveTemplate supports progressive enhancement - allowing apps to work both with and without JavaScript enabled.
+
+## How It Works
+
+### With JavaScript (WebSocket Mode)
+- Actions are sent via WebSocket for instant updates
+- No page reloads - UI updates in real-time
+- Best user experience for modern browsers
+
+### Without JavaScript (HTTP Form Mode)
+- Actions are submitted via standard HTML forms
+- Server returns full HTML pages using POST-Redirect-GET pattern
+- Page reloads after each action
+- Works on any browser, including text-based browsers
+
+## Key Concepts
+
+### Dual-Mode Forms
+
+Forms work in both modes by including both `lvt-submit` (for JS) and `method="POST"` + `lvt-action` (for no-JS):
+
+```html
+
+```
+
+- **With JS**: `lvt-submit` triggers a WebSocket action, preventing form submission
+- **Without JS**: Standard form submission sends POST request to server
+
+### POST-Redirect-GET (PRG) Pattern
+
+For non-JS clients, successful actions redirect using HTTP 303:
+
+1. User submits form via POST
+2. Server processes action, updates state
+3. Server responds with 303 redirect to same URL
+4. Browser follows redirect with GET
+5. User sees updated page
+
+This prevents duplicate submissions when users refresh the page.
+
+### Validation Errors
+
+When validation fails:
+- **With JS**: Errors appear instantly via WebSocket update
+- **Without JS**: Server re-renders the page with errors inline (no redirect)
+
+### Flash Messages
+
+Success/error messages are shown once after actions:
+- **With JS**: Messages appear in real-time
+- **Without JS**: Messages passed via query params after redirect
+
+## Running the Example
+
+```bash
+# Development mode (uses local client library)
+LVT_DEV_MODE=true go run .
+
+# Production mode (uses CDN client library)
+go run .
+```
+
+Visit http://localhost:8080 and try:
+1. With JavaScript enabled - notice instant updates
+2. Disable JavaScript and refresh - notice page reloads after each action
+3. Both modes provide the same functionality
+
+## Configuration
+
+Progressive enhancement is enabled by default. To disable it:
+
+```go
+// Via environment variable
+LVT_PROGRESSIVE_ENHANCEMENT=false go run .
+
+// Or via code
+tmpl := livetemplate.New("app", livetemplate.WithProgressiveEnhancement(false))
+```
+
+## Files
+
+- `main.go` - Controller, state, and action handlers
+- `progressive-enhancement.tmpl` - Template with dual-mode forms
+- `progressive_enhancement_test.go` - End-to-end tests for progressive enhancement behavior
+- `README.md` - This documentation
diff --git a/progressive-enhancement/main.go b/progressive-enhancement/main.go
new file mode 100644
index 0000000..b0252b9
--- /dev/null
+++ b/progressive-enhancement/main.go
@@ -0,0 +1,186 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/go-playground/validator/v10"
+ "github.com/livetemplate/livetemplate"
+ e2etest "github.com/livetemplate/lvt/testing"
+)
+
+// TodoController is a singleton that holds dependencies.
+// In this example, we don't have external dependencies like a database.
+type TodoController struct {
+ validate *validator.Validate
+}
+
+// TodoState is pure data, cloned per session.
+// It contains all the state needed to render the template.
+type TodoState struct {
+ Title string `json:"title"`
+ Items []Todo `json:"items"`
+ // Form input values (preserved on validation errors)
+ InputTitle string `json:"input_title"`
+}
+
+// Todo represents a single todo item.
+type Todo struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ Completed bool `json:"completed"`
+ CreatedAt string `json:"created_at"`
+}
+
+// AddInput is the input struct for the Add action.
+type AddInput struct {
+ Title string `json:"title" validate:"required,min=3,max=100"`
+}
+
+// Mount is called once when a session is created.
+func (c *TodoController) Mount(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
+ state.Title = "Progressive Enhancement Todo List"
+
+ // Pre-populate with sample items if empty
+ if len(state.Items) == 0 {
+ state.Items = []Todo{
+ {ID: "1", Title: "Learn about progressive enhancement", Completed: true, CreatedAt: formatTime()},
+ {ID: "2", Title: "Try the app without JavaScript", Completed: false, CreatedAt: formatTime()},
+ {ID: "3", Title: "Enable JavaScript and see the difference", Completed: false, CreatedAt: formatTime()},
+ }
+ }
+
+ // Check for flash messages from URL (after redirect)
+ if success := ctx.GetString("success"); success != "" {
+ ctx.SetFlash("success", success)
+ }
+ if errorMsg := ctx.GetString("error"); errorMsg != "" {
+ ctx.SetFlash("error", errorMsg)
+ }
+
+ return state, nil
+}
+
+// Add handles adding a new todo item.
+func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
+ var input AddInput
+ if err := ctx.BindAndValidate(&input, c.validate); err != nil {
+ // Preserve the input value so user doesn't have to retype
+ state.InputTitle = ctx.GetString("title")
+ return state, err
+ }
+
+ // Create new todo
+ newID := fmt.Sprintf("%d", time.Now().UnixNano())
+ state.Items = append(state.Items, Todo{
+ ID: newID,
+ Title: strings.TrimSpace(input.Title),
+ Completed: false,
+ CreatedAt: formatTime(),
+ })
+
+ // Clear the input field
+ state.InputTitle = ""
+
+ // Set flash message for success
+ ctx.SetFlash("success", fmt.Sprintf("Added: %s", input.Title))
+
+ return state, nil
+}
+
+// Toggle handles toggling a todo's completed status.
+func (c *TodoController) Toggle(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
+ id := ctx.GetString("id")
+ found := false
+ for i := range state.Items {
+ if state.Items[i].ID == id {
+ state.Items[i].Completed = !state.Items[i].Completed
+ found = true
+ ctx.SetFlash("success", "Item updated")
+ break
+ }
+ }
+ if !found {
+ ctx.SetFlash("error", "Item not found")
+ }
+ return state, nil
+}
+
+// Delete handles deleting a todo item.
+func (c *TodoController) Delete(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
+ id := ctx.GetString("id")
+
+ // Find index of the item to delete without modifying the slice during iteration.
+ deleteIndex := -1
+ for i, item := range state.Items {
+ if item.ID == id {
+ deleteIndex = i
+ break
+ }
+ }
+
+ if deleteIndex >= 0 {
+ state.Items = append(state.Items[:deleteIndex], state.Items[deleteIndex+1:]...)
+ ctx.SetFlash("success", "Item deleted")
+ } else {
+ ctx.SetFlash("error", "Item not found")
+ }
+ return state, nil
+}
+
+func formatTime() string {
+ return time.Now().Format("2006-01-02 15:04:05")
+}
+
+func main() {
+ log.Println("Progressive Enhancement Example starting...")
+
+ // Load configuration from environment variables
+ envConfig, err := livetemplate.LoadEnvConfig()
+ if err != nil {
+ log.Fatalf("Failed to load configuration: %v", err)
+ }
+
+ // Validate configuration
+ if err := envConfig.Validate(); err != nil {
+ log.Fatalf("Invalid configuration: %v", err)
+ }
+
+ // Create controller with validator
+ controller := &TodoController{
+ validate: validator.New(),
+ }
+
+ // Create initial state
+ initialState := &TodoState{}
+
+ // Create template with configuration
+ // Progressive enhancement is enabled by default
+ tmpl := livetemplate.Must(livetemplate.New("progressive-enhancement", envConfig.ToOptions()...))
+
+ // Mount handler
+ http.Handle("/", tmpl.Handle(controller, livetemplate.AsState(initialState)))
+
+ // Serve client library (development only)
+ http.HandleFunc("/livetemplate-client.js", e2etest.ServeClientLibrary)
+
+ port := os.Getenv("PORT")
+ if port == "" {
+ port = "8080"
+ }
+ log.Printf("Server starting on http://localhost:%s", port)
+ log.Println("")
+ log.Println("Try it:")
+ log.Println(" 1. Open in browser with JavaScript ENABLED - uses WebSocket for instant updates")
+ log.Println(" 2. Disable JavaScript and refresh - uses HTTP form submissions with page reloads")
+ log.Println(" 3. Both modes work identically, just with different performance characteristics")
+ log.Println("")
+
+ if err := http.ListenAndServe(":"+port, nil); err != nil {
+ log.Fatalf("Server failed to start: %v", err)
+ }
+}
diff --git a/progressive-enhancement/progressive-enhancement.tmpl b/progressive-enhancement/progressive-enhancement.tmpl
new file mode 100644
index 0000000..04486d9
--- /dev/null
+++ b/progressive-enhancement/progressive-enhancement.tmpl
@@ -0,0 +1,209 @@
+
+
+
+ {{.Title}}
+
+
+
+
+
+
{{.Title}}
+
This app works with or without JavaScript enabled
+
+
+
+
+ JavaScript Mode: Using WebSocket for instant updates without page reloads.
+ Actions update the page in real-time.
+