Frontend Craft — Gateway
Triage incoming Hotwire frontend requests, apply cross-cutting principles, and route to the right specialist skill. This skill owns no implementation cookbook — each specialist carries its own.
Routing Table
| Request Pattern | Route To |
|---|
| URL, history, frame navigation, Drive caching, rendering lifecycle, view transitions, page-refresh morphing | turbo-navigation-rendering
|
| Turbo Streams, broadcasting, stream morphing, optimistic state, real-time sync via broadcasting | |
| Stimulus controller design, lifecycle, DOM behavior, browser APIs | |
| Form submission, validation, autosave, inline edit, submit UX | |
| Media playback, gallery, upload preview, rich content integrations | |
| Native bridge, web/native boundary, path configuration | |
| Broad or ambiguous frontend request | Stay here — apply principles below, then route |
Core Workflow
Step 1: Classify the Problem
Determine the primary domain: navigation/rendering, streams/state sync, controller behavior, forms/submission, media UX, or native bridge. If the request clearly maps to one domain, route immediately.
Step 2: Apply Common Principles
Before routing, check whether these cross-cutting principles apply:
- Prefer built-in Turbo semantics first. Use attributes, frame , and stream actions before reaching for custom JavaScript.
- Decide ownership first. Every UI change must have a clear owner: a URL (Drive), a frame ID (Frames), or a stream target (Streams). Ambiguous ownership causes bugs.
- Avoid fixed timeouts as proxy for completion. Listen for lifecycle events (, ,
turbo:before-stream-render
) instead of .
- Validate back/forward/refresh behavior. Every navigation pattern must work correctly when the user presses back, forward, or refresh.
- Preserve focus and perceived continuity. Never suggest custom focus/caret/selection restoration without its completion binding. After a frame rerender, name or show
data-action="turbo:frame-render@document->focus-restore#restore"
(adapt the controller identifier to the implementation) and guard the intended frame. When the behavior depends on the response outcome, match the predicate to that behavior: use event.detail.fetchResponse.statusCode === 422
for validation-error focus restoration, and reserve event.detail.fetchResponse.succeeded
for success-only effects. If the binding and applicable guards are not carried through, omit the optional behavior. Frame updates must not disrupt scroll position.
- Design for idempotency and retry. Stream actions and form submissions must be safe to replay without duplication or corruption.
- Separate browser event concerns from Turbo lifecycle concerns. DOM events (click, input, resize) and Turbo events (turbo:load, turbo:before-cache) serve different purposes — do not conflate them.
- Classify stale navigation before prescribing cache controls. Distinguish an application-visit preview, a Turbo restoration visit, and browser bfcache; they do not share one lifecycle or fetch behavior.
Documentation accuracy: Start with the routed specialist's local, commit-pinned handbook plus its errata; these are the reproducible authority for this release. Use Context7 only to cross-check a version-sensitive API. If Context7 is unavailable or ambiguous, rely on the manifest-verified release tag/commit snapshot rather than an unpinned
page or memory. Examples in this plugin target Rails 8.1, Turbo 8, and turbo-rails 2.
Step 3: Resolve Overlap
When a request spans multiple skills, assign a lead skill:
| Overlap | Lead | Support |
|---|
| Form inside a lazy-loaded frame | | turbo-navigation-rendering
|
| Stream-driven form validation | | |
| Media upload with progress indicator | | |
| Optimistic UI with stream reconciliation | | turbo-navigation-rendering
|
| Frame navigation with an independent controller lifecycle/async constraint | turbo-navigation-rendering
| |
| Bridge component with form submission | | |
| CSS architecture for controller-driven UI | | (reference only) |
Page-refresh morphing (<meta name="turbo-refresh-method" content="morph">
) | turbo-navigation-rendering
| |
Stream-action morphing (turbo_stream.replace method: :morph
) | | turbo-navigation-rendering
|
| View transitions during page navigation (Drive visits, frame navigation) | turbo-navigation-rendering
| |
| View transitions triggered by stream updates (list animations, item add/remove) | | turbo-navigation-rendering
|
Rule: the skill that owns the hardest constraint (correctness, data integrity, platform boundary) leads.
List
as support only when the controller has an independent lifecycle, target, async-resource, or browser-API constraint; ordinary glue remains inside the lead specialist.
Routing Examples
| User Request | Classification | Route |
|---|
| "Add a modal that lets users edit their profile inline" | Form + frame boundary | Lead: , Support: turbo-navigation-rendering
|
| "Make the notification count update in real-time across tabs" | Real-time + cross-tab sync | |
| "Add a carousel for product images with swipe gestures" | Media rendering + library integration | |
Step 4: Route to Specialist
Hand off to the identified specialist skill by invoking it directly. Pass along any context from Step 2 (applicable principles) and Step 3 (lead/support assignment). The specialist will load its own references, handbook, and examples as needed. For multi-skill requests, invoke the lead skill — it will escalate to the support skill when needed.
Do not compress a specialist's correctness-sensitive distinctions into only a route name. A gateway triage or handoff must:
- Name the lead, any support roles that are present, and each relevant URL/frame/stream owner.
- Carry through exact statuses, visit types, and lifecycle event names when they affect correctness. If code shows a lifecycle handler, also name or show its binding.
- Keep the minimal Rails response/delivery contract in scope. Escalate deeper backend design only when the request actually implicates it.
- Self-check completion-dependent optional behavior: never leave focus/caret/selection restoration as a bare suggestion. For a frame rerender, carry through
data-action="turbo:frame-render@document->focus-restore#restore"
with its controller identifier adapted to the implementation, plus an intended frame guard. When response outcome matters, use the matching predicate: event.detail.fetchResponse.statusCode === 422
for validation-error focus restoration and event.detail.fetchResponse.succeeded
only for success-only effects. If the binding and applicable guards are not carried through, omit it; never invent a fixed delay.
- End with separate acceptance checks for each owner's state and failure path.
Step 5: Escalate Out of Scope
| Signal | Action |
|---|
| Request requires backend architecture beyond the response contract needed by Hotwire | Escalate that architecture work; keep the Turbo response contract in scope |
| Request requires native platform APIs beyond bridge components | Route to |
| Request is about deployment, CI, or infrastructure | Outside this plugin's scope |
| Request is about non-Hotwire JavaScript frameworks | Outside this plugin's scope |
Escalation Criteria
This plugin covers the Hotwire frontend layer: Turbo Drive, Turbo Frames, Turbo Streams, Stimulus, view-layer patterns, and the minimal Rails response contracts those features require (matching frame markup, Turbo Stream responses, redirect/status semantics, and broadcast declarations). It does not cover:
- Rails backend architecture beyond those response and delivery contracts
- Database design or migrations
- API design beyond Turbo Stream responses
- Non-Hotwire JavaScript frameworks (React, Vue, etc.)
- Infrastructure, deployment, or DevOps