datastar

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 18 Imported by: 0

README

go-datastar

Go Reference CI Go Report Card MIT License

DataStar protocol library for Go. Every patch is a first-class value that produces an sse.Event — so you can store, queue, filter, replay, and broadcast them through go-sse's transport infrastructure.

Built on go-sse.

What is this?

DataStar is a hypermedia framework that uses Server-Sent Events to push DOM patches, signal updates, and script execution from server to browser. go-datastar provides the Go server-side protocol vocabulary.

The key design principle: patches are values, not method calls. Every patch implements Patch interface { Event() sse.Event }, so you can construct one without an open connection, hand it to a Broadcaster[T], persist it in an EventStore, or filter it through a SubscribeFilter.

Why not starfederation/datastar-go?

The upstream SDK couples patch construction to a live SSE connection (PatchElements is a method on ServerSentEventGenerator). Patches are not values — you cannot queue, filter, replay, or broadcast them. go-datastar fixes this with a one-method interface that unlocks composition with go-sse.

Requirements

  • Go 1.26+
  • GOEXPERIMENT=jsonv2 environment variable (required transitively via go-branded-id through go-sse)
# All go commands need this:
GOEXPERIMENT=jsonv2 go build ./...
GOEXPERIMENT=jsonv2 go test ./... -race -count=1

Install

go get github.com/larsartmann/go-datastar

Optional sub-modules (separately versioned):

go get github.com/larsartmann/go-datastar/static        # embedded JS client bundle (zero deps)
go get github.com/larsartmann/go-datastar/datastartest  # E2E test helpers for consumer handlers

Quick start

Patches as values — broadcast to many connections

This is the core pattern that distinguishes go-datastar from the upstream SDK. Construct a patch without a connection, then broadcast it:

broadcaster := sse.NewBroadcaster[sse.Event]()

patch := datastar.NewElementsPatch("<div>Update</div>",
    datastar.WithSelectorID("feed"),
    datastar.WithModePrepend(),
)

// Every subscriber receives the same sse.Event
broadcaster.Broadcast(patch.Event())
Single-request response — fluent builder

For the common case of sending patches on a single HTTP connection, Response wraps a stream and provides fluent methods:

func handler(w http.ResponseWriter, r *http.Request) {
    stream := sse.NewStream(w, r)
    defer func() { _ = stream.Close() }()

    resp := datastar.NewResponse(stream)

    if err := resp.PatchElements("<div>Hello</div>", datastar.WithSelector("#feed")); err != nil {
        log.Printf("patch elements: %v", err)
        return
    }

    if err := resp.MarshalAndPatchSignals(map[string]any{"count": 1}); err != nil {
        log.Printf("patch signals: %v", err)
    }
}
Serve the DataStar JS client
mux.Handle("GET /datastar.js", datastar.ScriptHandler())
Full working example

A complete live-feed application is in example/:

go run ./example/
# Open http://localhost:8765

Core concept: the Patch interface

type Patch interface {
    Event() sse.Event
}

Four types implement this interface. Everything else is a convenience constructor:

Type What it does
ElementsPatch Merge HTML elements into the DOM
SignalsPatch Update reactive signals
ScriptPatch Execute JavaScript on the client
DispatchCustomEventPatch Dispatch a custom DOM event

Patch constructors

Elements
Constructor Returns Notes
NewElementsPatch(html, opts...) ElementsPatch Core element patch
NewRemovePatch(selector) ElementsPatch Remove element by CSS selector
NewRemoveByIDPatch(id) ElementsPatch Remove element by ID
ElementsFromTempl(component, opts...) (ElementsPatch, error) Render a Templ component
ElementsFromGostar(renderer, opts...) (ElementsPatch, error) Render a GoStar element
Signals
Constructor Returns Notes
NewSignalsPatch(v, opts...) (SignalsPatch, error) Marshal a Go value to signals JSON
NewSignalsIfMissingPatch(v, opts...) (SignalsPatch, error) Only set signals that don't exist

Pre-encoded JSON? Construct directly: datastar.SignalsPatch{Signals: []byte("{\"count\":1}")}.

Script execution
Constructor Returns Notes
NewScriptPatch(js, opts...) ScriptPatch Core script patch
NewRedirectPatch(url) ScriptPatch Redirect the browser
NewConsoleLogPatch(msg) ScriptPatch console.log on the client
NewConsoleErrorPatch(err) ScriptPatch console.error on the client
NewReplaceURLPatch(url) ScriptPatch history.replaceState
NewPrefetchPatch(urls...) ScriptPatch Speculation-rules prefetch
NewDispatchCustomEventPatch(name, detail) (DispatchCustomEventPatch, error) Dispatch a custom DOM event
Printf-style variants

WithSelectorf, NewRedirectfPatch, NewConsoleLogfPatch — same behavior with fmt.Sprintf formatting.

Options

Elements

WithSelector, WithSelectorID, WithSelectorf, WithMode, WithNamespace, WithViewTransitions, WithViewTransitionSelector, WithElementsEventID, WithElementsRetryDuration

Mode sugar: WithModeOuter, WithModeInner, WithModeRemove, WithModeReplace, WithModePrepend, WithModeAppend, WithModeBefore, WithModeAfter

Namespace sugar: WithNamespaceHTML, WithNamespaceSVG, WithNamespaceMathML

Signals

WithOnlyIfMissing, WithSignalsEventID, WithSignalsRetryDuration

Script

WithScriptAutoRemove, WithScriptAttributes, WithScriptAttributeKVs, WithScriptEventID, WithScriptRetryDuration

Custom events

WithCustomEventSelector, WithCustomEventBubbles, WithCustomEventCancelable, WithCustomEventComposed, WithCustomEventEventID

Response builder

Response wraps an sse.Stream for single-connection patching. Every method returns error:

Method Description
PatchElements(html, opts) Send an ElementsPatch
PatchElementsTempl(c, opts) Render + send a Templ component
PatchSignals(json, opts) Send pre-encoded JSON signals
MarshalAndPatchSignals(v, opts) Marshal + send a Go value
RemoveElement(selector) Remove element by selector
RemoveElementByID(id) Remove element by ID
ExecuteScript(js, opts) Send a ScriptPatch
Redirect(url, opts) Redirect the browser
ConsoleLog(msg, opts) console.log on the client
ConsoleError(err, opts) console.error on the client
DispatchCustomEvent(name, detail, opts) Dispatch a custom DOM event
ReplaceURL(url, opts) history.replaceState
Prefetch(urls...) Speculation-rules prefetch
ApplyPatches(patches...) Send multiple patches in sequence
Send(evt) Send a raw sse.Event
Stream() Access the underlying sse.Stream

