Plugin Progressive Enhancement

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-Versionbool
Version()X-Up-Versionstring
Target()X-Up-Targetstring — CSS selector
FailTarget()X-Up-Fail-Targetstring
Mode()X-Up-Modestring
FailMode()X-Up-Fail-Modestring
Validate()X-Up-Validatestring — field name
Context()X-Up-Contextmap[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-TargetOverride which fragment to swap
SetTitle()X-Up-TitleSet document title after update
RenderNothing()X-Up-Target: :noneSkip rendering, keep current page
EmitEvent()X-Up-EventsEmit custom DOM event on client
AcceptLayer()X-Up-Accept-LayerClose overlay with success value
DismissLayer()X-Up-Dismiss-LayerClose overlay with dismiss value
ExpireCache()X-Up-Expire-CacheExpire client cache by path pattern
SetContext()X-Up-ContextUpdate shared context across fragments

§ How It Works

1
Click / Form Submit

Unpoly intercepts link clicks and form submissions, adding X-Up-* request headers.

2
Server Middleware

The Nimbus middleware parses these headers and makes them available via unpoly.FromContext(c).

3
Fragment Response

Your controller renders the full page. Unpoly's client extracts only the targeted CSS selector and swaps it in the DOM.

4
SPA-like UX

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.