Internal navigation is written two ways with no rule separating them, so some links navigate client side and some do a full page load, and which you get depends on who wrote the line.
Full sweep of resources/**/*.stx in hq.training, 330 templates:
StxLink 332
internal <a href> 49
external 9
in-page #hash 10
mailto / tel 3
dynamic expression 180So StxLink is overwhelmingly the house idiom and the 49 are the exception.
It is not a policy
The same destinations are reached both ways, in the same application:
/coaches/athletes is an StxLink in components/AccountHome.stx, components/CoachAthleteBrowser.stx, components/coach-library.stx, views/today.stx and views/import.stx, and a plain anchor in views/coaches/index.stx (twice) and components/SettingsWebsite.stx.
/profile is an StxLink in components/AccountHome.stx and components/app-nav.stx (twice), and a plain anchor in views/metrics.stx.
Five files use both forms within the same file:
| file | internal anchors | StxLink |
|---|---|---|
views/[username].stx | 6 | 1 |
views/profile.stx | 4 | 2 |
views/coaches/index.stx | 2 | 5 |
components/SettingsWebsite.stx | 2 | 6 |
views/metrics.stx | 1 | 2 |
and 22 more use internal anchors with no StxLink at all.
It is not unmigrated legacy either
StxLink has been present since the original "convert to stacks app" commit, and 138 commits have touched it since. The files still carrying anchors were last edited on 2026-10-15, 2026-10-29 and 2026-10-30. This is current code being written this way now, not old code nobody has got to.
The actual cause
Nothing enforces it. There is no rule in the repository's lint configuration, and pickier does not ship one. Every link is whatever its author typed that day, and both forms work, so nothing ever corrects it.
Suggested fix
A lint rule is worth more here than a sweep. A one-off conversion of the 49 will drift again, which the October edits above already demonstrate. Something along the lines of "an <a> whose href begins with / inside a template should be an StxLink", with an allowance for the deliberate exceptions:
- the footer language switcher (
components/LanguageSwitch.stx), whose anchors are query-string and callpreventDefaultanyway, with the realhrefkept for middle-click and for people without JS - genuinely cross-shell links, if any are intended to force a full load
Then convert the 49 in one pass behind the rule.
Related: #24 covers workspace-nav.stx specifically, which is the one navigation component entirely on anchors. This issue is the general case.
Correcting my own earlier count in #24: a rough grep gave 392 and 250. Parsing properly, excluding external, hash, mailto and dynamic hrefs, it is 332 and 49. The conclusion in #24 is unchanged.