Convenience constructors:

  • NewResponse(stream) — wrap an existing stream
  • NewResponseFromHTTP(w, r) — create stream + response in one call
  • ErrorResponse(stream, message, code) — signals patch with error info
  • ErrorResponseFromError(stream, err) — signals patch with errorfamily metadata extracted from a Go error (code, family, retryable, httpStatus)
  • NotificationResponse(stream, message, kind) — signals patch with notification

Inbound: reading signals

var signals struct {
    Email string `json:"email"`
}
if err := datastar.ReadSignals(r, &signals); err != nil {
    log.Printf("read signals: %v", err)
}
  • ReadSignals(r, &target) — extracts signals from ?datastar= query param (GET/DELETE) or JSON body (all other methods). Returns nil if no signals present.
  • LastEventID(r) — extracts the last event ID from the Last-Event-ID header or lastEventId query param (for SSE reconnection replay).

HTTP helpers

DataStar action attribute strings for use in HTML:

datastar.GetSSE("/api/feed")
datastar.PostSSE("/api/items/%d", id)
datastar.PutSSE("/api/settings")
datastar.PatchSSE("/api/merge")
datastar.DeleteSSE("/api/items/%d", id)

Serving the JS client

Function Description
ScriptHandler() Serve the embedded DataStar JS (v1.0.2) with ETag + Cache-Control
ScriptHandlerWith(js, ver) Serve a custom JS bundle
ScriptTag(path) HTML <script type="module"> tag string
Version() Embedded JS client version string

Event store (reconnection replay)

MemoryStore is an in-memory ring buffer implementing sse.EventStore. It keeps the last N events so reconnecting clients can replay missed patches:

store := datastar.NewMemoryStore(128) // keep last 128 events

// In your producer:
store.Append(patch.Event())

// go-sse uses it for automatic replay on reconnection.

For multi-instance deployments, implement sse.EventStore against a shared backend (Redis, Postgres).

Testing your handlers

The datastartest subpackage provides E2E test helpers that parse SSE responses and decode DataStar datalines into typed values — so you can assert on patches without hand-rolling wire-format parsing:

import "github.com/larsartmann/go-datastar/datastartest"

func TestFeedHandler(t *testing.T) {
    events := datastartest.Collect(t, myHandler)
    datastartest.RequireEventCount(t, events, 2)

    // Elements: typed accessors decode the datalines
    datastartest.RequireElements(t, events[0], "#feed", "append", "<div>hello</div>")

    // Signals: unmarshal JSON into a struct
    var data struct{ Count int `json:"count"` }
    _ = events[1].UnmarshalSignals(&data)
}
POST requests

For POST/PUT/PATCH handlers with request bodies, use CollectPost or CollectWithRequest:

// POST with JSON body (most common pattern)
events := datastartest.CollectPost(t, handler, `{"name":"alice"}`)

// Or any method with custom content type
events := datastartest.CollectWithRequest(t, handler, http.MethodPut, body, "application/json")
Streaming handlers

For handlers that keep the connection open (e.g., broadcasting), use CollectN to read exactly N events, or CollectWithTimeout for a time-bounded read:

// Read exactly 3 events then close
events := datastartest.CollectN(t, streamingHandler, 3)

// Or read everything within a deadline (defensive against hung handlers)
events := datastartest.CollectWithTimeout(t, handler, 5*time.Second)
Script patches

Script patches (ExecuteScript, Redirect, ConsoleLog, etc.) wrap JS in <script> tags. Use IsScript() and ScriptContent() to extract and assert on the JavaScript:

events := datastartest.Collect(t, handler)

if events[0].IsScript() {
    js := events[0].ScriptContent() // "console.log('hello')"
}
Search helpers

When a handler sends multiple patches, use FindElement and FindSignals to locate specific events without indexing by position:

evt, ok := datastartest.FindElement(events, "#header")
sigEvt, ok := datastartest.FindSignals(events)

Error handling

Every error returned by go-datastar is a classified *errorfamily.Error carrying a stable code, a behavioral family, and structured context.

Three ways to handle errors
// 1. By code (stable string):
if errorfamily.Code(err) == datastar.CodeSignalsMarshalFailed { ... }

// 2. By sentinel (errors.Is matches by code+family):
if errors.Is(err, datastar.ErrEventNameRequired) { ... }

// 3. By family (behavioral — retryable? whose fault?):
if errorfamily.Classify(err) == errorfamily.Transient { /* backoff + retry */ }
Families
Family When Retryable HTTP Status
Rejection Bad or missing caller input no 400
Transient Temporary I/O failure reading body yes 503
Orchestration Internal render failure (templ, gostar) no 500
Error codes
Code Family Retryable
datastar.templ_render_failed Orchestration no
datastar.gostar_render_failed Orchestration no
datastar.body_read_after_close Rejection no
datastar.body_read_failed Transient yes
datastar.signals_unmarshal_failed Rejection no
datastar.signals_marshal_failed Rejection no
datastar.custom_event_detail_marshal_failed Rejection no
datastar.event_name_required Rejection no
datastar.element_patch_mode_invalid Rejection no
datastar.namespace_invalid Rejection no
datastar.stream_send_failed Transient yes

Wire format parity

go-datastar reproduces the exact DataStar wire format expected by the DataStar JavaScript client. Mode outer is never emitted (default). Namespace html is never emitted (default). Retry is emitted only when it deviates from the 1000ms default. Data-line construction order, script wrapping, and signal splitting all match the upstream SDK.

Companion libraries

  • go-sse — SSE transport (Stream, Broadcaster, EventStore, Replay, Heartbeat)
  • go-error-family — structured error classification

License

MIT

Documentation

Overview

Package datastar implements the DataStar protocol layer on top of go-sse.

DataStar is a hypermedia framework that uses Server-Sent Events (SSE) to push DOM patches, signal updates, and script execution commands from the server to the browser. This library provides the Go server-side vocabulary for that protocol.

Patches are values

The core design principle is that patches are first-class values, not method calls on a live connection. Every patch implements the Patch interface:

type Patch interface {
    Event() sse.Event
}

This means patches can be stored, queued, filtered, replayed, and broadcast using go-sse's Broadcaster[T], EventStore, SubscribeFilter, and Shutdown infrastructure — none of which is possible with the upstream starfederation/datastar-go SDK, where patches are methods bound to a live SSE generator.

Quick start

import (
    "github.com/larsartmann/go-datastar"
    "github.com/larsartmann/go-sse"
)

// Broadcast patches to multiple connections
broadcaster := sse.NewBroadcaster[sse.Event]()

// Create a patch as a value
patch := datastar.NewElementsPatch("<div>Hello</div>",
    datastar.WithSelector("#feed"),
    datastar.WithMode(datastar.ElementPatchModeInner),
)

// Broadcast it — every subscriber receives the same sse.Event
broadcaster.Broadcast(patch.Event())

Wire format parity

go-datastar reproduces the exact DataStar wire format expected by the DataStar JavaScript client. The data-line construction order, mode/namespace gating, retry logic, and script wrapping all match the upstream SDK behavior.

Classified errors

Every error returned by this library is a classified *errorfamily.Error carrying a stable machine-readable code, a behavioral family (Rejection, Transient, Orchestration), and structured context. Consumers can match by code, sentinel, or family:

// By stable code:
if errorfamily.Code(err) == datastar.CodeSignalsMarshalFailed { ... }

// By sentinel (errors.Is matches by code+family):
if errors.Is(err, datastar.ErrEventNameRequired) { ... }

// By behavioral family (retryable? whose fault?):
if errorfamily.IsRetryable(err) { /* backoff + retry */ }

See the Error System section in AGENTS.md for the full catalog.

Companion to go-sse

go-datastar depends on go-sse for the SSE transport layer (Stream, Broadcaster, EventStore, Replay). go-sse owns the wire format; go-datastar owns the protocol vocabulary. This separation keeps each library focused and composable.

Example (ErrorHandlingByCode)

Example_errorHandlingByCode shows how to match errors by their stable code. Codes are string constants that never change between versions, making them safe for programmatic branching.

package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/larsartmann/go-datastar"
	errorfamily "github.com/larsartmann/go-error-family"
)

func main() {
	// Simulate a request with malformed JSON signals
	req := httptest.NewRequestWithContext(
		context.Background(), http.MethodPost, "/", strings.NewReader("{bad json"))

	var target map[string]any

	err := datastar.ReadSignals(req, &target)

	switch errorfamily.Code(err) {
	case datastar.CodeSignalsUnmarshalFailed:
		fmt.Println("bad JSON — ask the user to fix their input")
	case datastar.CodeBodyReadFailed:
		fmt.Println("transient I/O — safe to retry")
	default:
		fmt.Println("unexpected error")
	}
}
Output:
bad JSON — ask the user to fix their input
Example (ErrorHandlingByFamily)

Example_errorHandlingByFamily shows how to use the behavioral family to decide whether to retry. Transient errors (temporary I/O) are safe to retry with backoff; Rejection errors are not (the caller's input is wrong).

package main

import (
	"fmt"

	"github.com/larsartmann/go-datastar"
	errorfamily "github.com/larsartmann/go-error-family"
)

func main() {
	// MarshalSignals with an unmarshallable value (channel)
	_, err := datastar.MarshalSignals(make(chan int))

	if errorfamily.IsRetryable(err) {
		fmt.Println("retrying...")
	} else {
		fmt.Println("not retryable — fix the input")
	}
}
Output:
not retryable — fix the input
Example (ErrorHandlingBySentinel)

Example_errorHandlingBySentinel shows how to match errors by sentinel value. errors.Is works because errorfamily compares by (code, family), so even a context-enriched clone of a sentinel still matches.

package main

import (
	"errors"
	"fmt"

	"github.com/larsartmann/go-datastar"
)

func main() {
	_, err := datastar.NewDispatchCustomEventPatch("", nil)

	if errors.Is(err, datastar.ErrEventNameRequired) {
		fmt.Println("eventName is required")
	}
}
Output:
eventName is required

Index

Examples

Constants

View Source
const (
	SelectorDatalineKey               = "selector "
	ModeDatalineKey                   = "mode "
	NamespaceDatalineKey              = "namespace "
	UseViewTransitionDatalineKey      = "useViewTransition "
	ViewTransitionSelectorDatalineKey = "viewTransitionSelector "
	ElementsDatalineKey               = "elements "
	SignalsDatalineKey                = "signals "
	OnlyIfMissingDatalineKey          = "onlyIfMissing "
)

Dataline key constants. These have a trailing space baked in, matching the DataStar wire format (e.g., "selector #feed" is emitted as a data line).

View Source
const (
	// CodeTemplRenderFailed: a [TemplComponent] failed to render to HTML.
	CodeTemplRenderFailed = "datastar.templ_render_failed"

	// CodeGostarRenderFailed: a [GoStarElementRenderer] failed to render to HTML.
	CodeGostarRenderFailed = "datastar.gostar_render_failed"

	// CodeBodyReadAfterClose: [ReadSignals] read the request body after it was
	// already closed, typically because an SSE stream consumed it first.
	CodeBodyReadAfterClose = "datastar.body_read_after_close"

	// CodeBodyReadFailed: [ReadSignals] could not read the request body.
	CodeBodyReadFailed = "datastar.body_read_failed"

	// CodeSignalsUnmarshalFailed: the inbound signals JSON could not be
	// unmarshaled into the caller's target.
	CodeSignalsUnmarshalFailed = "datastar.signals_unmarshal_failed"

	// CodeSignalsMarshalFailed: a Go value could not be marshaled to JSON for a
	// signals patch (e.g. a channel, function, or cyclic reference).
	CodeSignalsMarshalFailed = "datastar.signals_marshal_failed"

	// CodeEventNameRequired: [NewDispatchCustomEventPatch] was called with an
	// empty event name.
	CodeEventNameRequired = "datastar.event_name_required"

	// CodeCustomEventDetailMarshalFailed: [NewDispatchCustomEventPatch] could not
	// marshal the detail value to JSON (e.g. a channel, function, or cyclic reference).
	CodeCustomEventDetailMarshalFailed = "datastar.custom_event_detail_marshal_failed"

	// CodeElementPatchModeInvalid: [ElementPatchModeFromString] received an
	// unrecognized mode string.
	CodeElementPatchModeInvalid = "datastar.element_patch_mode_invalid"

	// CodeNamespaceInvalid: [NamespaceFromString] received an unrecognized
	// namespace string.
	CodeNamespaceInvalid = "datastar.namespace_invalid"

	// CodeStreamSendFailed: [Response] could not deliver an SSE event to the
	// underlying stream. Wraps any error returned by the transport.
	CodeStreamSendFailed = "datastar.stream_send_failed"
)

Error codes for go-datastar. Each is a stable string accessible via errorfamily.Code, enabling programmatic handling, metrics, and structured logging without string matching on human-readable messages.

View Source
const DatastarJSVersion = static.Version

DatastarJSVersion is the version of the embedded DataStar JavaScript client. It re-exports static.Version for backward compatibility with the root API.

View Source
const DatastarKey = "datastar"

DatastarKey is the query parameter key for DataStar signals on GET/DELETE requests.

View Source
const DefaultMemoryStoreCapacity = 128

DefaultMemoryStoreCapacity is the default number of events retained for reconnection replay when no capacity is specified.

View Source
const DefaultRetryDuration = 1000 * time.Millisecond

DefaultRetryDuration is the default SSE retry interval the DataStar client uses after a connection reset (1 second). Patches that leave RetryDuration at this value do not emit a retry field; only deviations from this default are sent on the wire.

Variables

View Source
var (
	// ErrBodyReadAfterClose is returned by [ReadSignals] when the request body
	// was already closed before reading. This usually means an SSE stream was
	// created before ReadSignals ran; re-order so ReadSignals reads the body
	// first. The underlying [http.ErrBodyReadAfterClose] is preserved as the cause.
	ErrBodyReadAfterClose = errorfamily.WrapRejection(
		http.ErrBodyReadAfterClose,
		CodeBodyReadAfterClose,
		"request body already closed (create the SSE stream after calling ReadSignals)",
	)

	// ErrEventNameRequired is returned by [NewDispatchCustomEventPatch] when the
	// event name argument is empty.
	ErrEventNameRequired = errorfamily.NewRejection(
		CodeEventNameRequired,
		"eventName is required",
	)
)

Sentinel errors for fixed-message failure modes. Match with errors.Is:

if errors.Is(err, datastar.ErrEventNameRequired) { ... }

errorfamily errors compare by (code, family), so a context-enriched clone returned by errorfamily.Error.WithContext still satisfies errors.Is against the sentinel. Dynamic-message errors (render failures, parse failures with the offending value) are constructed inline at their call site using the code constants above.

ValidElementPatchModes lists all valid element patch modes. It is a public package-level slice so callers can iterate the set without depending on the private definition. Treated as immutable.

ValidNamespaces lists all valid namespaces. It is a public package-level slice so callers can iterate the set without depending on the private definition. Treated as immutable.

Functions

func DeleteSSE

func DeleteSSE(urlFormat string, args ...any) string

DeleteSSE generates a DataStar @delete SSE action attribute string.

func ErrorResponse

func ErrorResponse(stream *sse.Stream, message string, code string) error

ErrorResponse sends a signals patch with error information that the DataStar client can display.

func ErrorResponseFromError added in v0.0.3

func ErrorResponseFromError(stream *sse.Stream, err error) error

ErrorResponseFromError sends a signals patch with error metadata extracted from a Go error using errorfamily classification. The payload includes the error message, stable code, behavioral family, retryability, and the HTTP status code that the family maps to — giving the DataStar client enough context to render an appropriate error UI.

For non-errorfamily errors, code will be empty and Classify defaults to Transient (fail-open for retry), so family will be "transient", retryable will be true, and HTTPStatus will be 503.

func GetSSE

func GetSSE(urlFormat string, args ...any) string

GetSSE generates a DataStar @get SSE action attribute string.

func LastEventID

func LastEventID(req *http.Request) sse.EventID

LastEventID extracts the last event ID from an HTTP request. It checks the standard "Last-Event-ID" header first, then falls back to the "lastEventId" query parameter (which the DataStar JS client sends on reconnection).

Returns an empty sse.EventID if no event ID is present.

func MarshalSignals

func MarshalSignals(v any) ([]byte, error)

MarshalSignals marshals a Go value to JSON for use as a DataStar signals payload. Returns an error instead of panicking.

func NotificationResponse

func NotificationResponse(stream *sse.Stream, message string, kind string) error

NotificationResponse sends a signals patch with a notification message.

func PatchSSE

func PatchSSE(urlFormat string, args ...any) string

PatchSSE generates a DataStar @patch SSE action attribute string.

func PostSSE

func PostSSE(urlFormat string, args ...any) string

PostSSE generates a DataStar @post SSE action attribute string.

func PutSSE

func PutSSE(urlFormat string, args ...any) string

PutSSE generates a DataStar @put SSE action attribute string.

func ReadSignals

func ReadSignals(req *http.Request, signals any) error

ReadSignals extracts DataStar signals from an HTTP request and unmarshals them into the signals target (a pointer to a struct).

For GET and DELETE requests, signals are read from the "datastar" query parameter. For all other methods, signals are read from the JSON request body.

Returns nil (with no data written) if no signals are present (empty query param or empty body).

Example

ExampleReadSignals demonstrates extracting signals from an inbound request body.

package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/larsartmann/go-datastar"
)

func main() {
	req := httptest.NewRequestWithContext(
		context.Background(),
		http.MethodPost,
		"/api",
		strings.NewReader(`{"count":2}`),
	)

	var signals struct {
		Count int `json:"count"`
	}
	if err := datastar.ReadSignals(req, &signals); err != nil {
		return
	}

	fmt.Println(signals.Count)
}
Output:
2

func ScriptHandler

func ScriptHandler() http.Handler

ScriptHandler returns an http.Handler that serves the embedded DataStar JavaScript client bundle with correct Content-Type, ETag, and Cache-Control headers. Only GET and HEAD requests are allowed; all others return 405.

func ScriptHandlerWith

func ScriptHandlerWith(scriptBytes []byte, _ string) http.Handler

ScriptHandlerWith returns an http.Handler that serves a custom JavaScript bundle. Use this to serve a different version of the DataStar client.

func ScriptTag

func ScriptTag(path string) string

