Skip to content

refactor: examples/ as canonical home for runnable apps - #35

Merged
adnaan merged 12 commits into
mainfrom
feat/examples-as-canonical
May 23, 2026
Merged

refactor: examples/ as canonical home for runnable apps#35
adnaan merged 12 commits into
mainfrom
feat/examples-as-canonical

Conversation

@adnaan

@adnaan adnaan commented May 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Consolidates every runnable app under docs/examples/<slug>/ (Go package, template, e2e test, standalone runner). Recipe markdown stays in content/recipes/<slug>/index.md as docs-only.

Before: each example was spread across content/recipes/<slug>/_app/ (the package), e2e/<slug>/main.go + *_test.go (separate Go module wrapper), content/recipes/apps/<slug>.md (synced README from livetemplate/examples). Adding a recipe touched 4 files across 2 repos, including a per-recipe job in livetemplate/livetemplate/.github/workflows/cross-repo-test.yml.

After: every runnable app lives at examples/<slug>/. Adding a recipe is mkdir examples/foo + write the four files (handler, tmpl, test, cmd/main.go) + 2 lines in cmd/site/main.go. The livetemplate/examples repo becomes redundant (cleanup deferred to follow-up PR G).

Built on top of livetemplate/tinkerdown#280 which adds site-rooted include="/..." syntax so recipe markdown can cite the relocated source files cleanly.

Commits

