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.HandlerCORS adds CORS headers for local development.
DefaultFuncMap
func DefaultFuncMap() template.FuncMapDefaultFuncMap returns the default template functions.
Guide: /goapplib/guide/templates/#template-functions
DefaultRateLimitKeyFunc
func DefaultRateLimitKeyFunc(r *http.Request) stringDefaultRateLimitKeyFunc 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.HandlerGzipFileServer 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) stringHtmxCurrentURL returns the HX-Current-URL header value.
HtmxPrompt
func HtmxPrompt(r *http.Request) stringHtmxPrompt returns the HX-Prompt header value.
HtmxTarget
func HtmxTarget(r *http.Request) stringHtmxTarget returns the HX-Target header value.
HtmxTrigger
func HtmxTrigger(r *http.Request) stringHtmxTrigger returns the HX-Trigger header value.
IsBoostedRequest
func IsBoostedRequest(r *http.Request) boolIsBoostedRequest checks if the request is a boosted link.
IsHtmxRequest
func IsHtmxRequest(r *http.Request) boolIsHtmxRequest 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.ServeMuxRegister 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.ServeMuxRegisterFunc 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.ServeMuxRegisterGroup 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.ServeMuxRegisterHandler registers an http.Handler at the given pattern. Convenience wrapper for mux.Handle.
SetupTemplates
func SetupTemplates(paths ...string) *tmplr.TemplateGroupSetupTemplates 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.ServeMuxSmartRegister 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) errorRenderTemplate 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) stringReturns the formatted DeleteUrl
EntityListingData.EditUrl
func (d *EntityListingData[ItemType]) EditUrl(id string) stringReturns the formatted EditUrl
EntityListingData.ViewUrl
func (d *EntityListingData[ItemType]) ViewUrl(id string) stringReturns 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) *HtmxResponseNewHtmxResponse creates a new HtmxResponse wrapper.
HtmxResponse.Location
func (h *HtmxResponse) Location(url string) *HtmxResponseLocation does a client-side redirect without full page reload.
HtmxResponse.LocationWithContext
func (h *HtmxResponse) LocationWithContext(spec map[string]any) *HtmxResponseLocationWithContext does a redirect with additional context.
HtmxResponse.PushURL
func (h *HtmxResponse) PushURL(url string) *HtmxResponsePushURL pushes a new URL onto the browser's history stack.
HtmxResponse.Redirect
func (h *HtmxResponse) Redirect(url string) *HtmxResponseRedirect tells HTMX to perform a client-side redirect.
HtmxResponse.Refresh
func (h *HtmxResponse) Refresh() *HtmxResponseRefresh tells HTMX to do a full page refresh.
HtmxResponse.ReplaceURL
func (h *HtmxResponse) ReplaceURL(url string) *HtmxResponseReplaceURL replaces the current URL in the browser's history.
HtmxResponse.Reselect
func (h *HtmxResponse) Reselect(selector string) *HtmxResponseReselect changes the selection from the response.
HtmxResponse.Reswap
func (h *HtmxResponse) Reswap(style string) *HtmxResponseReswap changes the swap method.
HtmxResponse.Retarget
func (h *HtmxResponse) Retarget(selector string) *HtmxResponseRetarget changes the target element for the swap.
HtmxResponse.StopPolling
func (h *HtmxResponse) StopPolling() *HtmxResponseStopPolling tells HTMX to stop polling.
HtmxResponse.Trigger
func (h *HtmxResponse) Trigger(event string) *HtmxResponseTrigger sets the HX-Trigger header to trigger a client-side event.
HtmxResponse.TriggerAfterSettle
func (h *HtmxResponse) TriggerAfterSettle(event string) *HtmxResponseTriggerAfterSettle sets HX-Trigger-After-Settle header.
HtmxResponse.TriggerAfterSwap
func (h *HtmxResponse) TriggerAfterSwap(event string) *HtmxResponseTriggerAfterSwap sets HX-Trigger-After-Swap header.
HtmxResponse.TriggerWithData
func (h *HtmxResponse) TriggerWithData(event string, data any) *HtmxResponseTriggerWithData 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.ServeMuxBuild 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.ServeMuxMux 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) OptionWithFragmentTemplate 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) OptionWithMiddleware adds middleware to the handler.
Guide: /goapplib/guide/routing/#register
WithTemplate
func WithTemplate(spec string) OptionWithTemplate 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() *RateLimitConfigDefaultRateLimitConfig 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) *RateLimitMiddlewareNewRateLimitMiddleware creates rate limiting middleware with the given config.
NewRateLimitMiddlewareWithKeyFunc
func NewRateLimitMiddlewareWithKeyFunc(config *RateLimitConfig, keyFunc func(*http.Request) string) *RateLimitMiddlewareNewRateLimitMiddlewareWithKeyFunc creates middleware with a custom key function.
RateLimitMiddleware.Wrap
func (m *RateLimitMiddleware) Wrap(next http.Handler) http.HandlerWrap 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.HandlerWrapAPI wraps an API handler with rate limiting.
RateLimitMiddleware.WrapAuth
func (m *RateLimitMiddleware) WrapAuth(next http.Handler) http.HandlerWrapAuth 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) *RateLimiterNewRateLimiter creates a new rate limiter with the specified limit per window.
RateLimiter.Allow
func (rl *RateLimiter) Allow(key string) boolAllow 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) errorStartWithHandler 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() boolShouldRenderFragment 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() intNextPage returns the next page number.
WithPagination.Offset
func (p *WithPagination) Offset() intOffset returns the offset for database queries.
WithPagination.Paginator
func (p *WithPagination) Paginator() *WithPaginationPaginator 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() intPrevPage 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() intTotalPages 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) errorCheckIslands 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.JSScriptJSON 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) []stringFor 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() []stringNames 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.JSJSON is the spec for a <script type="application/json"> element, written through ScriptJSON after Normalized.
Spec.Normalized
func (s Spec) Normalized() SpecNormalized 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() []stringSlots lists the slots the spec fills, in mount order.
Spec.Validate
func (s Spec) Validate() errorValidate 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) stringCacheKey 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() CacheBrowserCache 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) CacheDirCache 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) *HostNew 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) errorAdd 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) errorMount 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() stringNamespace 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)) errorRebuild 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.FSRoot 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) errorUnmount 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) errorPut 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.