Skip to content

Conversation

@bandiasek
Copy link
Contributor

@bandiasek bandiasek commented Sep 29, 2025

🔗 Linked issue

resolves #33315

📚 Description

When users get to a page, default nuxt router behavior is to compare "from" & "to" path using === method. Base on that comparison it evaluates whether it should apply scroll-to-top effect or not.

However, this comparison is not suitable in case of URLs ending with trailing slash. When navigating from page ending with trailing slash to same page not containing trailing slash at the end, it evaluates the page as being different page and it applies scroll to top effect.

How to reproduce:

It happens even in nuxt website.

Fix Description

My fix is using "ufo" library with function withoutTrailingSlash which will make decision for scroll behavior by default trailing slash agnostic.

@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Sep 29, 2025

Open in StackBlitz

@nuxt/kit

npm i https://pkg.pr.new/@nuxt/kit@33358

nuxt

npm i https://pkg.pr.new/nuxt@33358

@nuxt/rspack-builder

npm i https://pkg.pr.new/@nuxt/rspack-builder@33358

@nuxt/schema

npm i https://pkg.pr.new/@nuxt/schema@33358

@nuxt/vite-builder

npm i https://pkg.pr.new/@nuxt/vite-builder@33358

@nuxt/webpack-builder

npm i https://pkg.pr.new/@nuxt/webpack-builder@33358

commit: b4ba67e

@codspeed-hq
Copy link

codspeed-hq bot commented Sep 29, 2025

CodSpeed Performance Report

Merging #33358 will not alter performance

Comparing bandiasek:fix-nuxt/nuxt-router-default-scroll-trailingslash-agnostic (b4ba67e) with main (9bebe27)

Summary

✅ 10 untouched

@bandiasek
Copy link
Contributor Author

Do I need to include some note in documentation that default behavior has changed ? Is it even mentioned somewhere in the documentation ?

@bandiasek bandiasek marked this pull request as ready for review October 1, 2025 15:56
@bandiasek bandiasek requested a review from danielroe as a code owner October 1, 2025 15:56
@coderabbitai
Copy link

coderabbitai bot commented Oct 1, 2025

Walkthrough

Normalises paths by removing trailing slashes before comparing from.path and to.path in router navigation, making path comparison trailing-slash agnostic while preserving hash-based in-page scroll behaviour. Replaces imports/usages of withoutTrailingSlash from ufo with an inline url.replace(/\/$/, '') in payload and renderer code. Adds a router test asserting scrollTo is not triggered when only a trailing slash is added/removed and updates a server bundle size snapshot from 289k to 290k.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The modifications in payload.ts and renderer.ts replace withoutTrailingSlash imports with regular expression replacements, which relate to payload routing and caching rather than scroll behaviour and fall outside the scope of issue #33315. Please extract the payload and renderer trailing slash changes into a separate pull request or provide justification for including these refactors alongside the scroll behaviour fix.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly describes the core change of preventing unintended scroll-to-top when toggling a trailing slash in Nuxt router options, is clear and follows conventional commit style.
Linked Issues Check ✅ Passed The pull request modifies router.options.ts to normalise paths by removing trailing slashes before comparing, adds tests to validate this behaviour, and updates the snapshot accordingly, thus fully addressing issue #33315.
Description Check ✅ Passed The description directly addresses the linked issue, outlines reproduction steps and the fix approach for trailing slash agnostic scroll behaviour, and is clearly related to the changeset.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 560e250 and cc73826.

