Skip to content

Bug: <a href="#"> always navigates to / (drops current path) — MicroRouter regression vs page.js #129

@bricous

Description

@bricous

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

  1. Define any non-root route, e.g. FlowRouter.route('/profile', …).
  2. 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).
  3. 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:

  1. Replace <a href="#"> with <button type="button"> plus a JS click handler that calls window.scrollTo(...) — semantically correct and
    bypasses the click interceptor entirely.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions