Unpoly — Progressive Enhancement
Unpoly is a progressive enhancement framework that lets you build fast, server-rendered applications with the feel of a single-page app. The Nimbus Unpoly plugin provides server-side protocol support — request detection, response helpers, and CDN configuration.
§ Overview
The Unpoly plugin handles the server side of the Unpoly protocol. It reads Unpoly's request headers and provides convenient helpers to set response headers, so you can:
- Detect Unpoly requests and only render the changed fragment
- Set the document title for fragment updates
- Emit events on the client from the server
- Accept or dismiss overlays (modals, drawers, popups)
- Expire cached content selectively
- Validate form fields live
§ Installation
$ nimbus plugin:install unpoly
Or add manually:
import "github.com/CodeSyncr/nimbus/plugins/unpoly"
app.Use(unpoly.New())
The plugin automatically injects the Unpoly CSS and JS from a CDN, and adds the server-protocol middleware to all routes.
§ CDN Configuration
package config
import "github.com/CodeSyncr/nimbus/plugins/unpoly"
var Unpoly = unpoly.Config{
// Unpoly version to load from CDN (default: "3.12.0")
Version: "3.12.0",
// Custom CDN URLs (optional — auto-generated from version)
CSSURL: "https://cdn.example.com/unpoly.min.css",
JSURL: "https://cdn.example.com/unpoly.min.js",
// Set to true to skip CDN injection (if you bundle Unpoly yourself)
DisableCDN: false,
}
§ Request Helpers
Detect Unpoly requests and read their metadata:
import "github.com/CodeSyncr/nimbus/plugins/unpoly"
func (ctrl *PagesController) Show(c *http.Context) error {
up := unpoly.FromContext(c)
// Is this an Unpoly request? (has X-Up-Version header)
if up.IsUnpoly() {
// What fragment is Unpoly targeting?
target := up.Target() // e.g. ".main-content"
failTarget := up.FailTarget() // target on error
// What mode is the request in?
mode := up.Mode() // "root", "modal", "drawer", "popup", "cover"
failMode := up.FailMode()
// Is this a form validation request?
if up.Validate() != "" {
// Only the named field is being validated
return c.JSON(map[string]any{
"valid": true,
})
}
// Read Unpoly context (shared state across fragments)
ctx := up.Context() // map[string]any from X-Up-Context
}
return c.View("pages/show", data)
}
| Method | Header | Returns |
|---|---|---|
IsUnpoly() | X-Up-Version | bool |
Version() | X-Up-Version | string |
Target() | X-Up-Target | string — CSS selector |
FailTarget() | X-Up-Fail-Target | string |
Mode() | X-Up-Mode | string |
FailMode() | X-Up-Fail-Mode | string |
Validate() | X-Up-Validate | string — field name |
Context() | X-Up-Context | map[string]any |
§ Response Helpers
Control Unpoly's client-side behavior from the server:
func (ctrl *FormsController) Submit(c *http.Context) error {
up := unpoly.FromContext(c)
// Change what fragment to update in the response
up.SetTarget(".notification-area")
// Set the document title after fragment update
up.SetTitle("Form Submitted — My App")
// Render nothing (dismiss without changing content)
up.RenderNothing()
// Emit a client-side event
up.EmitEvent("form:submitted", map[string]any{
"id": 42,
})
// Accept an overlay (close modal/drawer with a value)
up.AcceptLayer(map[string]any{
"user_id": 42,
"status": "created",
})
// Dismiss an overlay (close without a value)
up.DismissLayer(map[string]any{
"reason": "cancelled",
})
// Expire cached content matching a path pattern
up.ExpireCache("/users/*")
// Update shared context
up.SetContext(map[string]any{
"flash": "User created successfully",
})
return c.View("forms/success", nil)
}
| Method | Response Header | Effect |
|---|---|---|
SetTarget() | X-Up-Target | Override which fragment to swap |
SetTitle() | X-Up-Title | Set document title after update |
RenderNothing() | X-Up-Target: :none | Skip rendering, keep current page |
EmitEvent() | X-Up-Events | Emit custom DOM event on client |
AcceptLayer() | X-Up-Accept-Layer | Close overlay with success value |
DismissLayer() | X-Up-Dismiss-Layer | Close overlay with dismiss value |
ExpireCache() | X-Up-Expire-Cache | Expire client cache by path pattern |
SetContext() | X-Up-Context | Update shared context across fragments |
§ How It Works
Unpoly intercepts link clicks and form submissions, adding X-Up-* request headers.
The Nimbus middleware parses these headers and makes them available via unpoly.FromContext(c).
Your controller renders the full page. Unpoly's client extracts only the targeted CSS selector and swaps it in the DOM.
No full page reload, smooth transitions, URL and history updated — all with traditional server-rendered templates.
Tip: Unpoly works seamlessly with Nimbus's .nimbus template engine. You get SPA-like navigation and overlay modals with zero JavaScript bundling needed.