📒 Files selected for processing (1)
  • test/nuxt/router.options.test.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/nuxt/router.options.test.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: test-fixtures (ubuntu-latest, built, rspack, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: release-pkg-pr-new
  • GitHub Check: test-size
  • GitHub Check: test-benchmark
  • GitHub Check: typecheck (ubuntu-latest, bundler)
  • GitHub Check: typecheck (windows-latest, bundler)
  • GitHub Check: code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Member

@danielroe danielroe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you! ❤️

@danielroe danielroe changed the title feat(nuxt): default router scroll behavior is trailing slash agnostic fix(nuxt): don't trigger scroll when changin trailing slash Oct 2, 2025
@danielroe danielroe changed the title fix(nuxt): don't trigger scroll when changin trailing slash fix(nuxt): don't trigger scroll when changing trailing slash Oct 2, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c15181 and 560e250.

📒 Files selected for processing (4)
  • packages/nuxt/src/app/composables/payload.ts (2 hunks)
  • packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts (2 hunks)
  • packages/nuxt/src/pages/runtime/router.options.ts (1 hunks)
  • test/nuxt/router.options.test.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/nuxt/src/pages/runtime/router.options.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts
  • test/nuxt/router.options.test.ts
  • packages/nuxt/src/app/composables/payload.ts
**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Write unit tests for core functionality using vitest

Files:

  • test/nuxt/router.options.test.ts
🧬 Code graph analysis (1)
packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts (2)
packages/nuxt/src/core/runtime/nitro/utils/cache.ts (1)
  • payloadCache (6-6)
packages/nuxt/src/core/runtime/nitro/utils/renderer/payload.ts (1)
  • renderPayloadResponse (12-24)
🪛 GitHub Actions: autofix.ci
test/nuxt/router.options.test.ts

[error] 35-35: AssertionError: expected "spy" to be called at least once. (nuxt-legacy router.options test: should call scrollTo after page transition is finished with async component)

🪛 GitHub Check: code
test/nuxt/router.options.test.ts

[failure] 35-35: test/nuxt/router.options.test.ts > scrollBehavior of router options with global transition > should call scrollTo after page transition is finished with async component
AssertionError: expected "spy" to be called at least once
❯ completeNavigation test/nuxt/router.options.test.ts:35:28
❯ test/nuxt/router.options.test.ts:84:5

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts (1)

10-10: LGTM! Simplified trailing slash removal.

The replacement of withoutTrailingSlash with a direct regex pattern .replace(/\/$/, '') is appropriate here. While the ufo helper is more comprehensive (handling edge cases like multiple trailing slashes), the simpler regex suffices for normalised internal URL handling and reduces the dependency footprint.

Also applies to: 182-182

packages/nuxt/src/app/composables/payload.ts (1)

1-1: LGTM! Consistent trailing slash normalisation.

The change from withoutTrailingSlash(url) to url.replace(/\/$/, '') maintains the same behaviour for normalised URLs. Since isPrerendered typically receives paths from useRoute().path (already normalised), the simpler regex pattern is sufficient and reduces dependencies.

Also applies to: 99-99

test/nuxt/router.options.test.ts (2)

69-80: LGTM! Test correctly validates trailing-slash-agnostic scroll behaviour.

The new test properly verifies that navigating between /about and /about/ (differing only by a trailing slash) does not trigger scrollTo on the second navigation. This correctly validates the PR's objective to make the default router scroll behaviour trailing-slash agnostic.


82-96: LGTM! Test refactoring improves maintainability.

Extracting the common navigation completion logic into the completeNavigation helper reduces duplication and makes the tests more readable. Once the race condition in the helper is resolved, these tests should function correctly.

Comment on lines +28 to +36
async function completeNavigation () {
await flushPromises()

// Ensure everything is settled
await expect.poll(() => pageTransitionFinish.mock.calls.length).toBeGreaterThan(0)

expect(pageTransitionFinish).toHaveBeenCalled()
expect(pageLoadingEnd).toHaveBeenCalled()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix race condition in test helper.

The completeNavigation helper polls for pageTransitionFinish (line 32) but then asserts that both pageTransitionFinish and pageLoadingEnd were called (lines 34-35). This creates a race condition where the assertion for pageLoadingEnd may fail if that hook hasn't been called yet when the poll completes.

Apply this diff to wait for both hooks:

 async function completeNavigation () {
   await flushPromises()

-  // Ensure everything is settled
-  await expect.poll(() => pageTransitionFinish.mock.calls.length).toBeGreaterThan(0)
+  // Ensure both transition hooks are called
+  await expect.poll(() => pageTransitionFinish.mock.calls.length).toBeGreaterThan(0)
+  await expect.poll(() => pageLoadingEnd.mock.calls.length).toBeGreaterThan(0)

   expect(pageTransitionFinish).toHaveBeenCalled()
   expect(pageLoadingEnd).toHaveBeenCalled()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function completeNavigation () {
await flushPromises()
// Ensure everything is settled
await expect.poll(() => pageTransitionFinish.mock.calls.length).toBeGreaterThan(0)
expect(pageTransitionFinish).toHaveBeenCalled()
expect(pageLoadingEnd).toHaveBeenCalled()
}
async function completeNavigation () {
await flushPromises()
// Ensure both transition hooks are called
await expect.poll(() => pageTransitionFinish.mock.calls.length).toBeGreaterThan(0)
await expect.poll(() => pageLoadingEnd.mock.calls.length).toBeGreaterThan(0)
expect(pageTransitionFinish).toHaveBeenCalled()
expect(pageLoadingEnd).toHaveBeenCalled()
}
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 35-35: AssertionError: expected "spy" to be called at least once. (nuxt-legacy router.options test: should call scrollTo after page transition is finished with async component)

🪛 GitHub Check: code

[failure] 35-35: test/nuxt/router.options.test.ts > scrollBehavior of router options with global transition > should call scrollTo after page transition is finished with async component
AssertionError: expected "spy" to be called at least once
❯ completeNavigation test/nuxt/router.options.test.ts:35:28
❯ test/nuxt/router.options.test.ts:84:5

🤖 Prompt for AI Agents
In test/nuxt/router.options.test.ts around lines 28 to 36, the
completeNavigation helper currently polls only for pageTransitionFinish before
asserting both hooks, causing a race where pageLoadingEnd may not have been
called yet; change the poll condition to wait until both
pageTransitionFinish.mock.calls.length and pageLoadingEnd.mock.calls.length are
greater than 0 (i.e., poll a function returning that both lengths > 0), keep the
existing flushPromises call before polling, then make the two expect(...)
assertions as-is after the poll completes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Page scrolls to top when removing trailing slash

2 participants