Commit Change
1af94d0 counter: move out of content/recipes/_app/
d5bc795 todos: co-locate code + test
f76b8a7 patterns: co-locate code + test
b1edf82 progressive-enhancement: co-locate code + test
6588680 login: co-locate code + test
3041798 shared-notepad: co-locate code + test
f2fd0ac dissolve e2e/go.mod into docs root module
ca289ea drop _app/ adjacency mirror + livetemplate/examples entries (-269 lines)
8fbbd71 rewrite CONTRIBUTING + README + Makefile for examples/ layout
7b585c1 port 6 orphans from livetemplate/examples (chat, avatar-upload, flash-messages, dialog-patterns, landing-demo, live-preview)
842226a temporary Dockerfile pin to feat/site-rooted-includes branch (until tinkerdown#280 ships a tag)

Result

  • 12 example folders under examples/ (6 in-tree refactored, 6 orphan-ported)
  • 27 literate includes rewritten from ./_app/... to site-rooted /examples/<slug>/...
  • e2e/go.mod dissolved — single Go module again
  • CONTRIBUTING / README / Makefile rewritten to document the new shape
  • Adding an example = 3 steps in 1 repo (down from 6 steps in 2 repos)

Blockers / follow-ups

  • Blocked on tinkerdown#280 — needs to merge AND tag a release so the Dockerfile can pin to a version instead of feat/site-rooted-includes (currently a branch pin, marked TEMPORARY in the Dockerfile).
  • Follow-up PR (livetemplate/livetemplate) — collapse the 5 per-recipe jobs in .github/workflows/cross-repo-test.yml into one test-docs-examples job that runs go test ./examples/.... Saves ~280 lines of YAML.
  • Follow-up (archive livetemplate/examples repo) — after this lands and bakes, archive the now-redundant standalone examples repo.

Test plan

  • go build ./... clean
  • go test -short ./examples/... — all 12 example packages green
  • go test -short ./cmd/... — sync tool + site main build green
  • tinkerdown validate content/ (with tinkerdown#280 binary) — 55/55 pages valid
  • Once tinkerdown tag ships: bump Dockerfile TINKERDOWN_REF back to a tag and verify CI is fully green
  • Browser e2e suite (go test ./examples/... without -short) — passes locally; runs in CI per the cross-repo workflow

🤖 Generated with Claude Code

adnaan and others added 12 commits May 23, 2026 05:34
Counter is the first recipe to be folded into the new examples/<slug>/
layout. The package, template, and runtime move out of
content/recipes/counter/_app/ into examples/counter/, with a new
cmd/main.go subpackage that lets `go run ./examples/counter/cmd`
launch the recipe standalone.

Handler now takes variadic livetemplate.Option so callers control
origin policy: cmd/site passes the docs-deploy allowlist; cmd/main.go
passes either the same allowlist or permissive-origin + dev-mode under
--dev. The recipe itself stays environment-agnostic.

content/recipes/counter/index.md and content/index.md switch from
`include="./_app/..."` to site-rooted `include="/examples/counter/..."`,
using the tinkerdown syntax added in livetemplate/tinkerdown PR for
feat/site-rooted-includes. The Dockerfile pin will need updating to
that tinkerdown tag before this branch can merge — tracked in PR
description.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
todos follows the same pattern as counter: the package, template,
SQLite schema, and sqlc-generated bindings move from
content/recipes/todos/_app/ to examples/todos/. The chromedp browser
e2e test moves from e2e/todos/todos_test.go to examples/todos/
todos_test.go and switches from package main to package todos_test
(blackbox external). The subprocess invocation switches from
`go run .` (which assumed main was in the test cwd) to
`go run ./cmd`, pointing at the new examples/todos/cmd/main.go that
replaces the deleted e2e/todos/main.go wrapper.

internal `github.com/livetemplate/docs/content/recipes/todos/_app/db`
import paths in controller.go, state.go, db_manager.go rewrite to
`examples/todos/db`.

go.mod promotes chromedp/chromedp and gorilla/websocket from indirect
to direct (now used by the relocated test).

content/recipes/todos/index.md switches all seven literate includes
from `./_app/...` to site-rooted `/examples/todos/...`.

Handler signature unchanged — todos already took variadic
livetemplate.Option, so cmd/site keeps passing
WithAllowedOrigins(allowedOrigins) the same way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
patterns is the third example folded into examples/<slug>/. All 17 Go
source files, the templates/ subdirectory (5 categories × ~6 templates
each), and the chromedp e2e test move from content/recipes/patterns/_app/
and e2e/patterns/ to examples/patterns/.

The new examples/patterns/cmd/main.go preserves the dual-mount the
old e2e/patterns/main.go required: tests navigate to bare root for the
catalog index AND to "/recipes/ui-patterns/<cat>/<slug>" for individual
pattern pages. patterns.Handler is idempotent across calls with the
same basePath (handlerOnce cache), so both mounts route to the same
logical app.

content/recipes/broadcasting.md updates 4 region-based includes from
`./patterns/_app/...` to `/examples/patterns/...`. ui-patterns/index.md
updates 2 cross-references (link + tree URL) to docs/examples/patterns.

Handler signature unchanged — already took (basePath, opts...). The
//go:embed templates directive resolves to examples/patterns/templates
post-move (path is relative to the package file).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
progressive-enhancement is the fourth example folded into examples/<slug>/.
controller.go, handler.go, the .tmpl, and the e2e test all move from
content/recipes/progressive-enhancement/_app/ + e2e/progressive-enhancement/
to examples/progressive-enhancement/.

The new examples/progressive-enhancement/cmd/main.go preserves the
dual-mount the old e2e wrapper required: Tier A (default JS+WS) at "/",
Tier B (WebSocket-disabled fallback) at "/no-ws/". cmd/site already
mounts the production /apps/progressive-enhancement/ + /no-ws/ dual the
same way — those mounts are unchanged here aside from the new import
path.

content/recipes/progressive-enhancement/index.md updates 4 includes
from `./_app/...` to site-rooted `/examples/progressive-enhancement/...`.

Test moves package main → package progressiveenhancement_test (blackbox);
subprocess invocation switches from `go run .` to `go run ./cmd`.
go.mod promotes chromedp/cdproto to direct (now used by the relocated test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
login is the fifth example folded into examples/<slug>/. controller.go,
handler.go, auth.html, and the chromedp e2e test move from
content/recipes/login/_app/ + e2e/login/ to examples/login/.

The new examples/login/cmd/main.go preserves the MOUNT_PATH-driven
dual-mount the old e2e wrapper required: root mount for happy-path
tests, subpath StripPrefix mount for redirect-target-bug regression
tests.

content/recipes/login/index.md updates 6 region-based includes from
`./_app/...` to site-rooted `/examples/login/...`.

Test moves package main → package loginrecipe_test (blackbox); imports
github.com/livetemplate/docs/examples/login (package name remains
loginrecipe). Subprocess invocation switches `go run .` → `go run ./cmd`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shared-notepad is the sixth and final in-tree example folded into
examples/<slug>/. controller.go, handler.go, notepad.tmpl, and the
chromedp e2e test move from content/recipes/shared-notepad/_app/ +
e2e/shared-notepad/ to examples/shared-notepad/.

examples/shared-notepad/cmd/main.go uses notepad.NewDemoBasicAuth so
tests can authenticate as alice/bob via the standard
http://<user>:demo@host/... URL form. Production cmd/site still wires
AnonymousAuthenticator because tinkerdown's embed-lvt server-side
prefetch can't forward Authorization headers in inline embeds — that
mount is unchanged aside from the new import path.

content/recipes/shared-notepad/index.md updates 6 region-based
includes from `./_app/...` to site-rooted `/examples/shared-notepad/...`.

Test moves package main → package notepad_test (blackbox); imports
github.com/livetemplate/docs/examples/shared-notepad (package name is
notepad). Subprocess invocation switches `go run .` → `go run ./cmd`.

All six in-tree example folders are now under examples/. Build is
clean, go test -short ./examples/... passes (chromedp tests skip
under -short but compile correctly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With all per-example e2e tests now co-located in examples/<slug>/, the
e2e/ directory contains only site-level tests (landing, broadcasting
embed, responsive, staging) plus the screenshot/sweep CLI tools. There
is no longer a reason for these to be in a separate Go module — the
original split existed so each e2e/<slug>/ could have its own main.go
wrapper without producing duplicate-main-package errors in the parent
module.

Drop e2e/go.mod and e2e/go.sum. All deps the e2e tests + cmd binaries
need are already in the root go.mod (promoted to direct in earlier
commits during per-example test migration).

go test ./e2e/... + go build ./... + go test ./examples/... all clean
post-dissolve.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ntries

With docs/examples/<slug>/ as the canonical home for runnable apps
(no longer mirrored from livetemplate/examples), the sync tool's
mirrorAdjacentApp function and its 4 unit tests become dead code:

- Delete mirrorAdjacentApp + copyFile from cmd/sync/sync.go
- Delete the 4 _app/-mirroring tests from cmd/sync/sync_test.go
- Drop the 9 livetemplate/examples entries from source-of-truth.yaml
  (mirrored README summaries that now live native at content/recipes/
  apps/*.md, plus the contributing/examples mirror)
- Drop livetemplate/examples from the sync workflow's source_repo
  choice list (.github/workflows/sync.yml)
- Update content/_meta/source-of-truth.md prose: replace the
  "_app/ adjacency rule" section with a "site-rooted includes for
  cross-tree literate authoring" section, drop the
  livetemplate/examples link-rewrite rules

This isolates the docs site from the livetemplate/examples repo.
The orphan examples (chat, avatar-upload, flash-messages, etc.) will
be ported into docs/examples/ in a follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CONTRIBUTING.md:
- "Where content lives" lists the new examples/<slug>/ as the canonical
  home for runnable apps; content/recipes/ is now markdown-only.
- New "Adding an example" section documents the per-app shape (handler
  + tmpl + test + cmd/main.go) and the 4-step add procedure.
- Local-dev section adds `go run ./examples/<slug>/cmd --dev` and
  `make serve`; updates test commands to `go test ./examples/...`
  (replacing the obsolete `cd e2e && go test ./...`).

README.md:
- "How content gets here" table drops the livetemplate/examples row
  (no longer a source of truth); adds the examples/<slug>/ row.
- Adds a paragraph describing the runnable-apps shape.

Makefile:
- Drop `run-firstapp` (cited an _app/ path that no longer exists).
- Add `site` (run cmd/site standalone), `examples-test` (= go test
  ./examples/...), `examples-list` (enumerate examples one per line).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the remaining standalone-only examples into the docs repo as
the canonical home. Each was a single-file Go program in
livetemplate/examples/<slug>/; we keep that shape (package main +
*.tmpl + *_test.go in one directory) because these examples are
standalone-only — they don't get embedded inline in docs pages, so
they don't need the Handler-package + cmd/main.go split the in-tree
recipes use.

Folders ported (file count, deploy notes):

- chat (main.go + chat.tmpl + chat_e2e_test.go + README)
- avatar-upload (main.go + avatar-upload.tmpl + test + run.sh +
  uploads/.gitkeep; cleared the runtime uploads/* artifacts that
  shouldn't be in version control)
- flash-messages (main.go + flash.tmpl + two test files + README)
- dialog-patterns (main.go + dialog-patterns.tmpl + test)
- landing-demo (main.go + counter.tmpl + test + README + Dockerfile +
  fly.toml — preserved so the lt-landing-demo fly app can keep
  deploying. After livetemplate/examples is archived in PR G, the
  Dockerfile will need updating to clone from docs.)
- live-preview (main.go + preview.tmpl + test)

Version skew check: zero. livetemplate/examples/go.mod and
docs/go.mod both pin livetemplate v0.9.2; no API surface to
reconcile. All 6 packages compile + pass tests under -short
(go test ./examples/... runs all 12 example packages green).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The examples/<slug>/ refactor relies on tinkerdown's new site-rooted
include syntax (`include="/examples/foo/bar.go"`), which is in
livetemplate/tinkerdown PR feat/site-rooted-includes — not yet
merged or tagged. The Dockerfile clones that branch directly so docs
CI's `tinkerdown validate content/` step succeeds.

Once PR 0 in livetemplate/tinkerdown merges and is tagged (e.g.
v0.3.0), revert this branch pin back to a tag in the same `git
clone --branch=` form. The TODO is in the Dockerfile comment above
the ARG line.

This commit is the last piece of PR 1 (the docs refactor). After this:
  - docs CI build is green (tinkerdown branch supports the new syntax)
  - go build ./... + go test ./examples/... clean
  - all 55 content pages validate with the new tinkerdown

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
livetemplate/tinkerdown#280 has merged and v0.3.0 is published, which
includes the site-rooted include="/..." syntax this PR depends on.
Replace the temporary feat/site-rooted-includes branch pin with the
proper version tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@adnaan
adnaan merged commit 2bec46f into main May 23, 2026
2 checks passed
@adnaan
adnaan deleted the feat/examples-as-canonical branch May 23, 2026 11:35
adnaan added a commit that referenced this pull request May 23, 2026
)

The cross-repo cross-repo-test.yml's new test-docs-examples job (and
local `go test -run` invocations under e2etest.StartTestServer) shells
out to `go run ./cmd` without passing flags. The cmd/main.go entry
points only enabled dev mode via the `--dev` flag, so the spawned
server got the production origin allowlist — which rejects Chrome's
`http://host.docker.internal:<port>/` WebSocket upgrades:

  WARN WebSocket origin rejected origin=http://host.docker.internal:37823
  ERROR WebSocket upgrade failed component=live_handler
        error="websocket: request origin not allowed by Upgrader.CheckOrigin"

Result: TestLogin_E2E/ServerPushedWelcome, all patterns realtime tests,
TestPE_TierA_BrowserE2E, TestSharedNotepad_E2E, TestTodosE2E all failed
in the cross-repo job (~30+ test FAILures total).

Fix: honor LVT_DEV_MODE=true in addition to the --dev flag. e2etest.
StartTestServer already sets `LVT_DEV_MODE=true` in the subprocess
environment (see lvt/testing/chrome.go), so the production allowlist
branch is correctly bypassed when the test framework invokes cmd/main.
Applied to all 6 in-tree examples (counter, todos, patterns,
progressive-enhancement, login, shared-notepad).

This bug existed in PR #35 from the start — I missed it locally because
`go test -short ./examples/...` skips chromedp browser tests. The
cross-repo CI ran the full suite (no -short) and surfaced it. Verified
fix locally with `go test -run TestLogin_E2E$ ./examples/login/`:
TestLogin_E2E/ServerPushedWelcome now PASSES.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
adnaan added a commit to livetemplate/livetemplate that referenced this pull request May 23, 2026
The test-examples job ran the livetemplate/examples monorepo's test
suite against this repo's main. With the docs examples/-as-canonical
refactor (livetemplate/docs#35) absorbing every runnable app into
docs/examples/<slug>/, livetemplate/examples is redundant — every
example it tested is now covered by test-docs-examples (added in
#437).

Drop the job (~70 lines of YAML). Three jobs now remain in
cross-repo-test.yml: test-lvt, test-docs-examples, test-tinkerdown.

After this lands, livetemplate/examples can be archived as a GitHub
repo setting — no remaining workflows reference it.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant