The filter and sort state lives in the URL's query string, so a filtered view is
bookmarkable and survives a reload. Filter links are SPA navigations that update the
history entry; Mount reads status and sort from the query params on every GET and
recomputes the list, falling back to defaults for unknown values so stale bookmarks
still render.
URL-Preserved Filters
Filter state lives in the URL. SPA link clicks update history; direct-loading a bookmark restores the view. Mount() reads query params.
Current: status=all, sort=name
Name
Status
Date
Audit access controls
completed
2024-08-19
Design homepage
completed
2024-02-14
Draft Q1 proposal
completed
2024-03-01
Host team offsite
completed
2024-06-12
Launch mobile app
active
2024-11-11
Migrate legacy database
active
2024-04-02
Refactor billing module
active
2024-09-28
Review authentication spec
active
2024-03-15
Ship payments beta
active
2024-07-04
Upgrade CI pipeline
active
2024-05-08
Write onboarding docs
completed
2024-04-20
Year-end retrospective
active
2024-12-30
Template
The filter and sort controls are ordinary <a href="?status=…&sort=…"> links — each one
carries the full query string and marks itself aria-current="page" when active. The
table ranges over .Items.
Mount only reads query params on a GET navigation (ctx.Action() == ""), validates
them against allow-lists, and always recomputes Items so both the initial render and
later actions see fresh data.
type URLFiltersController struct{}
// validStatuses / validSorts allow Mount to reject unknown query param values
// without crashing: unknown values fall back to the previous/default state
// rather than producing a 404 or an error. Bookmarks with stale params still
// render usefully.
var (
validStatuses = map[string]bool{"all": true, "active": true, "completed": true}
validSorts = map[string]bool{"name": true, "date": true}
)
func (c *URLFiltersController) Mount(state URLFiltersState, ctx *livetemplate.Context) (URLFiltersState, error) {
// ctx.Action() == "" means this is a GET navigation (initial load or SPA
// link click), not a POST action. Only read URL query params on GET to
// avoid clobbering state from an in-flight action.
if ctx.Action() == "" {
if s := ctx.GetString("status"); s != "" && validStatuses[s] {
state.Status = s
}
if s := ctx.GetString("sort"); s != "" && validSorts[s] {
state.Sort = s
}
}
// Always recompute the item list so the initial render (with defaults)
// and any subsequent action both see fresh data.
state.Items = filterItems(state.Status, state.Sort)
return state, nil
}
func urlFiltersHandler() http.Handler {
tmpl := newLayoutTmpl("templates/layout.tmpl", "templates/search/url-filters.tmpl")
return tmpl.Handle(&URLFiltersController{}, livetemplate.AsState(&URLFiltersState{
Title: "URL-Preserved Filters",
Category: "Search & Filtering",
Status: "all",
Sort: "name",
}))
}
// URLFiltersState holds the state for the URL-Preserved Filters pattern (#13).
type URLFiltersState struct {
Title string
Category string
Status string
Sort string
Items []FilterItem
}