ScriptTag returns an HTML <script> tag that loads the DataStar client from the given path.

func Version

func Version() string

Version returns the version of the embedded DataStar JavaScript client.

Types

type DispatchCustomEventOption

type DispatchCustomEventOption func(*DispatchCustomEventPatch)

DispatchCustomEventOption configures a DispatchCustomEventPatch.

func WithCustomEventBubbles

func WithCustomEventBubbles(b bool) DispatchCustomEventOption

WithCustomEventBubbles overrides the default bubbling (true).

func WithCustomEventCancelable

func WithCustomEventCancelable(b bool) DispatchCustomEventOption

WithCustomEventCancelable overrides the default cancelability (true).

func WithCustomEventComposed

func WithCustomEventComposed(b bool) DispatchCustomEventOption

WithCustomEventComposed overrides the default composed flag (true).

func WithCustomEventEventID

func WithCustomEventEventID(id string) DispatchCustomEventOption

WithCustomEventEventID sets the SSE event ID.

func WithCustomEventSelector

func WithCustomEventSelector(s string) DispatchCustomEventOption

WithCustomEventSelector replaces the default target (document) with a CSS selector.

type DispatchCustomEventPatch

type DispatchCustomEventPatch struct {
	EventName string
	Detail    any

	Selector   string
	Bubbles    bool
	Cancelable bool
	Composed   bool

	EventID       string
	RetryDuration int64 // milliseconds; 0 = default
	// contains filtered or unexported fields
}

DispatchCustomEventPatch dispatches a custom DOM event on the client via script execution. The detail value is marshaled to JSON in NewDispatchCustomEventPatch and passed as the event's detail property.

func NewDispatchCustomEventPatch

func NewDispatchCustomEventPatch(
	eventName string,
	detail any,
	opts ...DispatchCustomEventOption,
) (DispatchCustomEventPatch, error)

NewDispatchCustomEventPatch creates a DispatchCustomEventPatch with the given event name and detail value. The detail is marshaled to JSON when DispatchCustomEventPatch.Event is called.

func (DispatchCustomEventPatch) Event

Event returns the sse.Event for this custom event dispatch.

type ElementPatchMode

type ElementPatchMode string

ElementPatchMode controls how an element is merged into the DOM.

const (
	// DefaultElementPatchMode is the default mode (outer).
	DefaultElementPatchMode ElementPatchMode = ElementPatchModeOuter

	// ElementPatchModeOuter morphs the element into the existing element.
	ElementPatchModeOuter ElementPatchMode = "outer"

	// ElementPatchModeInner replaces the inner HTML of the existing element.
	ElementPatchModeInner ElementPatchMode = "inner"

	// ElementPatchModeRemove removes the existing element.
	ElementPatchModeRemove ElementPatchMode = "remove"

	// ElementPatchModeReplace replaces the existing element with the new element.
	ElementPatchModeReplace ElementPatchMode = "replace"

	// ElementPatchModePrepend prepends the element inside the existing element.
	ElementPatchModePrepend ElementPatchMode = "prepend"

	// ElementPatchModeAppend appends the element inside the existing element.
	ElementPatchModeAppend ElementPatchMode = "append"

	// ElementPatchModeBefore inserts the element before the existing element.
	ElementPatchModeBefore ElementPatchMode = "before"

	// ElementPatchModeAfter inserts the element after the existing element.
	ElementPatchModeAfter ElementPatchMode = "after"
)

func ElementPatchModeFromString

func ElementPatchModeFromString(modeStr string) (ElementPatchMode, error)

ElementPatchModeFromString converts a string to an ElementPatchMode. Returns an error for invalid mode strings.

type ElementPatchOption

type ElementPatchOption func(*ElementsPatch)

ElementPatchOption configures an ElementsPatch.

func WithElementsEventID

func WithElementsEventID(id string) ElementPatchOption

WithElementsEventID sets the SSE event ID for the element patch.

func WithElementsRetryDuration

func WithElementsRetryDuration(d time.Duration) ElementPatchOption

WithElementsRetryDuration overrides the SSE retry duration for the element patch.

func WithMode

func WithMode(mode ElementPatchMode) ElementPatchOption

WithMode overrides the DefaultElementPatchMode for the element.

func WithModeAfter

func WithModeAfter() ElementPatchOption

WithModeAfter creates an option that inserts after the target element.

func WithModeAppend

func WithModeAppend() ElementPatchOption

WithModeAppend creates an option that appends inside the target element.

func WithModeBefore

func WithModeBefore() ElementPatchOption

WithModeBefore creates an option that inserts before the target element.

func WithModeInner

func WithModeInner() ElementPatchOption

WithModeInner creates an option that replaces the inner HTML of the target.

func WithModeOuter

func WithModeOuter() ElementPatchOption

WithModeOuter creates an option that uses the outer merge mode (default).

func WithModePrepend

func WithModePrepend() ElementPatchOption

WithModePrepend creates an option that prepends inside the target element.

func WithModeRemove

func WithModeRemove() ElementPatchOption

WithModeRemove creates an option that removes the target element.

func WithModeReplace

func WithModeReplace() ElementPatchOption

WithModeReplace creates an option that replaces the target element without morphing.

func WithNamespace

func WithNamespace(ns Namespace) ElementPatchOption

WithNamespace specifies the XML namespace for the element.

func WithNamespaceHTML

func WithNamespaceHTML() ElementPatchOption

WithNamespaceHTML sets the namespace to HTML (the default — usually a no-op).

func WithNamespaceMathML

func WithNamespaceMathML() ElementPatchOption

WithNamespaceMathML sets the namespace to MathML.

func WithNamespaceSVG

func WithNamespaceSVG() ElementPatchOption

WithNamespaceSVG sets the namespace to SVG.

func WithSelector

func WithSelector(selector string) ElementPatchOption

WithSelector sets the CSS selector for the element patch target.

func WithSelectorID

func WithSelectorID(id string) ElementPatchOption

WithSelectorID is a convenience for WithSelector("#" + id).

func WithSelectorf

func WithSelectorf(format string, args ...any) ElementPatchOption

WithSelectorf is a printf-style variant of WithSelector.

func WithViewTransitionSelector

func WithViewTransitionSelector(selector string) ElementPatchOption

WithViewTransitionSelector scopes the view transition to a CSS selector.

func WithViewTransitions

func WithViewTransitions(enable bool) ElementPatchOption

WithViewTransitions enables the View Transition API for the merge.

func WithViewTransitionsEnabled

func WithViewTransitionsEnabled() ElementPatchOption

WithViewTransitionsEnabled is shorthand for WithViewTransitions(true).

func WithoutViewTransitions

func WithoutViewTransitions() ElementPatchOption

WithoutViewTransitions is shorthand for WithViewTransitions(false).

type ElementsPatch

type ElementsPatch struct {
	// Selector is the CSS selector for the target element.
	// Empty means no selector data line is emitted.
	Selector string

	// Mode controls how the element is merged. The default (outer) is never
	// emitted on the wire — it is the DataStar client's default behavior.
	Mode ElementPatchMode

	// Namespace specifies the XML namespace. The default (html) is never
	// emitted on the wire.
	Namespace Namespace

	// UseViewTransitions enables the View Transition API for the merge.
	UseViewTransitions bool

	// ViewTransitionSelector scopes the view transition to a specific element.
	ViewTransitionSelector string

	// HTML is the element content to patch into the DOM.
	HTML string

	// EventID is an optional SSE event identifier for reconnection replay.
	EventID string

	// RetryDuration overrides the default SSE retry interval. Only emitted
	// on the wire when > 0 and != [DefaultRetryDuration].
	RetryDuration time.Duration
}

ElementsPatch patches HTML elements into the DOM on the DataStar client. It is the most-used DataStar patch type.

Construct one with NewElementsPatch and functional options:

patch := datastar.NewElementsPatch("<div>Hello</div>",
    datastar.WithSelector("#feed"),
    datastar.WithMode(datastar.ElementPatchModeInner),
)
stream.Send(patch.Event())
Example

ExampleElementsPatch demonstrates the library's keystone: a patch is a value you construct without a connection. Its wire format is fully determined by the struct fields and options.

package main

import (
	"fmt"

	"github.com/larsartmann/go-datastar"
)

func main() {
	patch := datastar.NewElementsPatch("<div>Hello</div>",
		datastar.WithSelector("#feed"),
		datastar.WithModePrepend(),
	)

	fmt.Println(patch.Event().Data)
}
Output:
selector #feed
mode prepend
elements <div>Hello</div>

func ElementsFromGostar

func ElementsFromGostar(
	r GoStarElementRenderer,
	opts ...ElementPatchOption,
) (ElementsPatch, error)

ElementsFromGostar renders a GoStarElementRenderer to HTML and creates an ElementsPatch from the result.

func ElementsFromTempl

func ElementsFromTempl(c TemplComponent, opts ...ElementPatchOption) (ElementsPatch, error)

ElementsFromTempl renders a TemplComponent to HTML and creates an ElementsPatch from the result.

func NewElementsPatch

func NewElementsPatch(html string, opts ...ElementPatchOption) ElementsPatch

NewElementsPatch creates an ElementsPatch with the given HTML and options. The default mode is DefaultElementPatchMode (outer), which is never emitted on the wire.

func NewRemoveByIDPatch

func NewRemoveByIDPatch(id string, opts ...ElementPatchOption) ElementsPatch

NewRemoveByIDPatch creates an ElementsPatch that removes the element with the given ID. Equivalent to NewRemovePatch("#" + id).

func NewRemovePatch

func NewRemovePatch(selector string, opts ...ElementPatchOption) ElementsPatch

NewRemovePatch creates an ElementsPatch that removes the element matching the given CSS selector from the DOM. It is equivalent to:

NewElementsPatch("", WithModeRemove(), WithSelector(selector))

func (ElementsPatch) Event

func (p ElementsPatch) Event() sse.Event

Event returns the sse.Event for this element patch. The data lines are constructed in the exact order the DataStar JS client expects:

  1. selector (if non-empty)
  2. mode (if not outer)
  3. namespace (if non-empty and not html)
  4. useViewTransition true (if enabled)
  5. viewTransitionSelector (if non-empty and view transitions enabled)
  6. elements <line> (one per line of HTML)

type EventType

type EventType string

EventType is the DataStar protocol event type sent as the SSE event: field.

const (
	// EventTypePatchElements is the event for patching HTML elements into the DOM.
	EventTypePatchElements EventType = "datastar-patch-elements"

	// EventTypePatchSignals is the event for patching reactive signals.
	EventTypePatchSignals EventType = "datastar-patch-signals"
)

type GoStarElementRenderer

type GoStarElementRenderer interface {
	Render(w io.Writer) error
}

GoStarElementRenderer satisfies the component rendering interface for the GoStar template engine.

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore is an in-memory ring buffer implementing sse.EventStore. It keeps the last N events for SSE reconnection replay.

Events are sequenced by their numeric sse.EventID: EventsAfter returns all stored events whose ID is strictly greater than the requested lastID. Non-numeric IDs are treated as zero, so an empty or non-numeric lastID replays the entire buffer.

MemoryStore is safe for concurrent use. It is intended for single-process deployments and demos. For multi-instance setups, use a shared store (Redis, Postgres) that implements sse.EventStore.

func NewMemoryStore

func NewMemoryStore(capacity int) *MemoryStore

NewMemoryStore creates a MemoryStore that retains the last capacity events. If capacity is non-positive, DefaultMemoryStoreCapacity is used.

func (*MemoryStore) Append

func (s *MemoryStore) Append(evt sse.Event)

Append stores an event for later replay. If the buffer is full, the oldest event is evicted.

func (*MemoryStore) EventsAfter

func (s *MemoryStore) EventsAfter(lastID sse.EventID) ([]sse.Event, error)

EventsAfter returns stored events with IDs strictly greater than lastID, ordered ascending by sequence number. Non-numeric lastID values are treated as zero, replaying the entire buffer.

func (*MemoryStore) Len

func (s *MemoryStore) Len() int

Len returns the number of events currently stored.

type Namespace

type Namespace string

Namespace is the XML namespace to use when patching elements into the DOM.

const (
	// NamespaceHTML is the default namespace for HTML elements.
	NamespaceHTML Namespace = "html"

	// NamespaceSVG is the namespace for SVG elements.
	NamespaceSVG Namespace = "svg"

	// NamespaceMathML is the namespace for MathML elements.
	NamespaceMathML Namespace = "mathml"
)

