Go API

Every exported type, function and method of goapplib, page and wasmhost, read from the source on main when the site is built.

This page is generated. When the site builds, it reads goapplib’s Go source on main with go/doc and writes out each package’s exported API: every type, function, method, constant and variable, with its declaration and doc comment. A test fails the build if an exported name is missing, so the page can’t drift from the code the way the hand-kept list it replaces did. (We kept that list by hand, and it still showed Loader[AC] two releases after it was removed.) Where a guide page teaches a symbol, its entry links there.

A declaration marked build: js && wasm exists only in a build for the browser (GOOS=js GOARCH=wasm), and one marked build: !(js && wasm) only in a native one. pkg.go.dev has pretty much the same API for each tagged release, though it lags main until the next tag.

The packages: goapplib, page, wasmhost.

goapplib

import "github.com/panyam/goapplib"

Constants and variables

const (
	ColorReset  = "\033[0m"
	ColorCyan   = "\033[36m"
	ColorGreen  = "\033[32m"
	ColorYellow = "\033[33m"
	ColorBold   = "\033[1m"
)

ANSI color codes

CORS

func CORS(next http.Handler) http.Handler

CORS adds CORS headers for local development.

DefaultFuncMap

func DefaultFuncMap() template.FuncMap

DefaultFuncMap returns the default template functions.

Guide: /goapplib/guide/templates/#template-functions

DefaultRateLimitKeyFunc

func DefaultRateLimitKeyFunc(r *http.Request) string

DefaultRateLimitKeyFunc extracts the client identifier from the request. Uses X-Forwarded-For for proxied requests, otherwise RemoteAddr.

GetLocalIP

func GetLocalIP() (string, error)

GetLocalIP returns the local network IP address (preferring ethernet/wifi over localhost)

GzipFileServer

func GzipFileServer(root http.FileSystem) http.Handler

GzipFileServer wraps an http.Handler to serve pre-compressed .gz files when available and the client accepts gzip encoding.

For WASM files, this serves .wasm.gz (4.7MB) instead of .wasm (25MB), reducing transfer size by ~81%.

HtmxCurrentURL

func HtmxCurrentURL(r *http.Request) string

HtmxCurrentURL returns the HX-Current-URL header value.

HtmxPrompt

func HtmxPrompt(r *http.Request) string

HtmxPrompt returns the HX-Prompt header value.

HtmxTarget

func HtmxTarget(r *http.Request) string

HtmxTarget returns the HX-Target header value.

HtmxTrigger

func HtmxTrigger(r *http.Request) string

HtmxTrigger returns the HX-Trigger header value.

IsBoostedRequest

func IsBoostedRequest(r *http.Request) bool

IsBoostedRequest checks if the request is a boosted link.

IsHtmxRequest

func IsHtmxRequest(r *http.Request) bool

IsHtmxRequest checks if the request is an HTMX request.

LoadAll

func LoadAll(r *http.Request, w http.ResponseWriter, app any, loaders ...Loader) (error, bool)

LoadAll runs loaders in order with the page's app, and stops at the first one that returns an error or finished = true, returning what it returned. A nil loader is skipped. A page typically calls it from its own Load:

func (p *GamesPage) Load(r *http.Request, w http.ResponseWriter, app *goapplib.App[*Site]) (error, bool) {
	return goapplib.LoadAll(r, w, app, &p.BasePage, &p.WithPagination, goapplib.LoaderFunc[*Site](p.loadGames))
}

Guide: /goapplib/guide/concepts/#loaders

ParseTemplateSpec

func ParseTemplateSpec(spec string) (fileName, blockName string)

ParseTemplateSpec parses a template spec into file name and block name. Format: "path/to/template" or "path/to/template:BlockName"

If no ":" is present, block name is auto-derived from the base filename. If ":" is present, the part after ":" is used as the explicit block name. An empty block name after ":" means render the entire template file.

PrintStartupMessage

func PrintStartupMessage(address string)

PrintStartupMessage prints a colorized startup message with clickable links

Register

func Register[V View[AC], AC any](
	app *App[AC],
	mux *http.ServeMux,
	pattern string,
	opts ...Option,
) *http.ServeMux

Register registers a View at the given pattern. If mux is nil, a new ServeMux is created. Returns the mux for chaining.

Usage:

mux := http.NewServeMux()
goapplib.Register[*HomePage](app, mux, "/")
goapplib.Register[*GameListingPage](app, mux, "/games/")

Guide: /goapplib/guide/routing/#register

RegisterFunc

func RegisterFunc(
	mux *http.ServeMux,
	pattern string,
	handler http.HandlerFunc,
) *http.ServeMux

RegisterFunc registers a handler function at the given pattern. Convenience wrapper for mux.HandleFunc.

RegisterGroup

func RegisterGroup[G PageGroup[AC], AC any](
	app *App[AC],
	mux *http.ServeMux,
	prefix string,
	opts ...Option,
) *http.ServeMux

RegisterGroup registers a PageGroup under the given prefix. The group's routes are mounted with the prefix stripped.

Usage:

goal.RegisterGroup[*GamesGroup](app, rootMux, "/games")

Guide: /goapplib/guide/routing/#groups

RegisterHandler

func RegisterHandler(
	mux *http.ServeMux,
	pattern string,
	handler http.Handler,
) *http.ServeMux

RegisterHandler registers an http.Handler at the given pattern. Convenience wrapper for mux.Handle.

SetupTemplates

func SetupTemplates(paths ...string) *tmplr.TemplateGroup

SetupTemplates creates a TemplateGroup with the given paths. Paths are checked in order, so put your app's templates first for overrides.

Guide: /goapplib/guide/getting-started/#install

SmartRegister

func SmartRegister[V interface {
	View[AC]
	HtmxAware
}, AC any](
	app *App[AC],
	mux *http.ServeMux,
	pattern string,
	fullTemplateSpec string,
	fragmentTemplateSpec string,
	opts ...Option,
) *http.ServeMux

SmartRegister registers a View that renders fullTemplateSpec as a page and fragmentTemplateSpec for an htmx request that wants a fragment. It's Register with WithTemplate(fullTemplateSpec) and WithFragmentTemplate(fragmentTemplateSpec), kept for the code that calls it; new code can pass WithFragmentTemplate to Register or MuxBuilder.Page directly.

Guide: /goapplib/guide/htmx/#one-route-a-page-or-a-fragment

App

type App[AppContext any] struct {
	Context            AppContext
	Templates          *tmplr.TemplateGroup
	RenderTemplateFunc func(w http.ResponseWriter, templateFileName string, templateBlockName string, view any) error
}

App holds app-level shared state and template system. AppContext is the application context type defined by your application (e.g., *WeewarApp).

Guide: /goapplib/guide/concepts/#the-app-and-its-context

NewApp

func NewApp[AppContext any](ctx AppContext, templates *tmplr.TemplateGroup) *App[AppContext]

NewApp creates a new App with the given application context and templates.

Guide: /goapplib/guide/concepts/#the-app-and-its-context

App.NewMux

func (app *App[AppContext]) NewMux() *MuxBuilder[AppContext]

NewMux creates a MuxBuilder for fluent route building.

App.RenderTemplate

func (app *App[AppContext]) RenderTemplate(
	w http.ResponseWriter,
	templateFileName string,
	templateBlockName string,
	view any) error

RenderTemplate renders the named template with the given view data. The page is rendered in full before any of it is written, so on an error nothing has reached w and the caller can still answer with an error status (#108). If RenderTemplateFunc is set, it delegates to that function, which is responsible for its own writes.

AuthProvider

type AuthProvider interface {
	GetLoggedInUserId(r *http.Request) string
	GetUserById(id string) (AuthUser, error)
}

AuthProvider is the interface for auth services.

Guide: /goapplib/guide/views/#goapplibs-mixins

AuthUser

type AuthUser interface {
	Profile() map[string]any
}

AuthUser is a minimal user interface.

BasePage

type BasePage struct {
	Title               string // Page title (internal use, fallback for MetaTitle)
	BodyClass           string // CSS classes for <body>
	ActiveTab           string // Highlight active navigation tab
	CustomHeader        bool   // If true, skip default header rendering
	DisableSplashScreen bool   // If true, hide loading splash
	SplashTitle         string // Custom splash title
	SplashMessage       string // Custom splash message
	BodyDataAttributes  string // HTML data attributes for body

	// SEO fields
	MetaTitle       string // Title for <title> tag and search results (falls back to Title if empty)
	MetaDescription string // Meta description for search engines
	CanonicalUrl    string // Canonical URL for SEO (e.g., https://example.com/page/)
}

BasePage provides common page metadata. Embed this in your page structs.

Guide: /goapplib/guide/views/#goapplibs-mixins

BasePage.Load

func (p *BasePage) Load(r *http.Request, w http.ResponseWriter, vc any) (error, bool)

Load implements Loader for BasePage. Note: This is a generic implementation that works with any ViewContext.

EntityListingData

type EntityListingData[ItemType any] struct {
	// Page header
	Title       string
	Subtitle    string
	CreateUrl   string
	CreateLabel string

	// View configuration
	ViewMode           string // "grid" or "list"
	ViewModeStorageKey string // localStorage key for saving preference
	EnableViewToggle   bool
	GridContainerId    string
	SearchInputId      string
	SortSelectId       string
	SearchPlaceholder  string

	// URL "formats".  To get the Url we would do something like fmt.Sprintf(fmt, Id)
	ViewUrlFormat   string
	EditUrlFormat   string
	DeleteUrlFormat string
	SearchUrl       string
	RefreshUrl      string

	// Sort options
	SortOptions []SortOption

	// Items to display - templates access fields directly (e.g., .Id, .Name, .Description)
	Items []ItemType

	// Actions
	ShowActions    bool
	ShowEditButton bool // Show edit button (typically only when logged in)
	HtmxEnabled    bool
	RefreshTrigger string

	// Empty state
	EmptyTitle   string
	EmptyMessage string
}

EntityListingData provides data for the EntityListing template component. This struct is designed to work with the EntityListing.html template. ItemType is the type of items in the listing - templates access item fields directly (e.g., .Id, .Name).

Guide: /goapplib/guide/templates/#components

NewEntityListingData

func NewEntityListingData[ItemType any](title string, viewUrlFormat string) *EntityListingData[ItemType]

NewEntityListingData creates a new EntityListingData with sensible defaults.

EntityListingData.DeleteUrl

func (d *EntityListingData[ItemType]) DeleteUrl(id string) string

Returns the formatted DeleteUrl

EntityListingData.EditUrl

func (d *EntityListingData[ItemType]) EditUrl(id string) string

Returns the formatted EditUrl

EntityListingData.ViewUrl

func (d *EntityListingData[ItemType]) ViewUrl(id string) string

Returns the formatted ViewUrl

EntityListingData.WithCreate

func (d *EntityListingData[ItemType]) WithCreate(url, label string) *EntityListingData[ItemType]

WithCreate sets the create URL and label

EntityListingData.WithDelete

func (d *EntityListingData[ItemType]) WithDelete(url string) *EntityListingData[ItemType]

WithDelete sets the delete URL

EntityListingData.WithEdit

func (d *EntityListingData[ItemType]) WithEdit(url string) *EntityListingData[ItemType]

WithEdit sets the edit URL

EntityListingData.WithHtmx

func (d *EntityListingData[ItemType]) WithHtmx(searchUrl string) *EntityListingData[ItemType]

WithHtmx enables HTMX for the listing

EntityListingData.WithView

func (d *EntityListingData[ItemType]) WithView(url string) *EntityListingData[ItemType]

WithView sets the viewer URL

HtmxAware

type HtmxAware interface {
	ShouldRenderFragment() bool
}

HtmxAware is implemented by views that can detect HTMX requests.

HtmxResponse

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

HtmxResponse provides helpers for setting HTMX response headers.

Guide: /goapplib/guide/htmx/#answering-an-action

NewHtmxResponse

func NewHtmxResponse(w http.ResponseWriter) *HtmxResponse

NewHtmxResponse creates a new HtmxResponse wrapper.

HtmxResponse.Location

func (h *HtmxResponse) Location(url string) *HtmxResponse

Location does a client-side redirect without full page reload.

HtmxResponse.LocationWithContext

func (h *HtmxResponse) LocationWithContext(spec map[string]any) *HtmxResponse

LocationWithContext does a redirect with additional context.

HtmxResponse.PushURL

func (h *HtmxResponse) PushURL(url string) *HtmxResponse

PushURL pushes a new URL onto the browser's history stack.

HtmxResponse.Redirect

func (h *HtmxResponse) Redirect(url string) *HtmxResponse

Redirect tells HTMX to perform a client-side redirect.

HtmxResponse.Refresh

func (h *HtmxResponse) Refresh() *HtmxResponse

Refresh tells HTMX to do a full page refresh.

HtmxResponse.ReplaceURL

func (h *HtmxResponse) ReplaceURL(url string) *HtmxResponse

ReplaceURL replaces the current URL in the browser's history.

HtmxResponse.Reselect

func (h *HtmxResponse) Reselect(selector string) *HtmxResponse

Reselect changes the selection from the response.

HtmxResponse.Reswap

func (h *HtmxResponse) Reswap(style string) *HtmxResponse

Reswap changes the swap method.

HtmxResponse.Retarget

func (h *HtmxResponse) Retarget(selector string) *HtmxResponse

Retarget changes the target element for the swap.

HtmxResponse.StopPolling

func (h *HtmxResponse) StopPolling() *HtmxResponse

StopPolling tells HTMX to stop polling.

HtmxResponse.Trigger

func (h *HtmxResponse) Trigger(event string) *HtmxResponse

Trigger sets the HX-Trigger header to trigger a client-side event.

HtmxResponse.TriggerAfterSettle

func (h *HtmxResponse) TriggerAfterSettle(event string) *HtmxResponse

TriggerAfterSettle sets HX-Trigger-After-Settle header.

HtmxResponse.TriggerAfterSwap

func (h *HtmxResponse) TriggerAfterSwap(event string) *HtmxResponse

TriggerAfterSwap sets HX-Trigger-After-Swap header.

HtmxResponse.TriggerWithData

func (h *HtmxResponse) TriggerWithData(event string, data any) *HtmxResponse

TriggerWithData sets HX-Trigger with JSON data.

Loader

type Loader interface {
	Load(r *http.Request, w http.ResponseWriter, app any) (err error, finished bool)
}

Loader is one step of a page's Load, run in order by LoadAll. goapplib's mixins (BasePage, WithPagination, WithFiltering, WithAuth, WithHtmx) are Loaders as they are, so a page chains them with LoadAll. app is the page's *App[AC], passed through as is; a loader that needs it typed is a LoaderFunc[AC].

Before 0.7.0 this was Loader[AC], with app typed as *App[AC], which no built-in mixin satisfied (issue 107).

Guide: /goapplib/guide/concepts/#loaders

LoaderFunc

type LoaderFunc[AC any] func(r *http.Request, w http.ResponseWriter, app *App[AC]) (error, bool)

LoaderFunc is a typed Loader: an app's own loading step, which gets app as its *App[AC]. Given an app of any other type, it doesn't run, and returns an error naming both types, which LoadAll returns, so a page renders a 500 rather than panicking.

Guide: /goapplib/guide/concepts/#loaders

AuthLoader

func AuthLoader[AC any](auth *WithAuth, provider AuthProvider) LoaderFunc[AC]

AuthLoader returns a LoaderFunc that loads auth info, for LoadAll when you have an AuthProvider. Its app type can't be inferred, so name it: AuthLoader[*Site](&p.WithAuth, provider).

LoaderFunc.Load

func (f LoaderFunc[AC]) Load(r *http.Request, w http.ResponseWriter, app any) (error, bool)

Load implements Loader.

LoginConfig

type LoginConfig struct {
	EnableEmailLogin     bool
	EnableGoogleLogin    bool
	EnableGitHubLogin    bool
	EnableMicrosoftLogin bool
	EnableAppleLogin     bool
}

LoginConfig controls which login methods are available.

MuxBuilder

type MuxBuilder[AC any] struct {
	// contains filtered or unexported fields
}

MuxBuilder builds a ServeMux fluently, as an alternative to calling Register and RegisterGroup:

mux := app.NewMux().
	Page("/{$}", func() goapplib.View[*Site] { return &HomePage{} }).
	Group("/games", func(m *goapplib.MuxBuilder[*Site]) {
		m.Page("/{id}", func() goapplib.View[*Site] { return &GamePage{} })
	}).
	Build()

A Page renders exactly as Register would render the same view type.

Guide: /goapplib/guide/routing/#muxbuilder

MuxBuilder.Build

func (b *MuxBuilder[AC]) Build() *http.ServeMux

Build returns the constructed ServeMux.

MuxBuilder.Group

func (b *MuxBuilder[AC]) Group(prefix string, setup func(*MuxBuilder[AC])) *MuxBuilder[AC]

Group mounts the routes setup adds under prefix, which is stripped before they match, so a route "/{id}" in a Group("/games", ...) answers /games/7. Middleware added by Use before the Group wraps the whole group.

MuxBuilder.HandleFunc

func (b *MuxBuilder[AC]) HandleFunc(pattern string, h http.HandlerFunc) *MuxBuilder[AC]

HandleFunc registers an http.HandlerFunc.

MuxBuilder.Handler

func (b *MuxBuilder[AC]) Handler(pattern string, h http.Handler) *MuxBuilder[AC]

Handler registers an http.Handler.

MuxBuilder.Mux

func (b *MuxBuilder[AC]) Mux() *http.ServeMux

Mux returns the underlying ServeMux for direct access.

MuxBuilder.Page

func (b *MuxBuilder[AC]) Page(pattern string, maker func() View[AC], opts ...Option) *MuxBuilder[AC]

Page registers a view at pattern. maker returns a fresh view for each request, and the view's type picks its template the way Register's does (HomePage renders HomePage.html's HomePage block), unless WithTemplate says otherwise.

MuxBuilder.Static

func (b *MuxBuilder[AC]) Static(pattern string, dir string) *MuxBuilder[AC]

Static serves the files in dir at pattern, with pattern stripped from the path.

MuxBuilder.Use

func (b *MuxBuilder[AC]) Use(mw func(http.Handler) http.Handler) *MuxBuilder[AC]

Use adds middleware to every route registered after it on this builder, groups included. Routes registered before it aren't wrapped. Middleware added by several Uses runs in the order added.

Option

type Option func(*options)

Option configures registration behavior.

WithFragmentTemplate

func WithFragmentTemplate(spec string) Option

WithFragmentTemplate sets the template a page renders for an htmx request that wants a fragment rather than the whole page: when the view is HtmxAware (embedding WithHtmx makes it so, once WithHtmx has loaded) and its ShouldRenderFragment is true, which it is for an htmx request that isn't a boosted link. Any other request, or a view that isn't HtmxAware, gets the page's usual template. spec has WithTemplate's form, "path/File" or "path/File:Block".

Guide: /goapplib/guide/htmx/#one-route-a-page-or-a-fragment

WithMiddleware

func WithMiddleware(mw ...func(http.Handler) http.Handler) Option

WithMiddleware adds middleware to the handler.

Guide: /goapplib/guide/routing/#register

WithTemplate

func WithTemplate(spec string) Option

WithTemplate sets the template file and optional block name. Format: "path/to/template" or "path/to/template:BlockName"

If no ":" is present, block name is auto-derived from the base filename. If ":" is present, the part after ":" is used as the explicit block name. An empty block name after ":" means render the entire template file.

Examples:

  • "CanvasListingPage" -> file: CanvasListingPage.html, block: CanvasListingPage (auto-derived)
  • "canvases/CanvasListingPage" -> file: canvases/CanvasListingPage.html, block: CanvasListingPage (auto-derived)
  • "canvases/CanvasListingPage:MyBlock" -> file: canvases/CanvasListingPage.html, block: MyBlock
  • "canvases/CanvasListingPage:" -> file: canvases/CanvasListingPage.html, block: "" (entire file)

Guide: /goapplib/guide/routing/#register

PageGroup

type PageGroup[AC any] interface {
	// RegisterRoutes returns a ServeMux with all routes for this group.
	// Patterns should be relative (prefix is stripped by RegisterGroup).
	RegisterRoutes(app *App[AC]) *http.ServeMux
}

PageGroup is the interface for a group of related pages. Implement this to define a set of routes under a common prefix.

Guide: /goapplib/guide/routing/#groups

RateLimitConfig

type RateLimitConfig struct {
	// Auth endpoints (login, signup, forgot-password) - stricter limits
	AuthLimit  int
	AuthWindow time.Duration

	// API endpoints - more permissive
	APILimit  int
	APIWindow time.Duration
}

RateLimitConfig holds configuration for different rate limit tiers.

DefaultRateLimitConfig

func DefaultRateLimitConfig() *RateLimitConfig

DefaultRateLimitConfig returns sensible defaults for rate limiting.

RateLimitMiddleware

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

RateLimitMiddleware creates HTTP middleware that applies rate limiting.

NewRateLimitMiddleware

func NewRateLimitMiddleware(config *RateLimitConfig) *RateLimitMiddleware

NewRateLimitMiddleware creates rate limiting middleware with the given config.

NewRateLimitMiddlewareWithKeyFunc

func NewRateLimitMiddlewareWithKeyFunc(config *RateLimitConfig, keyFunc func(*http.Request) string) *RateLimitMiddleware

NewRateLimitMiddlewareWithKeyFunc creates middleware with a custom key function.

RateLimitMiddleware.Wrap

func (m *RateLimitMiddleware) Wrap(next http.Handler) http.Handler

Wrap wraps any handler with the API rate limiter. This is a convenience method for handlers that aren't auth-specific.

RateLimitMiddleware.WrapAPI

func (m *RateLimitMiddleware) WrapAPI(next http.Handler) http.Handler

WrapAPI wraps an API handler with rate limiting.

RateLimitMiddleware.WrapAuth

func (m *RateLimitMiddleware) WrapAuth(next http.Handler) http.Handler

WrapAuth wraps an auth handler with rate limiting.

RateLimiter

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

RateLimiter provides a simple in-memory rate limiting using sliding window. For production with multiple instances, consider using Redis-based rate limiting.

NewRateLimiter

func NewRateLimiter(limit int, window time.Duration) *RateLimiter

NewRateLimiter creates a new rate limiter with the specified limit per window.

RateLimiter.Allow

func (rl *RateLimiter) Allow(key string) bool

Allow checks if a request from the given key should be allowed.

SampleLoginPage

type SampleLoginPage[AC any] struct {
	BasePage
	CallbackURL string
	CsrfToken   string
	Config      LoginConfig
}

SampleLoginPage provides sample login page functionality. Embed this in your app-specific login page struct.

SampleLoginPage.Load

func (p *SampleLoginPage[AC]) Load(r *http.Request, w http.ResponseWriter, app *App[AC]) (err error, finished bool)

Load implements View[AC] for SampleLoginPage.

SampleProfilePage

type SampleProfilePage[AC any] struct {
	BasePage

	// User information
	UserID            string
	Nickname          string
	Email             string
	EmailVerified     bool
	Username          string
	Profile           map[string]any
	VerificationSent  bool
	VerificationError string
}

SampleProfilePage provides sample profile page functionality. Embed this in your app-specific profile page struct.

SampleProfilePage.Load

func (p *SampleProfilePage[AC]) Load(r *http.Request, w http.ResponseWriter, app *App[AC]) (err error, finished bool)

Load implements View[AC] for SampleProfilePage.

SampleRegisterPage

type SampleRegisterPage[AC any] struct {
	BasePage
	CallbackURL    string
	CsrfToken      string
	Name           string
	Email          string
	Password       string
	VerifyPassword string
	Errors         map[string]string
}

SampleRegisterPage provides sample registration page functionality. Embed this in your app-specific register page struct.

SampleRegisterPage.Load

func (p *SampleRegisterPage[AC]) Load(r *http.Request, w http.ResponseWriter, app *App[AC]) (err error, finished bool)

Load implements View[AC] for SampleRegisterPage.

SortOption

type SortOption struct {
	Value    string
	Label    string
	Selected bool
}

SortOption represents a sort dropdown option

View

type View[AC any] interface {
	// Load prepares the view data from the request.
	// app provides access to the application context and templates.
	// Returns (error, finished):
	//   - error: non-nil if an error occurred (will be displayed)
	//   - finished: true if response was already written (redirect, etc.)
	Load(r *http.Request, w http.ResponseWriter, app *App[AC]) (err error, finished bool)
}

View is the interface that all pages/views must implement. AC is the application context type (e.g., *WeewarApp).

Guide: /goapplib/guide/views/

WebAppServer

type WebAppServer struct {
	Address       string
	GrpcAddress   string
	AllowLocalDev bool
}

WebAppServer provides a generic HTTP server with optional CORS and logging.

WebAppServer.StartWithHandler

func (s *WebAppServer) StartWithHandler(ctx context.Context, handler http.Handler, srvErr chan error, stopChan chan bool) error

StartWithHandler starts the HTTP server with the given handler.

WithAuth

type WithAuth struct {
	LoggedInUserId string // Current user's ID
	Username       string // Current user's display name
	IsLoggedIn     bool   // True if user is authenticated
	IsOwner        bool   // True if user owns the current entity
}

WithAuth provides authentication info. Embed this in page structs that need user info.

Guide: /goapplib/guide/views/#goapplibs-mixins

WithAuth.Load

func (p *WithAuth) Load(r *http.Request, w http.ResponseWriter, vc any) (error, bool)

Load is a no-op for WithAuth. Use LoadWithAuth to load auth info with your auth services.

WithAuth.LoadWithAuth

func (p *WithAuth) LoadWithAuth(r *http.Request, provider AuthProvider) (error, bool)

LoadWithAuth loads auth info using the provided auth services.

WithFiltering

type WithFiltering struct {
	Query    string // Search query
	Sort     string // Sort field/direction
	ViewMode string // Display mode: "grid", "table", etc.
}

WithFiltering provides search and sort support. Embed this in page structs that support filtering.

Guide: /goapplib/guide/views/#goapplibs-mixins

WithFiltering.Load

func (p *WithFiltering) Load(r *http.Request, w http.ResponseWriter, vc any) (error, bool)

Load implements Loader for WithFiltering.

WithHtmx

type WithHtmx struct {
	IsHtmx      bool   // True if this is an HTMX request
	IsBoosted   bool   // True if this is a boosted link
	Target      string // HX-Target header value
	Trigger     string // HX-Trigger header value
	TriggerName string // HX-Trigger-Name header value
	CurrentURL  string // HX-Current-URL header value
	Prompt      string // HX-Prompt header value
}

WithHtmx provides HTMX request detection. Embed this in page structs that need HTMX-aware rendering.

Guide: /goapplib/guide/htmx/#one-route-a-page-or-a-fragment

WithHtmx.Load

func (p *WithHtmx) Load(r *http.Request, w http.ResponseWriter, vc any) (error, bool)

Load implements Loader for WithHtmx.

WithHtmx.ShouldRenderFragment

func (p *WithHtmx) ShouldRenderFragment() bool

ShouldRenderFragment returns true if only a fragment should be rendered.

WithPagination

type WithPagination struct {
	CurrentPage int   // 0-indexed current page
	PageSize    int   // Items per page
	TotalCount  int   // Total number of items
	HasPrevPage bool  // True if there's a previous page
	HasNextPage bool  // True if there's a next page
	Pages       []int // Page numbers to display in pagination UI
}

WithPagination provides pagination support. Embed this in page structs that display paginated lists.

Guide: /goapplib/guide/views/#goapplibs-mixins

WithPagination.EvalPages

func (p *WithPagination) EvalPages()

EvalPages calculates page numbers to display.

WithPagination.Load

func (p *WithPagination) Load(r *http.Request, w http.ResponseWriter, vc any) (error, bool)

Load implements Loader for WithPagination.

WithPagination.NextPage

func (p *WithPagination) NextPage() int

NextPage returns the next page number.

WithPagination.Offset

func (p *WithPagination) Offset() int

Offset returns the offset for database queries.

WithPagination.Paginator

func (p *WithPagination) Paginator() *WithPagination

Paginator returns self for template access via .Paginator This allows templates to use {{ .Paginator.HasPrevPage }} etc. when the page struct embeds WithPagination.

WithPagination.PrevPage

func (p *WithPagination) PrevPage() int

PrevPage returns the previous page number.

WithPagination.SetTotal

func (p *WithPagination) SetTotal(total int, hasMore bool)

SetTotal updates pagination state based on total count.

WithPagination.TotalPages

func (p *WithPagination) TotalPages() int

TotalPages is how many pages TotalCount fills at PageSize, or 0 before SetTotal. goapplib's Pagination template shows it.

page

import "github.com/panyam/goapplib/page"

Package page describes which islands a page mounts and where, as a Spec the server writes into the page for the browser to read (tsappkit's island page mounts each island from a registry by name).

A layout template places the slots (elements with data-slot); the Spec says which island goes in each, how it's presented, and its config. The partial templates/page/Islands.html writes it as <script type="application/json" id="page-spec">.

An app that needs more in its spec (the things a page starts with, say) embeds Spec in its own type and writes it with ScriptJSON:

type Spec struct {
	page.Spec
	Things []Thing `json:"things"`
}

func (s Spec) JSON() template.JS {
	c := s
	c.Spec = s.Spec.Normalized()
	return page.ScriptJSON(c)
}

encoding/json flattens the embedded Spec, so the browser sees one object with layout, islands and things.

CheckIslands

func CheckIslands(known []string, specs ...Spec) error

CheckIslands reports every island the specs name that isn't in known, the names the browser's registry can mount. The page itself only finds out at runtime, with a console warning on whichever page names the missing island, so this is for a test that walks an app's specs:

known := append(assets.Names(), "toolbar") // lazy entries, plus those bundled with the entry
if err := page.CheckIslands(known, homeSpec, gameSpec); err != nil {
	t.Fatal(err)
}

It returns nil when every name is known, and otherwise one error listing each unknown name once, sorted, with the layouts that use it. It returns an error rather than taking a *testing.T so the page package doesn't import testing, which would add the test flags to every binary that imports page.

Guide: /goapplib/guide/islands/#checking-and-debugging

ScriptJSON

func ScriptJSON(v any) template.JS

ScriptJSON is v as JSON that's safe inside a <script> element: every '<', '>' and '&' is escaped, so no string in it can close the script or open a comment. A value that can't be encoded is written as null.

Assets

type Assets struct {
	// Entries are the page scripts (esbuild's entry points), by name.
	Entries map[string]Chunk `json:"entries"`
	// Islands are the lazy islands' chunks, by island name.
	Islands map[string]Chunk `json:"islands"`
}

Assets says which built files a page and its islands need, so a page can write <link rel="modulepreload"> for what it loads at once (see For and the IslandPreloads partial in templates/page/Islands.html). Each lazy island (tsappkit's lazy) is its own chunk, and code an island shares with the page's entry script goes into chunks the entry imports. Without preload links the browser only finds those chunks, and then their imports, after the entry has arrived and been parsed.

The JSON form is the shape any bundler's build can write:

{"entries": {"main": {"file": "/static/main.js", "imports": ["/static/chunks/chunk-Z.js"]}},
 "islands": {"hero": {"file": "/static/chunks/hero-X.js", "imports": ["/static/chunks/chunk-Y.js"]}}}

LoadAssets reads that; LoadEsbuildMetafile builds it from esbuild's metafile.

Guide: /goapplib/guide/islands/#lazy-islands-and-preload-links

LoadAssets

func LoadAssets(file string) (*Assets, error)

LoadAssets reads Assets in its JSON form from a file.

LoadEsbuildMetafile

func LoadEsbuildMetafile(file string, opts EsbuildOptions) (*Assets, error)

LoadEsbuildMetafile builds Assets from the metafile of an esbuild build run with --splitting --format=esm --metafile. An output with a source file that some other output reaches through a dynamic import() is an island chunk; any other output with a source file is an entry. Both are named by opts.Name, and their Imports are the chunks they reach through static imports. Two islands, or two entries, with the same name are an error, since the page couldn't tell which one it means.

Guide: /goapplib/guide/islands/#lazy-islands-and-preload-links

Assets.For

func (a *Assets) For(entry string, spec Spec) []string

For lists the chunks to preload on a page whose script is the entry named entry and whose islands are spec's: the entry's imports first, then each island that mounts at once (Load "" or "eager") followed by its imports, in spec order and without repeats. The entry file itself is left out, since the page's <script> loads it. Islands that wait (idle, visible, media) are left out too, since preloading them would download what the page means to put off.

An entry or island Assets doesn't know adds nothing (an island bundled with the entry, say), and a nil Assets gives nothing, so a page can call it whether or not the app loaded a manifest.

Assets.Names

func (a *Assets) Names() []string

Names lists the islands Assets has a chunk for, sorted: the registry's lazy entries, as CheckIslands wants them. Islands bundled with the entry aren't among them. A nil Assets has none.

Chunk

type Chunk struct {
	File    string   `json:"file"`
	Imports []string `json:"imports"`
}

Chunk is one built script and what it needs. File is its URL; Imports are the URLs of every chunk it statically imports, directly or through another chunk, in a stable order. They never include a chunk reached only through a dynamic import(), which is another island and loads when that island does.

EsbuildOptions

type EsbuildOptions struct {
	// OutDir is the output directory as the metafile writes it (the --outdir
	// relative to where esbuild ran, "dist" say). It's cut from the front of
	// each output path before URLPrefix is added.
	OutDir string
	// URLPrefix is where the page serves OutDir, such as "/static/".
	URLPrefix string
	// Name gives the entry or island name for an output's source file (its
	// path as the metafile has it, "web/islands/hero.ts"). The default is the
	// file's base name without its extension ("hero", or "main" for
	// web/main.ts), so a registry entry `hero: lazy(() => import("./islands/hero"))`
	// needs no setting. Returning "" leaves that output out.
	Name func(source string) string
}

EsbuildOptions says how LoadEsbuildMetafile turns the metafile's paths into entry and island names and URLs.

Island

type Island struct {
	Name         string         `json:"name"`
	Slot         string         `json:"slot"`
	Presentation string         `json:"presentation,omitempty"`
	Config       map[string]any `json:"config"`
	// Load says when the browser mounts the island: "eager" (the default,
	// also written as ""), "idle" once the page has settled, "visible" when
	// its slot scrolls into view, or "media:<query>" while the media query
	// matches. Each mounts once. tsappkit's IslandPage (0.6.0 on) waits for
	// it; a deferred island mustn't be something another island or the page
	// needs at startup, since nothing waits for it.
	Load string `json:"load,omitempty"`
}

Island is one island on the page. Name picks the factory from the browser's registry; Slot is the data-slot of the element it mounts in. Presentation says how the layout shows it (an app's own words: "page", "panel", "strip"), for islands that draw differently in each. Config is handed to the factory as is, so it must be JSON.

Guide: /goapplib/guide/islands/#when-each-island-mounts

Spec

type Spec struct {
	// Layout names the arrangement of slots, for code that keeps state per
	// layout. The template that draws it is chosen by the page, not by this.
	Layout  string   `json:"layout"`
	Islands []Island `json:"islands"`
}

Spec is a page's layout and the islands it mounts, in mount order.

Guide: /goapplib/guide/islands/#the-go-side

Spec.JSON

func (s Spec) JSON() template.JS

JSON is the spec for a <script type="application/json"> element, written through ScriptJSON after Normalized.

Spec.Normalized

func (s Spec) Normalized() Spec

Normalized is a copy of the spec with a nil island list as empty and each nil Config as an empty map, so the browser always gets a list and objects rather than null. The spec itself is left alone.

Spec.Slots

func (s Spec) Slots() []string

Slots lists the slots the spec fills, in mount order.

Spec.Validate

func (s Spec) Validate() error

Validate reports a spec the browser couldn't mount as meant: no layout, an island without a name or with a slot name that isn't lowercase letters, digits and dashes, a load that isn't one of Island.Load's forms, or two islands in one slot. An app that extends the spec calls it from its own Validate and adds its own checks.

wasmhost

import "github.com/panyam/goapplib/wasmhost"

Package wasmhost runs an app's HTTP services (typically Connect handlers) as WebAssembly inside a browser Web Worker, answering the same generated clients a server answers.

A page pushes files in by name before it asks anything, and the handler reads them through one fs.FS (Root) whose top-level directories are the mounts. The page side lives in tsappkit (@panyam/tsappkit/wasmhost), which starts the worker and hands the Connect transport a fetch that posts to it.

The rules it encodes, each with a failure behind it (goapplib issue 32):

  • Never block inside a js.FuncOf callback. Every export returns a Promise and does its work on a goroutine, because a callback that waits on something the JS event loop must deliver (a fetch, a timer, a Promise) deadlocks the worker.
  • Push the files in before the request, never pull them during it. fs.FS is synchronous and every browser source of bytes is asynchronous, so the files go into memory first. State a service builds for itself is different: it reads and writes a Cache (BrowserCache in the browser, over the Origin Private File System), whose calls wait on the browser and so are made from a handler's goroutine, which is allowed to wait, never from a js.FuncOf callback.
  • Speak the app's existing wire protocol, so its generated clients don't know whether a server or the worker answered.

Host holds the platform-neutral part (mount table, handler, in-process requests) and builds and tests natively. Serve, ServeRebuild and Host.Export, which talk to JavaScript, are js && wasm only.

Constants and variables

var ErrMiss = errors.New("wasmhost: not in the cache")

ErrMiss is what Get returns for a key the cache has nothing under.

var ErrNoCache = errors.New("wasmhost: no cache in this browser")

ErrNoCache is what BrowserCache's methods return when the browser has no storage to offer the worker (no Origin Private File System, or a worker started without one). A service treats it as a miss and rebuilds.

var ErrNoHandler = errors.New("wasmhost: no handler")

ErrNoHandler is what Do returns before Handle or Rebuild has given the host a handler.

CacheKey

func CacheKey(parts ...[]byte) string

CacheKey is a cache key for inputs: the hex SHA-256 of the parts, each prefixed with its length, so ("ab", "c") and ("a", "bc") get different keys. Put a version string first, so a service that changes what it stores stops finding blobs an older build wrote.

Guide: /goapplib/guide/wasmhost-state/#a-cache-for-state-the-service-can-rebuild

Serve build: js && wasm

func Serve(ns string, handler http.Handler)

Serve answers requests with handler on globalThis[ns] and never returns. It is the whole main of a wasm build whose handler doesn't need the mounted files; one that does builds a Host, so it can pass Root to the handler, and calls Host.Serve.

Guide: /goapplib/guide/wasmhost/#the-pieces

ServeRebuild build: js && wasm

func ServeRebuild(ns string, build func(root fs.FS) (http.Handler, error))

ServeRebuild is Serve for a handler made over the mounted files, rebuilt on every mount change (see Host.Rebuild). A failed first build is logged to the console and reported to each request.

Guide: /goapplib/guide/wasmhost/#fixed-handlers-and-rebuilt-ones

Cache

type Cache interface {
	// Get returns a copy of the blob under key, or ErrMiss. A key that isn't one CacheKey could
	// make is an error rather than a miss.
	Get(ctx context.Context, key string) ([]byte, error)
	// Put stores b under key, replacing what was there. A reader never sees half a blob: it gets
	// the old one, the new one, or ErrMiss. The cache keeps its own copy of b.
	Put(ctx context.Context, key string, b []byte) error
}

Cache keeps state a service builds from its inputs and would rather not build again, such as a parsed design, a model or an index, so a reload or a restarted worker can skip the rebuild. Keys name the inputs (see CacheKey), not the blob, so a service computes the key from a request before it has anything cached, and the same inputs always find the same blob. In the browser it's BrowserCache, over the Origin Private File System, which outlives a reload; natively it's DirCache or MemCache.

Every worker the page starts for the same namespace sees the same BrowserCache, so a throwaway worker (tsappkit's oneShotFetch) can build a result through a large peak and Put it, and the long-lived worker Gets it without ever holding the peak.

It's a cache, not a datastore: anything in it must be rebuildable from its inputs, because a miss, ErrNoCache, a browser clearing the origin's storage, or any other error just means the service builds it again. Data a user would lose belongs in the app's server-side storage.

Get and Put may wait on the browser, so call them from a handler or another goroutine, never from inside a js.FuncOf callback (see the package doc). Implementations are safe for concurrent use.

Guide: /goapplib/guide/wasmhost-state/#a-cache-for-state-the-service-can-rebuild

BrowserCache build: js && wasm

func BrowserCache() Cache

BrowserCache is the Cache tsappkit's worker gives the wasm, over the Origin Private File System (wasmhost/<ns>/ in the page origin's private storage), so a blob Put before a reload is there for Get after it. The worker installs it as globalThis.wasmhostCache before Go starts; without one (no OPFS, or a worker from tsappkit before 0.6.4) every call returns ErrNoCache.

Each call waits on a JS Promise, so it must run on a goroutine, such as a handler's; ctx ends the wait, though the browser may still finish the write.

Guide: /goapplib/guide/wasmhost-state/#a-cache-for-state-the-service-can-rebuild

DirCache build: !(js && wasm)

func DirCache(dir string) Cache

DirCache is a Cache in a directory, one file per key, for a service running natively (on a server, or in tests). The directory is created on the first Put.

Chunk

type Chunk struct {
	Status int
	Header http.Header
	Body   []byte
}

Chunk is part of a response, sent when the handler calls Flush (http.Flusher). The first chunk of a response carries its status and header; later ones carry only body.

Host

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

Host is a mount table and the handler that serves requests over it.

A Host is in one of two modes. After Handle it serves one fixed handler, which reads the live Root and so sees each mount change on its next read. After Rebuild it calls the build function again on every Mount and Unmount, for apps whose handler snapshots the files when it is made. All methods are safe for concurrent use.

Guide: /goapplib/guide/wasmhost/#testing-it-natively

New

func New(ns string) *Host

New returns an empty host whose JavaScript exports, once Export or Serve installs them, live on globalThis[ns].

Host.Add

func (h *Host) Add(name string, files map[string][]byte) error

Add puts files into the mount name, keeping the files it already holds; a file at a path the mount already has replaces it. A missing mount is created, as Mount would. It takes ownership of the byte slices, and in rebuild mode it rebuilds once for the whole batch.

Add checks the whole batch before writing anything, so an error (an invalid path, a path that is already a directory, a file under a file) leaves the mount as it was. With a fixed handler (Handle), a request running at the same moment may see some of the batch and not the rest; in rebuild mode the new handler is built after the last file is in.

Host.Do

func (h *Host) Do(ctx context.Context, req Request) (Response, error)

Do serves req in-process and returns the buffered response. An HTTP error status is a Response, not an error. The error is for no handler (ErrNoHandler), a failed rebuild, an unparseable request, or a handler panic, which is recovered so one bad request can't kill the worker.

Host.DoStream

func (h *Host) DoStream(ctx context.Context, req Request, onChunk func(Chunk)) (res Response, err error)

DoStream is Do for a handler that streams: each time it calls Flush, the bytes it has written since the last Flush go to onChunk, synchronously and in order, while the handler is still running. The returned Response holds only what was written after the last Flush; if anything was flushed, its Status and Header repeat the first chunk's. A nil onChunk buffers the whole response, as Do does. onChunk is called on the handler's goroutine and must not block on it.

Host.Export build: js && wasm

func (h *Host) Export() (release func())

Export installs the host's functions on globalThis[ns], creating that object if it is absent, and then calls globalThis[ns].ready() if the loader put one there. It returns a function that removes the exports and releases them. Serve calls it; tests and custom mains can call it directly.

The exports, all returning Promises:

http(method, url, headers, body, onChunk?, id?)
                                  resolves {status, headers, body: Uint8Array}. With onChunk, each
                                  Flush in the handler calls onChunk({status?, headers?, body})
                                  at once (Host.DoStream), and the final body is what came after
                                  the last flush. With a numeric id, cancel(id) can end the
                                  request's context.
cancel(id)                        cancels the context of request id, if it's still running; a
                                  handler sees it the next time it checks ctx, which a loop that
                                  never yields to JS never gets to do (end its worker instead)
mount(name, {path: Uint8Array})   resolves when the files are mounted (and, in rebuild mode, built)
add(name, {path: Uint8Array})     the same, but keeps the files the mount already holds (Host.Add)
unmount(name)                     resolves when the mount is gone

Arguments are copied into Go before the call returns, so the caller may transfer or reuse its buffers straight away.

Host.Handle

func (h *Host) Handle(handler http.Handler)

Handle makes handler the host's one fixed handler, leaving rebuild mode if it was in it.

Host.Mount

func (h *Host) Mount(name string, files map[string][]byte) error

Mount replaces the mount name with files, keyed by slash-separated path. It takes ownership of the byte slices. In rebuild mode it returns the build's error, and the mount stays in place either way.

Host.Namespace

func (h *Host) Namespace() string

Namespace is the global name the host's exports are installed under.

Host.Rebuild

func (h *Host) Rebuild(build func(root fs.FS) (http.Handler, error)) error

Rebuild puts the host in rebuild mode: build is called now with Root and again after every Mount and Unmount, and its handler serves until the next change. If build fails, requests fail with its error until a later build succeeds. The error from this first build is returned.

Host.Root

func (h *Host) Root() fs.FS

Root is the live mount table as one fs.FS: mount "docs" holding "a/b.txt" reads as "docs/a/b.txt". It is the same value on every call, so a handler can keep it.

Host.Serve build: js && wasm

func (h *Host) Serve(handler http.Handler)

Serve makes handler the host's handler, installs the exports, and never returns.

Host.ServeRebuild build: js && wasm

func (h *Host) ServeRebuild(build func(root fs.FS) (http.Handler, error))

ServeRebuild puts the host in rebuild mode with build, installs the exports, and never returns.

Host.Unmount

func (h *Host) Unmount(name string) error

Unmount removes the mount name, which is not an error if it is absent. In rebuild mode it returns the build's error.

MemCache

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

MemCache is a Cache in memory, for tests and for a service with nowhere to persist. It forgets everything when the process ends.

MemCache.Get

func (m *MemCache) Get(_ context.Context, key string) ([]byte, error)

Get implements Cache.

MemCache.Put

func (m *MemCache) Put(_ context.Context, key string, b []byte) error

Put implements Cache.

Request

type Request struct {
	Method string
	URL    string
	Header http.Header
	Body   []byte
}

Request is one HTTP request as the worker receives it. URL is a path with an optional query ("/pkg.Service/Method?x=1"); the scheme and host the page used don't matter here.

Response

type Response struct {
	Status int
	Header http.Header
	Body   []byte
}

Response is the handler's answer, buffered whole.