Environment
- Package:
ostrio:[email protected] (also reproducible on 3.14.x)
- Internal router:
MicroRouter (introduced in 3.14, replacing page.js)
- Browser: any (Chromium / Firefox), checked on Linux + macOS
- Meteor: 3.4.x (irrelevant to the bug, but for context)
Summary
Any in-page anchor without a JS handler, e.g. a "back to top" button rendered as <a href="#">…</a>, no longer scrolls in place — it now triggers a
real navigation to /, sending the user to the root route.
Under page.js (≤ 3.13.x) the same markup was a no-op (page.js skipped fragment-only links), so this is a regression after the MicroRouter migration.
Steps to reproduce
- Define any non-root route, e.g.
FlowRouter.route('/profile', …).
- In the rendered template put a plain back-to-top anchor:
<a href="#"><i class="fa fa-arrow-up"></i></a>
(no event.preventDefault(), no JS click handler at all).
- Visit
/profile and click the anchor.
Expected: browser scrolls to top of /profile, URL stays /profile (page.js behaviour).
Observed: MicroRouter.show('/') is dispatched, the user is taken to the / route. The URL bar shows /.
Root cause
MicroRouter._onClick (in lib/micro-router.js) resolves the link's href against window.location.origin instead of window.location.href:
// lib/micro-router.js — _onClick
const linkUrl = new URL(href, window.location.origin);
For an anchor like href="#", the relative reference is fragment-only. Per RFC 3986 / WHATWG URL, it should resolve against the current document
URL (path + query preserved). But the base passed here has no path — it's just https://example.com — so:
new URL('#', 'https://example.com').pathname === '/'
The computed path is therefore '/', not '/profile#'. _isHashOnlyChange('/') then compares current.pathname='/profile' vs
next.pathname='/' and returns false (different pathnames), so the early return doesn't fire and event.preventDefault() + this.show('/') runs.
This affects every <a href="#"> that doesn't have a separate JS handler calling event.preventDefault() — common pattern for back-to-top buttons,
dropdown placeholders without Bootstrap data-* hooks, etc.
Proposed fix
Use the full current URL as base when resolving the href:
- const linkUrl = new URL(href, window.location.origin);
+ const linkUrl = new URL(href, window.location.href);
With that change, new URL('#', 'https://example.com/profile') correctly yields pathname: '/profile', and _isHashOnlyChange() returns true —
MicroRouter bails out and the browser handles the fragment natively, matching the legacy page.js behaviour.
I haven't seen a downside in local testing: same-origin links with absolute paths (/x) and same-origin absolute URLs are unaffected (new URL
ignores the base when the input is already absolute). Cross-origin links still get filtered by the existing if (linkUrl.origin !== window.location.origin) return; check.
Minimal repro
// any route file
FlowRouter.route('/profile', {
action() { this.render('layout', 'profile'); },
});
<!-- profile.html -->
<template name="profile">
<h1>Profile</h1>
<a href="#">back to top</a>
</template>
Click the anchor on /profile → routed to /.
Workaround for users until released
Either:
- Replace
<a href="#"> with <button type="button"> plus a JS click handler that calls window.scrollTo(...) — semantically correct and
bypasses the click interceptor entirely.
- Add a JS click handler on the anchor that calls
event.preventDefault() (MicroRouter early-returns on event.defaultPrevented).
This report is redacted with the help of Claude code.
Environment
ostrio:[email protected](also reproducible on 3.14.x)MicroRouter(introduced in 3.14, replacing page.js)Summary
Any in-page anchor without a JS handler, e.g. a "back to top" button rendered as
<a href="#">…</a>, no longer scrolls in place — it now triggers areal navigation to
/, sending the user to the root route.Under page.js (≤ 3.13.x) the same markup was a no-op (page.js skipped fragment-only links), so this is a regression after the MicroRouter migration.
Steps to reproduce
FlowRouter.route('/profile', …).event.preventDefault(), no JS click handler at all)./profileand click the anchor.Expected: browser scrolls to top of
/profile, URL stays/profile(page.js behaviour).Observed:
MicroRouter.show('/')is dispatched, the user is taken to the/route. The URL bar shows/.Root cause
MicroRouter._onClick(inlib/micro-router.js) resolves the link'shrefagainstwindow.location.origininstead ofwindow.location.href:For an anchor like
href="#", the relative reference is fragment-only. Per RFC 3986 / WHATWG URL, it should resolve against the current documentURL (path + query preserved). But the base passed here has no path — it's just
https://example.com— so:The computed
pathis therefore'/', not'/profile#'._isHashOnlyChange('/')then comparescurrent.pathname='/profile'vsnext.pathname='/'and returnsfalse(different pathnames), so the early return doesn't fire andevent.preventDefault() + this.show('/')runs.This affects every
<a href="#">that doesn't have a separate JS handler callingevent.preventDefault()— common pattern for back-to-top buttons,dropdown placeholders without Bootstrap data-* hooks, etc.
Proposed fix
Use the full current URL as base when resolving the href:
With that change,
new URL('#', 'https://example.com/profile')correctly yieldspathname: '/profile', and_isHashOnlyChange()returnstrue—MicroRouter bails out and the browser handles the fragment natively, matching the legacy page.js behaviour.
I haven't seen a downside in local testing: same-origin links with absolute paths (
/x) and same-origin absolute URLs are unaffected (new URLignores the base when the input is already absolute). Cross-origin links still get filtered by the existing
if (linkUrl.origin !== window.location.origin) return;check.Minimal repro
Click the anchor on
/profile→ routed to/.Workaround for users until released
Either:
<a href="#">with<button type="button">plus a JS click handler that callswindow.scrollTo(...)— semantically correct andbypasses the click interceptor entirely.
event.preventDefault()(MicroRouter early-returns onevent.defaultPrevented).This report is redacted with the help of Claude code.