func NamespaceFromString

func NamespaceFromString(nsStr string) (Namespace, error)

NamespaceFromString converts a string to a Namespace. Returns an error for invalid namespace strings.

type Patch

type Patch interface {
	// Event returns the SSE wire-format event for this patch. The returned
	// [sse.Event] contains the event type, data lines, optional event ID, and
	// optional retry duration — everything go-sse needs to serialize the patch.
	Event() sse.Event
}

Patch is the core interface of go-datastar. Every DataStar protocol message (element patches, signal patches, script execution, redirects, etc.) implements this interface.

The key design principle: patches are first-class VALUES, not method calls on a live connection. A Patch can be constructed, stored in a slice, filtered by a predicate, replayed from an EventStore, and broadcast through a sse.Broadcaster — all without an open HTTP connection.

Call Patch.Event to produce the final sse.Event that go-sse serializes to the wire:

patch := datastar.NewElementsPatch("<div>hi</div>", datastar.WithSelector("#feed"))
broadcaster := sse.NewBroadcaster[sse.Event]()
broadcaster.Broadcast(patch.Event())

Or broadcast patches directly for typed filtering:

patchCaster := sse.NewBroadcaster[datastar.Patch]()
patchCaster.Broadcast(patch)

type Response

type Response struct {
	// contains filtered or unexported fields
}

Response wraps an sse.Stream and provides fluent methods for sending DataStar patches on a single HTTP connection. Each method constructs a Patch, calls its Patch.Event method, and sends the resulting sse.Event via the underlying stream.

Create one per HTTP handler:

func handler(w http.ResponseWriter, r *http.Request) {
    stream := sse.NewStream(w, r)
    defer func() { _ = stream.Close() }()

    resp := datastar.NewResponse(stream)

    if err := resp.PatchElements("<div>Hello</div>", datastar.WithSelector("#feed")); err != nil {
        log.Printf("patch elements: %v", err)
        return
    }

    if err := resp.MarshalAndPatchSignals(map[string]any{"count": 1}); err != nil {
        log.Printf("patch signals: %v", err)
    }
}

func NewResponse

func NewResponse(stream *sse.Stream) *Response

NewResponse creates a Response wrapping the given sse.Stream.

func NewResponseFromHTTP

func NewResponseFromHTTP(w http.ResponseWriter, r *http.Request) *Response

NewResponseFromHTTP is a convenience that creates an sse.Stream from the ResponseWriter and Request, then wraps it in a Response.

func (*Response) ApplyPatches

func (r *Response) ApplyPatches(patches ...Patch) error

ApplyPatches sends multiple patches in sequence.

func (*Response) ConsoleError

func (r *Response) ConsoleError(err error, opts ...ScriptPatchOption) error

ConsoleError sends a console.error ScriptPatch.

func (*Response) ConsoleLog

func (r *Response) ConsoleLog(msg string, opts ...ScriptPatchOption) error

ConsoleLog sends a console.log ScriptPatch.

func (*Response) DispatchCustomEvent

func (r *Response) DispatchCustomEvent(
	eventName string,
	detail any,
	opts ...DispatchCustomEventOption,
) error

DispatchCustomEvent dispatches a custom DOM event on the client.

func (*Response) ExecuteScript

func (r *Response) ExecuteScript(script string, opts ...ScriptPatchOption) error

ExecuteScript sends a ScriptPatch on the underlying stream.

func (*Response) MarshalAndPatchSignals

func (r *Response) MarshalAndPatchSignals(v any, opts ...SignalsPatchOption) error

MarshalAndPatchSignals marshals a Go value to JSON and sends it as a SignalsPatch. Returns an error if marshaling fails.

func (*Response) PatchElements

func (r *Response) PatchElements(html string, opts ...ElementPatchOption) error

func (*Response) PatchElementsTempl

func (r *Response) PatchElementsTempl(c TemplComponent, opts ...ElementPatchOption) error

PatchElementsTempl renders a TemplComponent to HTML and sends it as an ElementsPatch.

func (*Response) PatchSignals

func (r *Response) PatchSignals(signalsJSON []byte, opts ...SignalsPatchOption) error

PatchSignals sends a SignalsPatch with the given pre-encoded JSON.

func (*Response) Prefetch

func (r *Response) Prefetch(urls ...string) error

Prefetch sends a speculation rules ScriptPatch to prefetch the given URLs.

func (*Response) Redirect

func (r *Response) Redirect(targetURL string, opts ...ScriptPatchOption) error

Redirect sends a redirect ScriptPatch.

func (*Response) RemoveElement

func (r *Response) RemoveElement(selector string, opts ...ElementPatchOption) error

RemoveElement sends an ElementsPatch that removes the given selector.

func (*Response) RemoveElementByID

func (r *Response) RemoveElementByID(id string, opts ...ElementPatchOption) error

RemoveElementByID sends an ElementsPatch that removes the element with the given ID.

func (*Response) ReplaceURL

func (r *Response) ReplaceURL(u url.URL, opts ...ScriptPatchOption) error

ReplaceURL sends a replaceState ScriptPatch.

func (*Response) Send

func (r *Response) Send(evt sse.Event) error

Send sends a raw sse.Event on the underlying stream.

func (*Response) Stream

func (r *Response) Stream() *sse.Stream

Stream returns the underlying sse.Stream.

type ScriptPatch

type ScriptPatch struct {
	// Script is the JavaScript source code to execute.
	Script string

	// AutoRemove controls whether the script element self-removes after
	// execution. nil (the default) and true both add data-effect="el.remove()".
	// Set to false to keep the element.
	AutoRemove *bool

	// Attributes are additional HTML attributes for the <script> tag.
	// Each should be a complete attribute (e.g., `type="module"`).
	Attributes []string

	// EventID is an optional SSE event identifier.
	EventID string

	// RetryDuration overrides the default SSE retry interval.
	RetryDuration time.Duration
}

ScriptPatch executes JavaScript on the DataStar client by injecting a <script> element into the DOM (patched via ElementsPatch with selector=body, mode=append).

By default the script element auto-removes itself after execution (data-effect="el.remove()"). Use WithScriptAutoRemove(false) to keep the element.

Construct one with NewScriptPatch:

patch := datastar.NewScriptPatch("console.log('hi')")
stream.Send(patch.Event())

func NewConsoleErrorPatch

