Eurasia (in which headers become wisdom)
Previously on The Solid Convergence…
V4 had risen. The cp -R methodology harvested V3’s beauty without its baggage. Gaby made the login screen glow. The Lizard approved.
But the sidebar had secrets yet to reveal.
The Three Rivers Problem
09:00 — The shell worked. Three regions, three rivers of data:
┌─────────────────────────────────────────────────────────┐
│ TOPBAR - notifications stream in, transient │
├─────────────────────────────┬───────────────────────────┤
│ │ │
│ CHAT │ SIDEBAR │
│ - hours of conversation │ - credentials │
│ - context accumulates │ - agents │
│ - long-lived │ - stores │
│ │ - click click click │
│ │ │
└─────────────────────────────┴───────────────────────────┘
The Squirrel’s eyes lit up. “Signals! Each region needs signals! We can track—”
🦎 “What happens to the signals when the user clicks around the sidebar for two hours?”
The Squirrel paused. Its tiny paw, already reaching for the docs, froze mid-air.
The Accumulation
In v3’s world, signals live until the page dies. Beautiful for multi-page apps—each navigation is a fresh start. The browser’s refresh button is the garbage collector.
But Solid doesn’t refresh. The user opens the app Monday morning. Starts a chat. Opens the sidebar. Edits a credential—signals spawn. Navigates to agents—more signals. Back to credentials—more signals. The old ones don’t die. They can’t. The page never reloaded.
// Hour 1:
_solid.credential_1.name = "api-key"
_solid.credential_1.secret = "***"
// Hour 2 (navigated away and back):
_solid.credential_1.name = "api-key" // orphan
_solid.credential_1.secret = "***" // orphan
_solid.credential_2.name = "api-key" // new instance
_solid.credential_2.secret = "***" // new instance
// Hour 8:
// 847 orphaned signals
// "Why is the tab using 2GB of RAM?"
🐿️ “We could… namespace them? With components? And then manually clean—”
🦎 “Pretzel.”
“What?”
🦎 “You’re building a pretzel. Stop.”
The Choice
Three paths through the labyrinth:
- Fit the app to Signals — Page per config, lose the unified experience
- Build the pretzel — Manual signal lifecycle, component namespacing, cleanup on navigate
- Different tool — Let the DOM be the only client state
The Squirrel wanted door #2. It always wants door #2. Door #2 has abstractions.
🦎 “What does the server already know?”
The Server Knows
The server knows everything:
- Which sidebar mode (compact vs detailed)
- Which credential is being edited
- Whether there are unsaved changes
- What the breadcrumbs should say
The client just needs to tell the server what context it’s in. One header. One hint.
<body data-solid-sidebar="detailed">
// On every HTMX request, inject what we know
document.body.addEventListener('htmx:configRequest', (e) => {
for (const [key, value] of Object.entries(document.body.dataset)) {
if (key.startsWith('solid')) {
e.detail.headers[key] = value;
}
}
});
// Server reads it, renders accordingly
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
headers := make(map[string]string)
for name, values := range r.Header {
if strings.HasPrefix(strings.ToLower(name), "solid") {
headers[strings.ToLower(name)] = values[0]
}
}
ctx := WithSolidHeaders(r.Context(), headers)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
The Squirrel stared. “That’s… that’s just headers.”
🦎 “Poor man’s signals.”
The Idiomorph Gambit
But signals do something headers can’t: they update the UI reactively.
Or do they?
Signal way:
signal changes → DOM updates automatically
Solid way:
blur fires → PUT to server → server returns HTML → idiomorph merges
Same result. Different path. The server re-renders the whole editor—breadcrumbs, form, footer—and idiomorph figures out what changed.
// The editor container - morphs as a unit
templ Editor(cred *Credential, crumbs Breadcrumbs, form FormData) {
<div id="credential-editor" hx-swap="morph">
@Breadcrumbs(crumbs.WithStatus(form.Status()))
@Form(form)
@Footer(form)
</div>
}
When you blur a field:
- PUT fires with the new value
- Server saves to GitStore draft
- Server re-renders Editor with updated state
- Idiomorph merges: focus preserved, only changes applied
- Breadcrumb now says “Changed”, footer has “Save” button
No signals. No cleanup. The old DOM is gone. The new DOM is truth.
🐿️ “But what about the edited indicators? The little dots that show which fields changed?”
🦎 “Server knows. Server renders.”
func (h *Handlers) SaveFieldDraft(w http.ResponseWriter, r *http.Request) {
// Save the draft
h.store.SaveDraft(id, field, value)
// Re-render the whole editor with new state
Editor(cred, breadcrumbs, formData).Render(r.Context(), w)
}
The Resizable Revelation
14:00 — The sidebar needed to resize. Drag a divider, change the width. Simple enough.
But the sidebar morphs. New content swaps in. CSS variables on the sidebar element would die with each morph.
🐿️ “State! We need to persist the—”
🦎 “Put it on the parent.”
// Width lives on #app, not #sidebar
app.style.setProperty('--sidebar-user-width', `${width}px`);
The sidebar morphs. The width survives. CSS variables on ancestors outlive their children.
15:00 — But compact mode shows icons. Detailed mode shows full content. The server needs to know which to render.
// When width crosses threshold, update body
document.body.dataset.solidSidebar = width > threshold ? 'detailed' : 'compact';
// Trigger a refresh
htmx.trigger(document.body, 'sidebar:refresh');
The sidebar re-fetches itself. The solidSidebar: detailed header flows through. The server renders the detailed view with stats widgets and full labels.
No signals. Just headers. Poor man’s signals.
The Tally
Signals accumulated: 0
Headers convention: data-solid-* → solidCamelCase → context
Morph target: #credential-editor (breadcrumbs + form + footer)
Width persistence: CSS variable on #app
Mode detection: 30% viewport threshold
Stats widget: Visible in detailed mode only
What survived from v3:
- Morphs (idiomorph)
- Server-sent events (hx-ext="sse")
- Server is truth philosophy
What we left behind:
- Client signals
- Component namespacing
- Signal lifecycle management
- The pretzel
The Convention
HTML: data-solid-sidebar="compact"
JS: document.body.dataset.solidSidebar
Header: solidSidebar: compact
Go: GetSolidHeader(ctx, HeaderSidebar)
Adding a new “signal”:
- Set
data-solid-foo="bar"on body - Read
GetSolidHeader(ctx, "solidFoo")in handler - There is no step 3
🦎 “The best abstraction is the one you don’t think about.”
The Moral
We loved v3. We really did. The morphs were beautiful. The signals were elegant. The SSE integration was first-class.
But v3 was built for a world where pages reload. Where signals die naturally. Where the garbage collector is the browser’s refresh button.
Solid lives in a different world. Three rivers. Hours of context. A sidebar that clicks a thousand times.
So we kept what worked:
- Morphs → idiomorph
- SSE → hx-ext=“sse”
- Server truth → even more server truth
And replaced what didn’t:
- Signals → headers (poor man’s signals)
- Components → well-known morph targets
- Client state → what client state?
The Squirrel still twitches when it sees data-solid-*. “It’s not reactive,” it whispers.
🦎 “It’s reactive enough. The server reacts. The DOM updates. The user doesn’t know the difference.”
“But—”
🦎 “Fuckit.”
🐿️☕🦎
Post-credits scene:
The sidebar resizes smoothly. The stats widget appears at 30% width.
Somewhere in the internets, someone asks: “How do I persist state across morphs?”
A lizard appears in the thread.
“Put it on the parent. Or in a header. Or both.”
The questioner is confused. “But what about signals?”
“What signals?”
The lizard vanishes.
The sidebar keeps working.
See also:
The Saga (in which we learn by doing):
- The Solid Convergence — V4 rises from V3’s ashes
- The V3 Saga Final Chapter - Is It Fun To Fight Windmills — “Is it fun to fight windmills?”
- The Labyrinth of a Hundred Forms — Interfaces defeat squirrels
- The Lizard Brain vs The Caffeinated Squirrel — “fuckit” becomes philosophy
The Changelog (what actually shipped):
db3835efeat(uimode): Generic solid* header convention36b5759feat(shell): Add resizable sidebar with drag divider33ff380feat(credential): Add state-aware editor containerf527275feat(credential): Add idiomorph form re-render on blur
The References (standing on shoulders):
- Idiomorph — The morph algorithm that makes this possible
