Commit Graph

28 Commits

Author SHA1 Message Date
James Anderson b0367f1448 fix(link): avoid reusing dynamic app route prefetches (#2450)
* fix(link): avoid reusing dynamic app route prefetches

* fix(link): preserve static dynamic app prefetches

* test(e2e): scope segment cache client params to prod
2026-07-01 11:14:11 +01:00
James Anderson b91d5d1396 fix(pages): interpolate same-segment rewrite params (#2030)
* fix(pages): interpolate same-segment rewrite params

* test(pages): fix same-segment navigation CI
2026-06-15 21:49:28 +00:00
James Anderson 866c71809e fix(pages): preserve rewritten URLs during navigation (#2028)
* fix(router): preserve rewritten pages query navigation

* fix(router): align pages url object locale handling

* fix(router): preserve raw pages url search

* fix(router): escape fragments in raw pages search

* fix(router): preserve localized rewritten pages urls

* test(router): fix pages navigation CI failures

* fix(router): preserve Next search fragment formatting
2026-06-15 22:34:36 +01:00
Nathan Nguyen 10b5086f4d fix(app-router): align router autoscroll with Next (#2004)
* fix(app-router): align router autoscroll with Next

App Router navigation could diverge from Next.js router autoscroll semantics around hydration timing, loading-shell same-page search navigations, and routes whose first committed node is React-hoisted into head. That broke the upstream router-autoscroll deploy-suite because the client marked hydration too early, static loading-shell templates were not usable for navigation, and the fallback scroll path masked the old-handler hoisted-head behavior.

The fix moves the Next-compatible hydration marker into a passive effect, wires production hydrateRoot error callbacks to match Next's implicit root-boundary and recoverable-error handling, lets authoritative loading-shell templates drive static route optimistic payloads, and skips fallback document scrolling when the committed document only exposes a React-hoisted head resource. The ported tests cover the upstream router-autoscroll behavior plus focused unit boundaries for prefetch mode, optimistic routing, head-resource detection, and hydrateRoot callbacks.

* fix(app-router): hoisted-head scroll fallback is per-intent, not global head scan

* test(app-router): add full-chain scroll intent integration test

* fix(app-router): match legacy scroll focus parity

* fix(app-router): preserve latest scroll navigation intent

* fix(app-router): scope scroll and error recovery

* fix(app-router): gate navigation failure recovery

* fix(app-router): match navigation failure handling

* fix(app-router): align scroll completion parity

* fix(app-router): retain latest failure target

* fix(app-router): preserve route error boundaries

* fix(app-router): clear discarded failure targets

* fix(app-router): align failure recovery boundaries

* fix(app-router): disarm refused recovery targets

* fix(app-router): preserve failure target ownership

* fix(app-router): match boundary recovery parity

---------

Co-authored-by: James <james@eli.cx>
2026-06-14 23:59:28 +01:00
James Anderson 3f6c174c20 fix(app-router): resolve query-only Link href against current path (#1540) (#1844)
A <Link href={{ query: {...} }} /> with no `pathname` was resolved by
defaulting the base to "/", producing "/?query" instead of a query-only
href. The shallow navigation then recorded the wrong history entry (the
site root) so back/forward traversal landed on the wrong URL/page.

Mirror Next.js's formatUrl() (`pathname = urlObj.pathname || ''`): when
`pathname` is omitted, emit a relative query-only href (`?query`) so the
existing relative-href resolution applies it against the current path.

Adds a ported e2e regression (middleware-rewritten shallow link history
traversal) plus a Link unit test.

Closes #1540
2026-06-08 19:30:07 +01:00
Nathan Nguyen cb27f3a682 fix(link): full-prefetch dynamic routes without loading shells (#1734)
* fix(link): full-prefetch dynamic routes without loading shells

Automatic App Router prefetch skipped dynamic routes unless they exposed a loading boundary. That diverged from Next.js segment-cache behavior for client-param routes, where making the link visible must initiate a prefetch and a click to the exact URL can reuse the prefetched payload without another request.

The bad assumption was that every dynamic route needs shell-only learning or no prefetch. Vinext keys the prefetched RSC snapshot by the exact target URL, so dynamic routes without a loading shell can safely seed navigation cache while dynamic routes with loading.tsx keep the existing shell-only path.

Port the focused Next.js client-params regression into the Link decision and scheduling tests.

* fix(link): skip shell-first inlining for client-param prefetches

Automatic dynamic routes without loading shells now seed the navigation cache, but the prefetch-inlining branch still treated those cacheable requests like static route payloads and issued a loading-shell request before the full fetch.

That shell request has no loading boundary to learn from for client-param routes, so it is a wasted roundtrip. Split cache eligibility from shell-first transport eligibility, document the route-shape heuristic, and derive the test-only full-prefetch helper from the same decision.

Add coverage that dynamic no-loading routes remain single-fetch under prefetch inlining.
2026-06-05 11:25:17 +01:00
James Anderson f4d13e25b9 fix(i18n): make locale sticky across client navigations (#1407)
* fix(i18n): make Pages Router locale sticky across client navigations

Refs #1336 (item 2).

Writes a Next.js-compatible history state shape on every router
push/replace, captures the active locale in `state.options.locale`, and
teaches the popstate handler to:

1. Ignore history entries written by third-party code (no `__N: true`).
2. Drop the first popstate when it carries the same locale + `as` as the
   current page (Safari tab-restore / BFCache replay parity).
3. Honour the locale recorded in history state when re-fetching the page
   on back/forward, so default-locale roots still go through their
   locale-qualified HTML endpoint.

This matches the Next.js behaviour exercised by
`test/e2e/ignore-invalid-popstateevent` and the client half of
`test/e2e/i18n-preferred-locale-detection`.

Scope boundary: the server-side `Set-Cookie: NEXT_LOCALE=...` on initial
Accept-Language detection is intentionally NOT modified here. The
existing `parseCookieLocaleFromHeader` already gives the cookie priority
over Accept-Language, and changing where the cookie is written would
overlap with #1336 item 4 (default-locale prefix normalisation).

* fix(i18n): preserve back/forward navigation under new history-state shape

The previous commit attached `__N: true` to router-owned history entries,
but two regressions slipped in:

1. `saveScrollPosition` merges scroll fields onto the *current* entry
   before a push. When the current entry was the initial document load
   (`state === null`), the merge produced a state object without `__N`,
   which the popstate foreign-state filter then dropped — breaking
   browser back/forward.

   Fix: when the existing state is null, mint a router-shaped state
   (`{ __N: true, url, as, options, key }`) before merging the scroll
   fields, so every entry the router touches carries `__N`.

2. The Safari-replay filter compared `state.as` against `window.location`,
   but `window.location` has already changed by the time popstate fires
   on a real back/forward. That made every genuine back navigation look
   like a replay and get ignored.

   Fix: compare against `_lastPathnameAndSearch` (the URL the router
   last actively navigated to). After a real back, that tracker still
   points at the entry we were on, so a genuine navigation differs and
   passes the filter — matching Next.js's `state.as === this.asPath`
   semantics.

Verified locally:
- tests/pages-router-i18n-sticky-locale.test.ts: 7/7 pass
- tests/shims.test.ts: 944/944 pass
- tests/e2e/pages-router/{navigation,before-pop-state,hydration,router-events}.spec.ts:
  28/28 pass (these were the CI failures from the previous commit).

* fix(i18n): stamp initial history entry with active locale on install

Closes the remaining gap in the popstate locale-recovery path: the
initial document entry has `state: null` until the first push, so a
back-navigation to it would fall back to the live `window.__VINEXT_LOCALE__`
— which an intervening locale-changing push may have flipped. Stamping
router-shaped state at install captures the SSR-resolved locale into
`options.locale` and lets the popstate handler restore the correct locale.

Also:
- Strip the hash from `state.as` so the Safari-replay filter's
  `withBasePath(state.as) === _lastPathnameAndSearch` comparison works for
  entries created from a hash-bearing URL (the tracker never carries a hash).
- Drop the unnecessary `withBasePath as withBasePathPrefix` import alias.

Adds 3 tests: install-time stamping, no-clobber of pre-existing state, and
the back-to-initial-entry locale-recovery regression.

* fix(i18n): prefer active locale over default + scope Safari-replay filter

Three small follow-ups discovered by CI on top of the i18n-sticky-locale
work:

1. `getCurrentBrowserLocale` was falling back to `__VINEXT_DEFAULT_LOCALE__`
   before `__VINEXT_LOCALE__` when no path prefix was present, which threw
   away the active locale on default-locale paths served under a
   non-default locale. Flip the fallback order so the active locale wins —
   exactly what the new `pages-router-i18n-sticky-locale.test.ts` 'Router
   .push without an explicit locale' case asserts.

2. The Safari-replay filter on the first popstate event was running even
   after the user had already navigated. After a hash-only `Router.push`,
   the entry's `state.as` is recorded without a hash, which happens to
   match `_lastPathnameAndSearch` (also without a hash); the filter then
   incorrectly dropped a genuine `goBack()` popstate, freezing the E2E
   router-events suite. Gate the filter behind a new `_routerDidNavigate`
   flag so it only fires during the synthetic initial-load replay window.

3. Update the 6 `expect(pushState).toHaveBeenCalledWith({}, ...)`
   assertions in `tests/shims.test.ts` to use
   `expect.objectContaining({ __N: true })`, since pushState now carries a
   Next.js-compatible state object instead of `{}`.

* test(i18n): align unprefixed-path locale tests with sticky-locale behavior

The previous "treat unprefixed path as default locale" assertions in
tests/link.test.ts and tests/shims.test.ts contradicted the intentional
sticky-locale flip in ab662c07 (getCurrentBrowserLocale now prefers
__VINEXT_LOCALE__ over __VINEXT_DEFAULT_LOCALE__). Update both tests to
assert the new behavior: a default-locale path served under a non-default
locale keeps its active locale for client-side root navigations.
2026-06-03 23:12:43 +01:00
James Anderson f7ea402cd7 fix(link): forward href/onClick to child in legacyBehavior (#1623)
* fix(link): forward href/onClick to child in legacyBehavior (#1469)

When `<Link legacyBehavior>` is set, the shim previously rendered its own
wrapping `<a>` around the user's child anchor, producing nested anchors and
duplicated content (e.g. a hidden `<a id="custom-button"></a>` alongside the
user's anchor) and breaking onClick propagation.

Mirror Next.js: when `legacyBehavior` is true, forward `href`/`onClick`/
`onMouseEnter`/`onTouchStart` to the single child via `React.cloneElement`
instead of wrapping. `href` is only forwarded when `passHref` is set or the
child is an `<a>` without its own `href`. String/number children are wrapped
in a plain `<a>` first so they have an element to clone, matching Next.js.

Ported from .nextjs-ref/packages/next/src/client/link.tsx (legacyBehavior
branch) and covered by .nextjs-ref/test/e2e/legacy-link-behavior-pages.

Fixes #1469

* fix(link): address bigbonk legacyBehavior parity gaps (#1469)

- Skip <Link>'s own onClick prop when invoked via legacyBehavior. The
  child's onClick is the canonical handler; calling Link's onClick too
  caused a double-fire. Added a dev console.warn matching Next.js when
  onClick/onMouseEnter are passed to <Link legacyBehavior>.
- Use the child element's own ref in legacyBehavior (matches Next.js)
  instead of routing through Link's forwardedRef. Merged with the
  internal intersection-observer ref so prefetching still works.
- Use `'href' in child.props` (matches Next.js) instead of `!== undefined`
  so an explicit `href={undefined}` on the child <a> is treated as the
  child owning its href and is not overwritten.
- Clone the child in the dangerous-scheme branch when legacyBehavior is
  set, so blocked javascript:/data:/vbscript: URLs do not produce nested
  anchors. The dangerous href is intentionally not forwarded to the child.

Added 3 unit tests covering the new behaviors.
2026-05-28 13:56:10 +01:00
Nathan Nguyen 4b4dc8d2ef fix: handle Pages Router middleware redirects (#1439) 2026-05-26 11:35:58 +01:00
James Anderson 485046ed09 fix(link): warn on repeated forward slashes in href (#1563)
* fix(link): warn on repeated forward slashes in href

Closes #1554

When a `<Link href>` contains repeated forward-slashes (e.g. `/foo//bar`)
or a backslash, Next.js's `resolveHref` emits a `console.error` so the
developer notices the invalid URL during dev/CI. vinext's Link shim did
not surface that warning, which caused the Next.js Deploy Suite test
`test/e2e/repeated-forward-slashes-error` to fail waiting for the
message in the CLI output.

Add a small `warnOnRepeatedSlashesInHref` helper that mirrors the regex
and message used by Next.js, and call it from the Link body after the
href is resolved. The href continues to render and navigate unchanged —
Next.js does not block, it only warns.

Ported from Next.js: packages/next/src/client/resolve-href.ts

* fix(link): normalize repeated slashes and drop dedup for Next.js parity

Address review feedback on #1563:

- Remove the module-level `warnedRepeatedSlashHrefs` Set. Next.js fires
  this warning on every `resolveHref` call, so dedup was an unintended
  divergence and a latent test-ordering hazard.
- Add `normalizeRepeatedSlashes` (port of
  packages/next/src/shared/lib/utils/normalize-repeated-slashes.ts) and
  apply it after warning, so `<Link href="/a//b">` renders `href="/a/b"`
  exactly as Next.js does.
- Preserve protocol-relative URLs (`//example.com/...`) unchanged —
  vinext treats those as external via `isAbsoluteOrProtocolRelativeUrl`,
  so we skip both the warning and the normalization for them to avoid
  regressing the existing "locale does not mangle protocol-relative
  URLs" test.
- Document the `window.location.pathname` fallback (Next.js uses
  `router.pathname`; the difference is cosmetic and not asserted by the
  compat test).
- Add tests covering no-dedup behaviour, href normalization (including
  backslashes, query-string preservation, and protocol-prefixed URLs).
2026-05-26 10:10:08 +01:00
Nathan Nguyen b6fc03cfc3 fix(router): preserve default locale for unprefixed root links (#1442)
* fix(router): preserve default locale for unprefixed root links

Unprefixed Pages Router URLs represent the default locale, even when the browser's preferred locale has previously been detected as a non-default locale. Treating the ambient runtime locale as authoritative makes a Link from /new to / request /id and returns the wrong page locale.

Derive implicit client navigation locale from the current URL first, then domain/default locale state, and render a locale-qualified default root fallback href so non-intercepted root links do not re-enter preferred-locale detection. The regression tests port the relevant Next.js i18n preferred-locale cases at the Link and router boundaries.

* refactor(router): share client locale inference

The review called out that Link and Router had identical browser locale fallback logic. Keeping that chain duplicated makes future i18n changes easy to apply in one place but miss in the other.

Move the URL-first browser locale lookup into a shared shim helper and add SSR coverage for the intentional default-locale root fallback href.
2026-05-22 13:58:42 +01:00
Nathan Nguyen 82b6388ece fix(app-router): render optimistic loading shells for dynamic navigation (#1421)
* fix(app-router): render optimistic loading shells for dynamic navigation

Dynamic App Router links currently skip automatic prefetch for route patterns with params, so a navigation to a sibling dynamic URL waits for fresh RSC data before committing anything. That diverges from Next.js, where automatic prefetch learns the loading shell for dynamic routes and the router can commit the shell immediately while the full payload is fetched.

The client prefetch path now requests a loading-shell render for dynamic app routes, keeps that entry out of the navigation cache, learns reusable optimistic route templates from settled shell prefetches, and commits a detached optimistic payload for matching dynamic navigations. The server emits shell-only RSC payloads under a separate cache variant so normal navigation data is not polluted.

Tests cover matching semantics, shell-only page wiring, cache key isolation, prefetch cache consumption, Link prefetch behavior, and the upstream Next.js optimistic-routing deploy-suite case.

* chore(app-router): address optimistic routing review
2026-05-22 10:07:26 +01:00
James Anderson ddfdd18a1a fix(pages-router): honour shallow prop on next/link (#1413)
Closes the last sub-problem of #1332.

The Pages Router `Router.push` / `Router.replace` already short-circuit the
`_next/data` fetch when `options.shallow === true`, but `<Link shallow>` never
made it to the router because `shallow` was missing from `LinkProps`. The prop
silently fell into `...rest`, was spread onto the underlying `<a>`, and the
click handler called `Router.push(href, undefined, { scroll, locale })` with
no shallow flag — so every `<Link shallow>` did a full server roundtrip and
GSSP re-ran.

This matches Next.js's deploy-suite fixture
`test/e2e/middleware-trailing-slash/app/pages/shallow.js`, which clicks a
`<Link href="/sha?hello=world" shallow>` and asserts the GSSP-generated
random message stays unchanged afterwards.

The fix is mechanical: add `shallow?: boolean` to `LinkProps`, destructure
it in the Link component, forward it through `navigatePagesRouterLink` into
the router transition options. `shallow` is only included in the router
options when explicitly set so existing callers that omit it keep the same
options shape.
2026-05-21 22:13:21 +01:00
Nathan Nguyen fa1392616d fix(pages-router): preserve i18n client root navigation (#1323)
* fix(pages-router): preserve i18n client root navigation

Pages Router client navigation fetched the unprefixed root URL even when the transition resolved to the default locale. That let root Accept-Language detection redirect a link click from /new to the preferred locale path, diverging from Next.js.

Use the active locale for implicit Link and Router transitions, fetch default-locale root HTML through a locale-qualified internal URL while preserving the visible unprefixed URL, and parse i18n __NEXT_DATA__ scripts that append locale globals.

* fix(pages-router): preserve i18n client root navigation

Pages Router client navigation fetched the unprefixed root URL even when the transition resolved to the default locale. That let root Accept-Language detection redirect a link click from /new to the preferred locale path, diverging from Next.js.

Use the active locale for implicit Link and Router transitions, fetch default-locale root HTML through a locale-qualified internal URL while preserving the visible unprefixed URL, and parse i18n __NEXT_DATA__ scripts that append locale globals.

* fix(pages-router): validate next data parsing

* fix(pages-router): preserve link locale transitions

* fix(pages-router): preserve popstate locale fetch

* chore(pages-router): address locale review nits
2026-05-20 10:40:58 +01:00
Nathan Nguyen ff46514563 fix(link): preserve native URI scheme navigation (#1268)
* fix(link): preserve native URI scheme navigation

Link treated only http, https, and protocol-relative hrefs as browser-owned. Native schemes such as mailto:, tel:, and sms: were therefore locale-prefixed, eligible for prefetch normalization, and intercepted by client navigation.

The URL boundary now mirrors Next.js absolute URL scheme classification while preserving vinext's protocol-relative handling. Link rendering, click interception, relative resolution, and prefetch normalization all share that classification so non-local schemes stay with the browser while same-origin absolute URLs remain routable.

Adds focused regression coverage for native scheme rendering, click handling, and prefetch decisions.

* refactor(router): reuse shared URL scheme detection

Router and navigation shims already had local external URL predicates with the same browser-owned URL semantics now used by Link.

Reuse the shared url-utils predicate for Pages Router locale handling, Pages Router external detection, App Router external detection, and App Router prefetch normalization. Add focused shim coverage for native URI schemes through the Pages Router helpers.

* test(link): add hasAttribute stub to native URI scheme click event

The merge of main introduced a download-attribute check in handleClick that
runs before the native-URI early return, so test click events must provide
currentTarget.hasAttribute.

---------

Co-authored-by: James <james@eli.cx>
2026-05-16 19:14:26 +00:00
James Anderson da9998a00f fix(link): normalise href for trailingSlash config (#1282)
* fix(link): normalise href for trailingSlash config

Port `normalizePathTrailingSlash` from Next.js (client/normalize-trailing-slash.ts)
into a small util under `shims/url-utils.ts`, wire the resolved `trailingSlash`
config through a new `process.env.__VINEXT_TRAILING_SLASH` build-time define,
and apply it inside `<Link>` so the rendered `href` matches the canonical URL
form instead of relying on a 308 redirect bounce.

This unblocks the upstream `e2e/trailing-slashes/*` suite, which asserts on
rendered `<Link>` hrefs (not just redirect Location headers).

* fix(link): normalise href again after applying basePath

Matches Next.js's `addBasePath`, which runs `normalizePathTrailingSlash`
after prefixing. Without this second pass, joining a non-empty basePath
with the bare root (`/`) under `trailingSlash: false` would render
`/foo/` instead of the canonical `/foo`.

* Update packages/vinext/src/shims/url-utils.ts

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

---------

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
2026-05-16 19:05:38 +00:00
Nathan Nguyen 6cf4148ffd fix(link): keep auto prefetch from fetching dynamic app routes (#1177)
* fix(link): keep auto prefetch from fetching dynamic app routes

App Router Link treated null, undefined, and auto prefetch as the same full RSC prefetch used by prefetch={true}. That violates Next.js's public prefetch contract and over-fetches dynamic route payloads as soon as links enter the viewport.

The broken assumption was that prefetch only needed a boolean enabled state. Split Link prefetch into disabled, auto, and full modes, emit a small browser route manifest, and allow automatic full RSC prefetch only for matched static app routes while preserving explicit full prefetch.

Add focused tests for the mode mapping and generated route manifest.

* test(link): opt interception prefetch fixture into full mode

The interception-context E2E asserts that full RSC prefetch cache keys stay separate by source context. After auto prefetch stops full-fetching dynamic routes, that fixture needs to request the full prefetch mode explicitly.
2026-05-14 09:25:53 +01:00
James Anderson 1ff166eb92 chore: turn on more lint rules (#714)
* chore: turn on more lint rules

* fmt

* add todos
2026-03-29 20:40:57 +01:00
Stephen Zhou c17d6941be chore: migrate to vite plus (#535)
* chore: migrate to vite plus

* Disable typeAware and typeCheck

* Update CI

* Fix CI

* Fix test

* Clean

* Run test with vp

* Try revert

* react: false In test

* Fix test

* Revert "Try revert"

This reverts commit 009da10473.

* Update

* Update

* Try revert ci changes

* revert

* Run vp migrate

* Disable typeAware and typeCheck for now

* Better resolve for test

* Use vp dev instead of vite

* Update expect

* Fix NormalizeManifestModuleId

* Try increase timeout

* Update to use vp

* Try new check

* Bring back npx vp

* Migrate CI

* Make next-intl resolvable

* Update

* Update

* Update
2026-03-15 10:50:13 +00:00
Nathan Nguyen ce8629be96 fix: use AsyncLocalStorage for per-request i18n locale state (#501)
The i18n locale context (locale, defaultLocale, domainLocales,
hostname) was stored on bare globalThis properties, which are
shared across concurrent requests. Under concurrent traffic
(Cloudflare Workers or Node.js production server), request A's
locale could be overwritten by request B before A finishes
rendering, causing Link components to produce incorrect
locale-prefixed URLs.

Introduce ALS-backed i18n state following the existing pattern
used by router-state.ts and head-state.ts:

- i18n-context.ts: bridge module with fallback globalThis
  accessors and a registration API for ALS-backed overrides
- i18n-state.ts: server-only module that registers ALS-backed
  accessors on import, providing per-request isolation

link.tsx now reads locale context via getI18nContext() instead
of bare globalThis, dev-server.ts sets context via ssrLoadModule
for the SSR environment, and the production entry wraps
rendering in runWithI18nState().
2026-03-12 17:36:17 +00:00
Nathan Nguyen 38dc409a43 feat: Add Pages Router i18n domain routing (#471)
* Add Pages Router domain locale routing

* Add coverage for Pages Router domain locale behavior

* Format Pages Router i18n domain routing changes

* Handle basePath in Pages Router i18n domains

* Refactor Pages Router i18n domain fixtures

* Update Pages Router entry snapshot after rebase

* Align domain locale redirects with Next.js

* Clarify Pages Router domain locale helpers

* Fix case-insensitive locale prefixes

* Simplify same-domain locale URL handling

* Cover same-domain locale alias redirects

* Document Pages Router i18n invariants

* Guard preferred locale in domain redirects
2026-03-12 13:49:59 +00:00
Jared Stowell cb2d1c9ed1 fix: Link onNavigate URL resolution for relative hrefs (#483)
* Fix onNavigate relative href report

* Verify onNavigate relative href

* Fix onNavigate URL mismatch

* Review url-utils changes
2026-03-12 08:17:43 +00:00
Jared Stowell f035e1a05e fix: object-form query array serialization in next/link and next/router (#457)
* Fix query param handling

* Fix scalar query serialization
2026-03-11 10:46:57 +00:00
Nathan Nguyen 15cd5d2881 fix: skip locale prefix for absolute and protocol-relative Link hrefs (#455)
* fix: skip locale prefix for absolute and protocol-relative Link hrefs

applyLocaleToHref() blindly prepended /${locale} to any href string,
producing malformed URLs like /fr/https://example.com/about or
/fr///example.com/about for absolute and protocol-relative hrefs.

Guard against http://, https://, and // URLs before applying the
locale prefix — these are external or same-origin absolute URLs that
should not be rewritten.

* fix: skip locale prefix for absolute and protocol-relative URLs in applyNavigationLocale

applyNavigationLocale() in router.ts (used by Router.push() / Router.replace())
had the same bug as applyLocaleToHref() in link.tsx — it would prepend /{locale}
to absolute and protocol-relative URLs, producing malformed hrefs like
/fr/https://example.com/about or /fr///cdn.example.com/img.png.

Add the same guard that was applied to applyLocaleToHref(), and add
corresponding tests covering https://, http://, and // URL patterns.

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 08:38:48 +00:00
Jared Stowell c4064ebb23 fix: next/form submitter overrides and query-string GET URLs (#426)
* Add submitter handling to form shim

* Update form shim URL validation

* Fix form submit attribute casing
2026-03-11 00:31:52 +00:00
James Anderson 764a496ce7 add oxfmt formatter (#380)
* add oxfmt formatter: config, scripts, CI, editor setup, docs

* rebuild lockfile

* fix: add Format to required checks list, remove dead ignore pattern

* run fmt

* add format to agents.md again
2026-03-09 14:56:14 +00:00
Divanshu Chauhan (divkix) 6762472e70 fix: normalize same-origin absolute URLs for client-side navigation (#336)
* fix: normalize same-origin absolute URLs for client-side navigation

When <Link href="https://example.com/path"> is used and example.com
matches the current origin, vinext now does SPA navigation instead
of a full page reload. This matches Next.js behavior.

Adds toSameOriginPath() utility that extracts the local path from a
same-origin absolute URL. Applied consistently across:
- link.tsx handleClick() and prefetch paths
- navigation.ts navigateImpl() (App Router)
- router.ts push()/replace() (Pages Router hook + singleton)

Closes #335

* fix: handle protocol-relative URLs in toSameOriginPath()

Use window.location.origin as base for `//` URLs so they resolve
correctly for same-origin detection, while keeping `new URL(url)`
(no base) for http/https URLs to preserve existing behavior.

Addresses Copilot review comment on PR #336.

* Update tests/link.test.ts

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

---------

Co-authored-by: James Anderson <james@eli.cx>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
2026-03-08 08:55:07 +00:00
Steve Faulkner 12fea722b6 Initial public release of vinext 2026-02-24 09:29:39 -06:00