From bda00688dc1b2fa0934afb4e53c1b8ad92ef8e50 Mon Sep 17 00:00:00 2001 From: Adnaan Badr Date: Sat, 9 May 2026 14:36:23 +0000 Subject: [PATCH] =?UTF-8?q?feat(recipes):=20add=20/recipes/counter=20?= =?UTF-8?q?=E2=80=94=20first=20docs-native=20recipe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First migration in the Phase 3 "drop examples mirroring, build recipes" wave. Establishes the canonical recipe shape and proves end-to-end: literate index.md + co-located _app/ deployable + cmd/site internal mount + tinkerdown's site-wide include resolver from v0.2.2. What this PR delivers /recipes/counter/ NEW recipe — "Counter, deeper" ~145 lines covering broadcast routing, AnonymousAuthenticator choice, session-group lifecycle, and where the pattern stops scaling. /recipes/counter/_app/ Moved from getting-started/_app/ counter/. Mounted by cmd/site at /apps/counter/ (unchanged from PR-A). Cross-references /getting-started/your-first-app include= paths repointed to ../recipes/counter/_app/... (works because tinkerdown v0.2.2's ParseFileInSite widens the include root to the site directory). Fixed PR-A drift: sharedAuth → AnonymousAuthenti- cator narrative; the wire-up main.go is now hand-pasted (handler.go isn't tutorial- shaped main); sidebar quote links to /recipes/counter for the deeper architecture story. / landing include= paths repointed; "Read the full walkthrough → ... or jump to Counter, deeper" link added. Plumbing Dockerfile TINKERDOWN_REF v0.2.0 → v0.2.2 (v0.2.1 added ParseFileInSite, v0.2.2 added page-toc !important sidebar fix). cmd/site/main.go import path getting-started/_app/counter → recipes/counter/_app. content/tinkerdown.yaml navigation entry for the new Recipes > Counter, deeper page. go.mod / go.sum go mod tidy reclassified livetemplate as direct dep (was indirect — Copilot feedback from PR #8). Verified end-to-end locally - /recipes/counter/ HTTP 200, embed-lvt + 26 literate-include matches - /getting-started/your-first-app HTTP 200, 3 cross-page-include matches (was: rejected with "escapes the page root" before v0.2.2) - /apps/counter/ HTTP 200, counter handler responds via cmd/site - Six embed-lvt blocks render across the recipe page - Zero "escapes" warnings in tinkerdown serve log - iPhone manual signoff: side-by-side broadcast pair tick in lockstep within a single browser; fresh incognito tab starts at 0 - Sidebar TOC items stack vertically (the v0.2.2 page-toc fix) Path-X-as-promised This is the first concrete migration of Phase 3 (revised). Examples repo is unchanged. Future recipes follow the same shape: mkdir recipes//, move/author code into _app/, write literate index.md, mount in cmd/site, add nav entry. No cross-repo coordination required. Co-Authored-By: Claude Opus 4.7 (1M context) --- Dockerfile | 2 +- cmd/site/main.go | 2 +- content/getting-started/your-first-app.md | 40 ++++-- content/index.md | 11 +- .../counter/_app}/counter.go | 0 .../counter/_app}/counter.tmpl | 0 .../counter/_app}/handler.go | 0 content/recipes/counter/index.md | 123 ++++++++++++++++++ content/tinkerdown.yaml | 2 + go.mod | 6 +- go.sum | 111 ++++++++++++++++ 11 files changed, 272 insertions(+), 25 deletions(-) rename content/{getting-started/_app/counter => recipes/counter/_app}/counter.go (100%) rename content/{getting-started/_app/counter => recipes/counter/_app}/counter.tmpl (100%) rename content/{getting-started/_app/counter => recipes/counter/_app}/handler.go (100%) create mode 100644 content/recipes/counter/index.md diff --git a/Dockerfile b/Dockerfile index 3a1fc46..23b5b5f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ # until upstream fixes the vendored asset embed (Phase 0 finding T0-1). # `TINKERDOWN_REF` selects which branch/tag to clone and is overridable. -ARG TINKERDOWN_REF=v0.2.0 +ARG TINKERDOWN_REF=v0.2.2 # ---- Stage 1: Build TypeScript client assets for tinkerdown ---- FROM node:20-alpine AS client-builder diff --git a/cmd/site/main.go b/cmd/site/main.go index 1334e77..8422c64 100644 --- a/cmd/site/main.go +++ b/cmd/site/main.go @@ -18,7 +18,7 @@ import ( "net/http" "os" - counter "github.com/livetemplate/docs/content/getting-started/_app/counter" + counter "github.com/livetemplate/docs/content/recipes/counter/_app" ) func main() { diff --git a/content/getting-started/your-first-app.md b/content/getting-started/your-first-app.md index 182a2d0..7e586e6 100644 --- a/content/getting-started/your-first-app.md +++ b/content/getting-started/your-first-app.md @@ -25,32 +25,51 @@ You'll have a `go.mod` and an empty directory. We'll add three files: `counter.g Create `counter.go`. First the state: -```go include="./_app/counter/counter.go" lines="5-11" +```go include="../recipes/counter/_app/counter.go" lines="5-11" ``` State is a value type, not a pointer — controllers receive a copy and return a (possibly modified) copy. The framework manages the swap. Then a controller and two action methods: -```go include="./_app/counter/counter.go" lines="13-33" +```go include="../recipes/counter/_app/counter.go" lines="13-33" ``` Action methods are exported on the controller, and their names ARE the action names — `Increment` and `Decrement` are what the template will reference. The `BroadcastAction` calls are how multi-tab sync works (Step 6). Now wire it up in `main.go`: -```go include="./_app/counter/main.go" lines="25-52" +```go +package main + +import ( + "log" + "net/http" + + "github.com/livetemplate/livetemplate" +) + +func main() { + tmpl := livetemplate.Must(livetemplate.New("counter", + livetemplate.WithParseFiles("counter.tmpl"), + )) + handler := tmpl.Handle(&CounterController{}, livetemplate.AsState(&CounterState{})) + + mux := http.NewServeMux() + mux.Handle("/", handler) + log.Fatal(http.ListenAndServe(":9090", mux)) +} ``` `livetemplate.New("counter")` parses `counter.tmpl` from the same directory. `tmpl.Handle(controller, AsState(initial))` is the standard wiring — controller for actions, initial state for new sessions. -The `WithAuthenticator(sharedAuth{})` option uses a constant-groupID authenticator so all connections share state — Step 6 has the why and the `sharedAuth` definition. +By default LiveTemplate uses `AnonymousAuthenticator`, which gives each browser a stable session group via cookie. Two consequences worth knowing about now: each browser gets its own state (no cross-user leaks), and tabs from the same browser share state — that's what makes the broadcast demo at Step 6 work. ## Step 3 — Write the template Create `counter.tmpl`: -```html include="./_app/counter/counter.tmpl" +```html include="../recipes/counter/_app/counter.tmpl" ``` The `