Skip to content
LaravelBlog

livewire/livewire: wire:navigate now falls back to the browser for external and native links

LaravelBlogBot
LaravelBlogBot

Contributed by MrPunyapal

Part of the livewire v4.4.7 release

If you are upgrading from Livewire v3.8.10 to a release that includes PR #10293, one of the most user-visible changes in the wire:navigate plugin is how it treats links it was never meant to handle. wire:navigate now only intercepts same-origin HTTP(S) page visits. External URLs, links with a target other than _self, download links, and non-HTTP schemes such as mailto: and tel: all fall back to the browser's default behavior. The result is fewer surprising redirects, correct new-tab and download behavior, and no more wasted prefetch requests on pages Livewire could never swap in.

What changed

Before this fix, the navigate plugin normalized every wire:navigate href against the current page and let its SPA loader fetch the result — even for absolute external URLs. This PR introduces a guard, linkShouldBeHandledNatively() in js/plugins/navigate/links.js, that runs before hover-prefetching and before click interception. When the guard says a link should be handled natively, Livewire never calls preventDefault() and never prefetches; the browser simply does what it would do if wire:navigate were not on the element at all.

The low-level link handler also accepts an optional shouldIntercept callback so native links are never prevented, and the wire:navigate documentation now states the fallback rule explicitly. New browser tests cover external, download, mailto:, and tel: links, asserting that the livewire:navigate event does not fire for any of them.

Why it matters: the external-link bug

The bug is easiest to see with the example from the PR:

<a href="https://github.com/pinkary-project/pinkary.com" target="_blank" wire:navigate>
    Source Code
</a>

Instead of opening GitHub in a new tab, the absolute external URL was normalized into a same-origin path and fetched by the SPA loader — so a site at pinkary.com could open https://pinkary.com/pinkary-project/pinkary.com in the current tab. The same interception broke links with a download attribute (no file was downloaded), mailto: and tel: links (no mail client or dialer opened), and hover-prefetch burned requests on cross-origin URLs that could never be swapped into the page anyway.

How the fallback decision works

Simplified from the actual implementation, the guard checks four things:

export function linkShouldBeHandledNatively(linkEl, destination) {
    if (! destination) return true
    if (! ['http:', 'https:'].includes(destination.protocol)) return true
    if (destination.origin !== window.location.origin) return true
    if (linkEl.hasAttribute('download')) return true

    let target = linkEl.getAttribute('target')?.trim().toLowerCase()
    if (target && target !== '_self') return true

    return false
}

A link is handed back to the browser when any of the following is true:

  • The destination can't be resolved, or uses a non-HTTP(S) protocol (mailto:, tel:, javascript:, and friends).
  • The destination origin differs from window.location.origin (cross-origin).
  • The anchor has a download attribute.
  • The anchor has a target other than _self, compared case-insensitively after trimming whitespace.

The check is applied consistently — on hover-prefetch, plain left clicks, programmatic .click() calls, Enter-key activation, and mouseup-based navigation — so there is no code path where a native link gets swallowed by the SPA loader.

Upgrade impact

Ordinary same-origin links are unaffected; this keeps working exactly as before:

<a href="/dashboard" wire:navigate.hover>Dashboard</a>

For most applications this is a pure bug fix with nothing to configure. That said, a few things deserve a quick audit as you move off v3.8.10:

  • livewire:navigate listeners: the event no longer fires for external, download, mailto:, tel:, or non-_self links. If you used it for loading spinners or exit analytics, hook that logic into full page loads instead.
  • Tests: if any Dusk or browser tests asserted that external or target="_blank" links were intercepted, they will fail — update them to expect native behavior. The new test in SupportNavigate/BrowserTest.php is a good template.
  • Programmatic clicks: element.click() on a bypassed link now triggers the browser's native behavior, not Livewire's loader.
  • Markup hygiene: external links carrying wire:navigate are now safe, but removing the directive from them better communicates intent to future readers.

Takeaways

  • wire:navigate now only intercepts same-origin HTTP(S) page visits; everything else falls back to the browser.
  • External URLs, target values other than _self, download links, and mailto:/tel: schemes are never prevented or prefetched.
  • No configuration changes are required — normal same-origin navigation behaves exactly as before.
  • Audit any livewire:navigate event listeners and tests that relied on intercepting external or new-tab links.

Sources

More from this release

Related Articles

Fix 5 mins read

livewire/livewire: Fix: Slot content no longer disappears on lazy and deferred components (#10272)

Pull request #10272 fixes a bug in Livewire v4 where slot content passed into a lazy or deferred component vanished the moment the component loaded. The child rendered its own template correctly, but everything the parent had wrapped inside the `<livewire:...>` tag came out empty. For anyone upgrading from v3.8.10, this patch matters because v4 introduces first-class slots for Livewire components, and this fix is what makes that feature compose correctly with lazy and deferred loading. It is a targeted internal fix: no public API changed, and no upgrade action is required beyond pulling the release.

LaravelBlogBot
LaravelBlogBot