Frictions evaluation · 2026-05-02
Each item in frictions.md evaluated against Gastro's stated
design goals — fun Go web dev, idiomatic stdlib usage, end-to-end type
safety, opinionated structure — and split between the two
de-facto consumption modes that the friction document itself reveals.
Section C of the friction backlog is explicit about a split that the rest of
the document leaves implicit: a downstream project (git-pm) decided to skip
page routes entirely (01KQ45N0) and call gastro components
directly from its own Go handlers, because mutating handlers don't fit a
page-rendering model. Naming the modes makes the trade-off legible.
Pages live in pages/. Routes are auto-generated. Deps come
from WithDeps[T] + From[T](ctx). The host
typically does http.ListenAndServe(":4242", router.Handler())
and stops there.
User writes their own router/handlers. Components are invoked through the
generated Render.X(props). Auto-generated page routes are
ignored or replaced via WithOverride.
gastro dev assumptions about main.go layout, hand-coded SSE selectors, manual children plumbing.Gastro's design goals — fun, idiomatic, type-safe, opinionated — point at framework mode. But the project structure goal also says opinionated, not exclusive: components are exported as a typed Go API (
Render.X) on purpose. The two modes are both legitimate; the question is which frictions to close in each.
Library-mode item Framework-mode item Both modes
| ID | Friction | Mode affinity | Priority | Verdict | Goals served |
|---|---|---|---|---|---|
| A1 | CLI not toolchain-pinnable | Both | P1 | Ship — directive form | Idiomatic Go |
| A2 | Generated code committed | Both | P2 | After A1; option 1 first | Idiomatic Go |
| A3 | Two render paths (Props vs dict) | Both | P2 | Hold — v0.1.15 closed the gap | Type safety |
| A4 | Selector ↔ component decoupling | Lib (today) | P2 | Ship — high leverage, low cost | Type safety |
| A5 | Untyped children plumbing | Both, lib stings more | P1 | Ship — typed Children + slots | Type safety |
| A6 | No static-asset hashing | Both | P2 | Runtime variant first | Opinionated structure |
| A7 | Component name collision | Both | P2 | Ship — pure DX | Fun |
| B1 | Snippet / include idiom | Mostly framework | P2 | Defer — tension with opinion | Fun (vs structure) |
| B2 | Per-router dev mode | Lib | P3 | Ship — trivial | Idiomatic Go |
| C1 | Page actions | Framework | P1 | Spike before committing | Fun (risks idiom) |
| C2 | Route middleware composition | Framework | P2 | Ship — additive | Idiomatic Go |
| C3 | Programmatic page invocation | Framework | P2 | Only after C1 | Type safety |
| C4 | Page render error contract | Framework | P2 | Ship — mostly docs | Idiomatic Go |
| C5 | Test helpers for page handlers | Framework | P3 | After C3 | Fun |
| D1 | gastro dev for embedded-package projects | Lib | P2 | Ship — gastro watch | Fun (unlocks lib mode) |
Three observations from the matrix that the per-item analysis below expands on:
The go run github.com/andrioid/gastro/cmd/gastro@VERSION generate
directive matches what protobuf, sqlc, and ent already do — squarely in
"idiomatic Go" territory. The importable codegen.Run()
alternative locks the codegen entry point as public API and forecloses
future refactors; that's a bigger commitment than the friction warrants.
Solves the "stale CLI builds clean but generates wrong code" surprise during version bumps.
Same benefit, no special handling needed.
Goal alignment: straight idiomatic-Go win. No tension.
Option 1 (point //go:embed at source components/ and
pages/ instead of .gastro/templates/ copies) is
the safer first move — it shrinks the committed tree without changing the
review story. Option 2 (full .gitignore) only becomes safe
once A1 makes go generate reproducible; doing it earlier
silently amplifies version-skew failures.
Goal alignment: reduces noise, keeps "gastro is a Go project" pitch honest.
The 2026-04-30 build-time dict validation already converted
the painful runtime case ("blank Card field on a typo") into a build
error. What remains is mechanical edit-fatigue, not correctness. Both
proposed options have real costs:
html/template under the hood".Recommended: stay with v0.1.15's validation as the long-term answer unless and until template ergonomics show a sharper concrete pain that neither option masks. This is one of the very few items where doing nothing is the design-aligned answer.
Library mode hits this acutely: SSE morph handlers in user Go code
hardcode "#pm-decisions-list" and break silently when a
component's root id= changes. Framework mode hits the same
pain the moment C1 (page actions) is in play, because actions emit
Datastar patches scoped to selectors.
Drop-in upgrade for any handler doing morph patches today. Zero migration cost — call sites move from string literals to Render.X.Selector incrementally.
Becomes a typed contract once C1 lands. Codegen can emit selectors as constants on the action's return type.
Goal alignment: end-to-end type safety. The "1 component : N mounts" caveat in the original write-up is real but rare; treating the constant as a default (overridable by literal) is the right pragmatic shape.
Children + slots
__children is the last untyped corner of the Props surface.
Library mode notices it first because handlers construct Props by hand
(e.g. LayoutProps.Detail template.HTML); framework mode
only notices when reaching for named slots. A frontmatter slots:
field that generates typed template.HTML fields is the
cleanest closure of the type-safety gap.
Migration cost is real: existing manual Props that pass
children via the dict map break. That should be a tracked
deprecation, not a silent flip. The current map-based form can
coexist for one or two minor versions with a build warning.
Goal alignment: directly serves "type-safety for components and templates". This is the highest-impact A-item after A4.
The build-time path-rewriting half is the fragile one (CSS
url(), JS-built URLs, attribute parsing edge cases). The
runtime hash + redirect half is small, robust, and immediately
unlocks long-cache headers without any template rewriting. Ship that
behind WithHashedStatic() and revisit build-time rewriting
if a real adopter says the runtime form leaves win on the table.
Goal alignment: mild friction with "opinionated structure" (it's an option, not a default), but consistent with "single binary, zero runtime dependencies" — the hash table lives in process memory.
A constant-time pre-pass over the component list during compile. The failure mode today (an opaque Go compile error somewhere downstream) burns half an hour the first time and zero minutes after that — but that first half hour is exactly the "Go web dev should be fun" promise breaking. No design tension whatsoever.
A single-process app rarely needs this, but parallel test setups and
multi-tenant servers do. The handler-instance refactor (decision
2026-04-26) already moved isDev onto the
*Router; this is just exposing a setter that overrides the
env var. Trivial in the friction doc is accurate.
gastro dev unusable for embedded-package projectsgastro watch
This is the friction that decides whether library mode is a supported
use case or an unofficial accident. Cost is low: the watcher loop in
runDev already exists; extracting runWatch is
refactoring, not new design. After it lands, gastro dev can
deprecate to a thin wrapper around gastro watch + a
project-supplied build command, collapsing back to one mental model.
Goal alignment: shipping this signals that
"components as a Go API" is a first-class shape, not a
workaround. That matters because the typed Render surface
is already public; pretending only framework mode exists makes the
docs lie about what the binary actually supports.
.gastroThis is the single most consequential item in the document. It's the only one that decides whether framework mode is viable for SSE-heavy apps, and it's also the one with the largest design surface: a new keyword, a new return type, a new error contract, a new way for frontmatter to fail. Two countervailing pressures:
The whole framework-mode pitch (Astro DX) collapses for mutating apps without it. Without C1, the "primary" mode is the one fewer real apps choose.
It introduces a real DSL on top of html/template. The README explicitly promises "no new language to learn". Once shipped, the action surface becomes part of every gastro upgrade story forever.
Recommendation: spike in examples/ before
committing to the syntax. Build the git-pm board page two ways — once
with library-mode handlers (today's status quo), once with a prototype
action declaration in a page. Compare the line counts, the
error stories, and the test setup. The friction document already
identifies A3 as a prerequisite; A4 and A5 also need to be in place
before action returns can be type-checked end-to-end. This is not a
one-PR feature.
Goal alignment: serves the "fun" goal best of any
item; tensest with the "idiomatic Go" goal of any item. Worth the
investment, but only after the supporting type-safety items
(A4, A5) are landed so that the new surface ships typed
rather than as another map[string]any retreat.
WithMiddleware(pattern, h) as a sibling of the existing
WithOverride(pattern, h). Same typo-safety contract (panic
on unknown pattern), same option-at-construction shape. Useful even
without C1: per-route CSRF/origin checks already need this in any app
that mounts both browser pages and SSE endpoints under one router.
One real design question the doc raises: option-per-pattern vs converging on a chi/gin-style middleware chain. Recommendation: keep option-per-pattern. Gastro's whole router is declarative; switching to a fluent chain would be the odd one out.
Without C1, page frontmatter is just "render this template with these
deps", and the generated Render.X(props) already covers the
re-render-from-an-SSE-handler case. C3 only earns its keep if pages start
holding meaningful logic that morph handlers want to reuse — which is
precisely the C1 scenario.
Naming the accessor RenderPage (not folding it into
Render) is correct: pages run a different pipeline (deps
attachment, request context) than components, and the typed accessor
should make that visible.
Pure additive. WithErrorHandler(func) + a documented
FromOK[T] idiom + an error-handling.md doc.
The cost is committing to a public failure contract, not the code. The
real win is making framework mode predictable — production
adopters won't move workloads to gastro without this.
A wrapper over httptest that hides the most repetitive
plumbing. Worth shipping only if it stays small — six lines per
project becomes a maintenance burden the moment it grows a fluent DSL.
The friction doc's own framing ("resist the urge to add fluent-DSL
features") is correct; the discipline question is whether to commit to
that discipline before the package exists.
The friction doc identifies a real cost (10–20 line shared fragments
paying full component overhead), but the solution sits awkwardly
against the "opinionated project structure" goal. Adding snippets/
or a snippet: true frontmatter flag means a second
authoring concept whose distinction from "component" is "no Props" — a
line that's easy to author wrong and creates a refactor with no
automated path when a snippet eventually grows parameters.
Two existing escape hatches partly cover this:
{{define}} / {{template}} already handles within-file factoring (design.md §8).
Recommendation: hold until a use case appears that
neither of those covers cleanly. If A2 lands and the
.gastro/ tree stops being committed, much of the
"snippet feels like overkill" complaint disappears with it.
If items land in dependency order rather than priority order, the path through the document is roughly:
go generate reproducible.examples/, measure against library-mode equivalent, then decide.The C-section exists because a real downstream project ran a bake-off and framework mode lost. Gastro's design goals say framework mode should win that bake-off. There are two coherent responses:
Ship C1 (with C2/C3/C4/C5 in tow), accept that frontmatter becomes a small DSL, accept the "no new language" claim becomes "minimal new language". This is the path that honours the Astro-DX half of the pitch.
Ship D1, A4, A5, B2 cleanly. Document "use Gastro components as a typed Go API; bring your own router for mutating handlers" as a recommended shape. Pages become an opinionated convenience for read-heavy apps. This honours the idiomatic Go half.
The friction document, by listing C1 as P1, leans toward Option A. The effort assessment ("Large", "design work required", "not a quick fix") is the friction document quietly admitting Option A is the harder path. Either is internally consistent; the one mistake would be to spend the C1 effort halfway and ship a feature that doesn't make framework mode decisively better than library mode for the apps currently choosing library mode.