Skip to content

Releases: jackielii/structpages

v0.6.3 — path-based element ids

Choose a tag to compare

@jackielii jackielii released this 03 Jun 12:21

Highlights

Path-based ID / IDTarget. Element ids are now derived from a page's full field-name path from the root, not just the leaf mount name. This fixes id collisions between distinct pages that previously collapsed to the same id — most notably two same-named types from different packages, or the same field name mounted under different parents. Each id is a pure function of that node's own path, so it's stable: it changes only when that node is renamed or moved, never because of an unrelated sibling edit.

Standalone functions are package-prefixed. ID(ctx, UserWidget) now returns "<package>-user-widget", so two same-named function components in different packages stay distinct.

WithMaxIDLength option. Tunes the character budget (default 40) before an id degrades from the readable full-path form (admin-users-user-list) to the compact leaf-only form (user-list, plus a stable hash suffix when the leaf name is shared).

Ref ambiguity is now an error. A qualified Ref("Widget.List") that matches more than one mount reports an ambiguity error (listing the routes) instead of silently resolving to the first match.

⚠️ Behavior change

Nested-page ids change shape — they now include their ancestor prefix (e.g. detail-pageadmin-users-detail-page). Apps generate both the id attribute and the hx-target through ID/IDTarget, so HTMX stays consistent automatically, but any hardcoded id strings in CSS/JS/templates referencing a nested component must be updated. Top-level page ids are unchanged.

Full Changelog: v0.6.2...v0.6.3

tools/lint/v0.1.11 — -tags flag

Choose a tag to compare

@jackielii jackielii released this 03 Jun 14:24

Adds a -tags flag to structpages-lint:

structpages-lint -tags devtools ./...

Loads packages under the given build tags so that URLFor / ID call sites in always-compiled code that reference routes mounted only behind a build tag (e.g. a dev-tools-only page group) resolve against the tree that actually mounts them — eliminating false-positive "cannot resolve" diagnostics for tag-gated routes.

Full Changelog: tools/lint/v0.1.10...tools/lint/v0.1.11

tools/lint/v0.1.10 — per-binary urlfor scope

Choose a tag to compare

@jackielii jackielii released this 03 Jun 12:21

Scopes the URLFor ambiguity analysis per main binary. A page type reachable from one command no longer triggers false-positive ambiguity reports against an unrelated binary in the same module.

Full Changelog: tools/lint/v0.1.9...tools/lint/v0.1.10

v0.6.0 — mount-aware ID/IDTarget + []any chain form

Choose a tag to compare

@jackielii jackielii released this 25 May 11:30

Summary

ID / IDTarget gain mount-aware semantics. Four fixes to existing primitives — no new public API surface.

What's new

Self-render uses the current mount

ID(ctx, p.M) / IDTarget(ctx, p.M) now consult the current request's page node. The same struct mounted under multiple parents with different field names emits per-mount ids correctly.

type root struct {
    AdminDash dashboardPage `route:"/admin"`
    UserDash  dashboardPage `route:"/user"`
}
// Before: both mounts emit "#admin-dash-header" (first match wins).
// After:  admin emits "#admin-dash-header", user emits "#user-dash-header".

Cross-page ambiguity error

When the receiver type is mounted multiple times and the call has no matching current-page context:

  • Identical kebab ids (entryPage pattern: three mounts all named EntryDetail) → return silently.
  • Divergent kebab ids → error listing the available mounts and three disambiguation primitives.

[]any chain form for ID / IDTarget

Mirrors URLFor's chain shape, with the trailing element holding the method spec:

IDTarget(ctx, []any{adminRoot{}, dashboardPage{}, "Header"})  // chain + string
IDTarget(ctx, []any{adminRoot{}, dashboardPage.Header})       // chain + method expr

When the trailing method expression's receiver type matches the prior chain step's type, the implicit step collapses.

Disambiguation primitives

Three coherent options when ambiguity arises:

Want Use
per-mount id, self-render method expression — Fix #1 makes this reliable
type-stable id (overlay slot) standalone function func MySlot() templ.Component { ... }
target specific mount, type-safe []any{Parent{}, Leaf{}, "Method"} chain
target across package, can't import Ref("Parent.Field.Method")

Benchmarks

New BenchmarkURLGenerationV05 and BenchmarkIDGenerationV06 suites under bench/ cover the v0.5.0 and v0.6.0 surfaces. Baselines saved to bench/before-main.txt and bench/after-v06.txt.

Tooling

tools/lint/v0.1.6 ships alongside with matching analyzer support — see https://github.com/jackielii/structpages/releases/tag/tools%2Flint%2Fv0.1.6

Full diff

v0.3.0...v0.6.0

tools/lint/v0.1.7 — params check honours chain trailing fragments

Choose a tag to compare

@jackielii jackielii released this 25 May 11:46

Summary

Fixes a false-positive [params] diagnostic on URLFor calls that compose path and query placeholders via the []any chain form.

What was wrong

URLFor(ctx,
    []any{Page{}, \"?preset={preset}\"},
    map[string]any{\"slug\": \"x\", \"preset\": \"weekly\"})

The lint analyzer parsed placeholders only from node.FullRoute (the page's route path) and ignored placeholders supplied by the trailing string fragments of the chain. preset looked unknown, so the call emitted a false-positive [params] diagnostic and users had to add // structpages:lint:ignore params.

What changed

  • resolvePageArgAndFragment returns the concatenated trailing fragments alongside the chain leaf.
  • checkParamMap merges them into the pattern before extracting placeholders.
  • Error messages now show the full effective pattern (path + fragment) and the known-keys list includes fragment placeholders.

Install

go install github.com/jackielii/structpages/tools/lint/cmd/structpages-lint@v0.1.7

For his-project

The // structpages:lint:ignore params in modules/receptionist/pages/analytics_urls.go is no longer needed after upgrading.

Full diff

tools/lint/v0.1.6...tools/lint/v0.1.7

tools/lint/v0.1.6 — ID-context Ref + chain form analyzer

Choose a tag to compare

@jackielii jackielii released this 25 May 11:31

Summary

Matches structpages v0.6.0 semantic changes plus a lint↔runtime fix that's been outstanding since the analyzer landed.

What's new

Lint mirrors runtime for ID-context qualified Ref

The runtime's idForRef walks the whole page tree by Name when resolving a qualified Ref like Ref("EntryDetail.Overlays"). The lint analyzer was anchoring only at root or root.Children — so valid ID-context Refs whose anchor sits deeper in the tree errored at lint time even though the runtime accepted them. The asymmetry forced suppressions in cross-package slot-target code.

  • findRefAnchor switches behavior on isIDContext: whole-tree walk for ID, root + children for URL.
  • checkRefConversion only emits when both URL and ID contexts fail (a real typo). Per-context failures surface at the call site instead.
  • URLFor's call site no longer silently swallows Ref errors — it emits URL-context errors that the permissive conversion check intentionally lets through.

[]any chain form for ID / IDTarget

End-to-end validation of the new []any{Parent{}, Leaf{}, "Method"} and []any{Parent{}, Leaf{}.Method} shapes — mirroring the URLFor chain analyzer.

Install

go install github.com/jackielii/structpages/tools/lint/cmd/structpages-lint@v0.1.6

Full diff

tools/lint/v0.1.5...tools/lint/v0.1.6

v0.3.0

Choose a tag to compare

@jackielii jackielii released this 23 May 15:32

What's Changed

  • feat(lint): static analyzer for URLFor / ID / IDTarget / Ref by @jackielii in #10

Full Changelog: v0.2.0...v0.3.0

v0.2.0 — strict URLFor + chain form + Ref qualified path

Choose a tag to compare

@jackielii jackielii released this 18 May 19:49

Closes #8.

URLFor(ctx, SomeType{}) previously returned the first match silently when the same page type was mounted under multiple parents — production traffic going to the wrong page, no error, no warning. This release fixes that and adds the type-safe disambiguation primitive.

Highlights

Strict URLFor — no opt-out. Bare type lookups that match multiple nodes now error with every match listed. There is no `WithLenientURLFor` escape hatch; silent first-match is always wrong, so disambiguating at the call site is mandatory.

`[]any{...}` chain form. Inside the composition slice, leading typed values now form a chain through the page tree (each typed value descends into a child of that type). Strings still concat literally as URL fragments. Typed values after a string fragment are rejected.

```go
// Before: ambiguous, silently returned first match
url, _ := structpages.URLFor(ctx, entryPage{}, map[string]any{"slug": "button"})

// After: chain disambiguates by anchoring on the unique parent
url, _ := structpages.URLFor(ctx,
[]any{componentsRoot{}, entryPage{}},
map[string]any{"slug": "button"})
// → "/components/button"
```

`Ref` qualified path. `Ref("Parent.Field")` and `Ref("Grand.Parent.Field")` walk down by `PageNode.Name`. Existing `Ref("Name")` and `Ref("/route")` forms unchanged. This is the cross-package fallback for callers that can't import the typed page.

Recommended call shape: `URLFor(ctx, page, params)` with `map[string]any` for params.

Breaking changes

  • Bare `URLFor(ctx, T{})` where `T` is mounted under multiple parents now errors. Migrate to `[]any{parent, T{}}` chain form or `Ref("Parent.Field")`.
  • `[]any{TypedA, TypedB}` previously concatenated each one's FullRoute independently. It now forms a chain through the tree. No known callers in this repo or examples relied on the old semantics; if you did, switch to `[]any{TypedA, "/literal-path"}` or compose the routes manually.

Validation patterns

The new `examples/url-validation/` directory ships the full pattern for keeping URLs from drifting silently in production:

  • `urls.go` — typed helper per URL family
  • `validate.go` — `validateURLs(sp)` exercising every helper at boot; `main.go` calls it after `Mount` so a broken deploy refuses to start serving
  • `integration_test.go` — renders every page and asserts URLs in the body; runs in CI

`skills/structpages/SKILL.md` §3 rewritten with the form table, chain semantics, and the validation pattern.

Migration cookbook

If you hit the strict-mode error in your app:

  1. Bare lookup of a uniquely-mounted type still works — most call sites need no change.
  2. Same type under multiple parents — switch to `[]any{parentType{}, leafType{}}`.
  3. Can't import the target page (cross-package) — switch to `structpages.Ref("Parent.Field")`.
  4. Validate every URL at boot — copy the `validateURLs` pattern from `examples/url-validation/validate.go`.

Full PR discussion: #9

v0.1.10

Choose a tag to compare

@jackielii jackielii released this 11 May 15:29

Features

  • WithURLPrefix(prefix string) option: tells structpages it is being served behind a path prefix that's stripped before requests reach the registered routes (e.g., http.StripPrefix or a reverse proxy). The prefix is prepended to every URL returned by URLFor — both *StructPages.URLFor and the request-context URLFor(ctx, ...) — without affecting route registration.

    inner := http.NewServeMux()
    sp, _ := structpages.Mount(inner, pages{}, "/", "App",
        structpages.WithURLPrefix("/admin"))
    outer := http.NewServeMux()
    outer.Handle("/admin/", http.StripPrefix("/admin", inner))
    // sp.URLFor(home{}) → "/admin"
    // sp.URLFor(users{}) → "/admin/users"

Tests

  • TestMountAtSubpath locks in the existing behavior of mounting via Mount's route arg (e.g., Mount(mux, pages, "/admin", "App")).
  • TestMountUnderOuterMux covers both integration shapes: outer mux with StripPrefix + WithURLPrefix, and outer mux without stripping where the inner Mount registers at the prefix directly.

v0.1.9 — htmx 4 support, new examples, and Claude Code skill

Choose a tag to compare

@jackielii jackielii released this 07 May 18:25

New

  • HTMXv4RenderTarget (htmx.go) — opt-in TargetSelector for htmx 4 frontends. Handles htmx 4's reshaped headers:
    • HX-Target now arrives as "<tag>#<id>" (or bare "<tag>"); the selector prefers the id when present, falls back to the tag, then applies the existing component-matching rules.
    • HX-Request-Type: full is honored as a hard hint to render Page (covers <body> swaps and hx-select).
    • HX-Source is htmx 4's trigger header — it identifies the trigger, not the swap target, so it is intentionally not used for component routing.
    • Wire it via WithTargetSelector(structpages.HTMXv4RenderTarget). The default HTMXRenderTarget is unchanged and remains correct for htmx 1.x/2.x.
    • See: https://four.htmx.org/reference/#headers

Examples

  • examples/blog — comprehensive blog example with module-based component layout (atoms/molecules/layout), demonstrating Props, partial rendering, and URLFor end-to-end (#5).
  • examples/html-template — new example showing structpages with the stdlib html/template engine (no Templ), including layout/partial composition and binding urlFor per request (#4, #6, #7).

Documentation & tooling

  • New skills/structpages/ Claude Code skill (SKILL.md, reference.md, examples.md) plus a .claude-plugin/plugin.json manifest, so library users can install structpages as a Claude Code plugin.
  • Docs realigned with current source; htmx and IDFor docs corrected.

Internal

  • Minor refactor in formatPathSegments (url_for.go); behavior unchanged.
  • golangci-lint cleanups; CI lint fix.

Upgrade

go get -u github.com/jackielii/structpages@v0.1.9

No breaking changes. HTMXRenderTarget users do not need to migrate; adopt HTMXv4RenderTarget only when serving an htmx 4 frontend.

Full Changelog: v0.1.8...v0.1.9