func NewConsoleErrorPatch(err error, opts ...ScriptPatchOption) ScriptPatch

NewConsoleErrorPatch creates a ScriptPatch that calls console.error with the given error's message. The message is JS-quoted via %q.

func NewConsoleLogPatch

func NewConsoleLogPatch(msg string, opts ...ScriptPatchOption) ScriptPatch

NewConsoleLogPatch creates a ScriptPatch that calls console.log with the given message. The message is JS-quoted via %q.

func NewConsoleLogfPatch

func NewConsoleLogfPatch(format string, args ...any) ScriptPatch

NewConsoleLogfPatch is a printf-style variant of NewConsoleLogPatch.

func NewPrefetchPatch

func NewPrefetchPatch(urls ...string) ScriptPatch

NewPrefetchPatch creates a ScriptPatch that injects a speculation rules JSON block to prefetch the given URLs.

func NewRedirectPatch

func NewRedirectPatch(targetURL string, opts ...ScriptPatchOption) ScriptPatch

NewRedirectPatch creates a ScriptPatch that redirects the browser to the given URL using setTimeout.

func NewRedirectfPatch

func NewRedirectfPatch(format string, args ...any) ScriptPatch

NewRedirectfPatch is a printf-style variant of NewRedirectPatch.

func NewReplaceURLPatch

func NewReplaceURLPatch(u url.URL, opts ...ScriptPatchOption) ScriptPatch

NewReplaceURLPatch creates a ScriptPatch that replaces the browser URL using history.replaceState.

func NewScriptPatch

func NewScriptPatch(script string, opts ...ScriptPatchOption) ScriptPatch

NewScriptPatch creates a ScriptPatch with the given JavaScript source and options. The default retry duration is DefaultRetryDuration.

func (ScriptPatch) Event

func (p ScriptPatch) Event() sse.Event

Event returns the sse.Event for this script patch. The script is wrapped in a <script> element and sent as a patch-elements event with selector=body, mode=append — matching the DataStar SDK wire format exactly.

type ScriptPatchOption

type ScriptPatchOption func(*ScriptPatch)

ScriptPatchOption configures a ScriptPatch.

func WithScriptAttributeKVs

func WithScriptAttributeKVs(kvs ...string) ScriptPatchOption

WithScriptAttributeKVs sets script attributes from key-value pairs. If the argument count is odd, the final unpaired key is silently dropped.

Prefer WithScriptAttributes for pre-formatted attributes.

func WithScriptAttributes

func WithScriptAttributes(attrs ...string) ScriptPatchOption

WithScriptAttributes sets additional HTML attributes for the <script> tag. Each should be a complete key="value" pair (e.g., `type="module"`).

func WithScriptAutoRemove

func WithScriptAutoRemove(b bool) ScriptPatchOption

WithScriptAutoRemove controls whether the script element self-removes. Pass false to keep the element.

func WithScriptEventID

func WithScriptEventID(id string) ScriptPatchOption

WithScriptEventID sets the SSE event ID for the script patch.

func WithScriptRetryDuration

func WithScriptRetryDuration(d time.Duration) ScriptPatchOption

WithScriptRetryDuration overrides the SSE retry duration for the script patch.

type SignalsPatch

type SignalsPatch struct {
	// Signals is the JSON-encoded signal payload.
	Signals []byte

	// OnlyIfMissing instructs the client to only set signals that don't
	// already exist.
	OnlyIfMissing bool

	// EventID is an optional SSE event identifier.
	EventID string

	// RetryDuration overrides the default SSE retry interval. Only emitted
	// when > 0 and != [DefaultRetryDuration].
	RetryDuration time.Duration
}

SignalsPatch patches reactive signals on the DataStar client. The signals payload must be JSON-encoded bytes.

Construct one with NewSignalsPatch (marshals a Go value) or directly (if you already have JSON bytes):

// From a Go struct:
patch, err := datastar.NewSignalsPatch(map[string]any{"count": 42})

// From pre-encoded JSON:
patch := datastar.SignalsPatch{Signals: []byte(`{"count":42}`)}
Example

ExampleSignalsPatch shows pre-encoded JSON signals emitted as a wire event.

package main

import (
	"fmt"

	"github.com/larsartmann/go-datastar"
)

func main() {
	patch := datastar.SignalsPatch{Signals: []byte(`{"count":1}`)}

	fmt.Println(patch.Event().Data)
}
Output:
signals {"count":1}

func NewSignalsIfMissingPatch

func NewSignalsIfMissingPatch(v any, opts ...SignalsPatchOption) (SignalsPatch, error)

NewSignalsIfMissingPatch creates a SignalsPatch with OnlyIfMissing=true.

func NewSignalsPatch

func NewSignalsPatch(v any, opts ...SignalsPatchOption) (SignalsPatch, error)

NewSignalsPatch creates a SignalsPatch from a Go value, marshaling it to JSON. Returns an error if marshaling fails (unlike the SDK's panicking MarshalAndPatchSignals).

func (SignalsPatch) Event

func (p SignalsPatch) Event() sse.Event

Event returns the sse.Event for this signals patch. The data lines are:

  1. onlyIfMissing true (if OnlyIfMissing is set)
  2. signals <line> (one per line of the JSON payload)

type SignalsPatchOption

type SignalsPatchOption func(*SignalsPatch)

SignalsPatchOption configures a SignalsPatch.

func WithOnlyIfMissing

func WithOnlyIfMissing(onlyIfMissing bool) SignalsPatchOption

WithOnlyIfMissing instructs the client to only patch signals that are missing.

func WithSignalsEventID

func WithSignalsEventID(id string) SignalsPatchOption

WithSignalsEventID sets the SSE event ID for the signals patch.

func WithSignalsRetryDuration

func WithSignalsRetryDuration(d time.Duration) SignalsPatchOption

WithSignalsRetryDuration overrides the SSE retry duration for the signals patch.

type TemplComponent

type TemplComponent interface {
	Render(ctx context.Context, w io.Writer) error
}

TemplComponent satisfies the component rendering interface for the Templ template engine. This separate type ensures compatibility with Templ without imposing a dependency on those who prefer a different template engine.

Directories

Path Synopsis
datastartest module
Example: live feed using go-datastar patches as values with go-sse Broadcaster.
Example: live feed using go-datastar patches as values with go-sse Broadcaster.
static module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL