Skip to content

Comments

fix: refactor routing to fix errors#1575

Closed
alexdln wants to merge 22 commits intonpmx-dev:mainfrom
alexdln:chore/test-route-overriting
Closed

fix: refactor routing to fix errors#1575
alexdln wants to merge 22 commits intonpmx-dev:mainfrom
alexdln:chore/test-route-overriting

Conversation

@alexdln
Copy link
Member

@alexdln alexdln commented Feb 22, 2026

🔗 Linked issue

Resolves #1558 #1530 #1534 #1267 #1266 #1194

🧭 Context

  • Version pages almost always displayed a 500 - server error as the title;
  • Version pages often failed to load, with a redirect to segmentPath in half the cases;
  • In many cases, the readme didn't appear, or the content disappeared after hydration.

From the initial research:

  • Server always sees the wrong path for version pages. But sometimes it returns a blank page, and once on the client, it manages to work correctly and displays the page as expected
  • Sometimes it redirects immediately, apparently on the client. That is, the request path is correct, but the response immediately shows that the path is segmented, not injected parameters
  • Definitely server routing issue. But when there's a trailingSlash at the end of the path, the problem doesn't reproduce (https://npmxdev-git-fork-alexdln-chore-test-route-overriting-npmx.vercel.app/package/obsidium/v/3.1.0/)
  • But if we enable trailingSlash in the vercel settings, then for versions it will always redirect to a page without it (?!)
  • It seems to redirect when we request for the version failed for some reason (f.e. rate limit). That is, if our request failed, we get a server error and a redirect. And if it somehow went through, we get a blank page from server, since the [name] package doesn't exist and then we trying to load it client-side

📚 Description

  • Rewrote the version loading logic, as the previous variant often lost data from server and failed to load on the client;
  • Enabled trailingSlash - without it, navigation errors in Nitro often occur
    • Enabled it via the internal middleware, as the Vercel setting often didn't work as expected. And the nuxt.config setting didn't work at all
  • Removed certain settings in nuxt.config for payload paths. They aren't rendered independently of the HTML, but they could be cached. This caused mismatches, and when the initial payload was returned, an additional hydration error occurred

Some prefetch requests now return an 404 error - this isn't a degradation; it's just that previously, in the event of a failure, a fallback was returned with an empty object that could be cached and processed. Now the behavior is more correct.

🤞🤞🤞

@vercel
Copy link

vercel bot commented Feb 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 23, 2026 2:32pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 23, 2026 2:32pm
npmx-lunaria Ignored Ignored Feb 23, 2026 2:32pm

Request Review

@codecov
Copy link

codecov bot commented Feb 22, 2026

Codecov Report

❌ Patch coverage is 33.33333% with 18 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/composables/npm/useResolvedVersion.ts 0.00% 8 Missing and 2 partials ⚠️
app/middleware/trailing-slash.global.ts 53.84% 4 Missing and 2 partials ⚠️
app/pages/package/[[org]]/[name].vue 0.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

Introduces explicit trailing-slash handling site-wide: a new trailingSlash prop on Link Base is added and forwarded to NuxtLink; many NuxtLink/LinkBase usages were updated with trailing-slash="append" or "remove". nuxt.config.ts now sets site.trailingSlash: true and vercel.json trailingSlash removed. The global trailing-slash middleware was moved to server-side and changed to append slashes. Tests and pages were updated to expect trailing-slash URLs. Separately, useResolvedVersion was refactored to use useAsyncData and package page error handling was simplified.

Possibly related PRs

Suggested labels

front

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement the core objectives from #1558 and related issues: rewriting version loading logic [app/pages/package/[[org]]/[name].vue, app/composables/npm/useResolvedVersion.ts], enabling trailing-slash handling via middleware [app/middleware/trailing-slash.global.ts], and removing conflicting nuxt.config payload settings [nuxt.config.ts] to fix hydration mismatches and routing errors.
Out of Scope Changes check ✅ Passed All changes are within scope: trailing-slash prop additions across components align with the middleware implementation; LinkBase.vue prop addition [app/components/Link/Base.vue] supports trailing-slash propagation; test updates reflect the new trailing-slash URL format expectations.
Description check ✅ Passed The pull request description is directly related to the changeset, detailing the rationale for version loading refactoring, trailing-slash enablement, and payload configuration removal.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
app/components/Package/DeprecatedTree.vue (1)

86-93: ⚠️ Potential issue | 🔴 Critical

Remove trailing-slash="append" — feature not available in Nuxt 4.3.1

The trailingSlash per-link prop on <NuxtLink> was added in Nuxt v3.17.0 (released April 27, 2025, via PR #31820). However, it is not available in Nuxt 4.3.1. The v4.3.1 release notes do not include this feature, and trailing-slash behaviour in Nuxt 4 is controlled only via global configuration, not per-link attributes. Remove the trailing-slash="append" attribute to prevent the prop from being silently ignored or causing unexpected behaviour.

test/nuxt/components/VersionSelector.spec.ts (1)

306-326: ⚠️ Potential issue | 🟠 Major

Trailing slash on file-path links breaks the code-page file viewer.

The updated expectation confirms that VersionSelector now generates a trailing slash for links whose URL pattern contains a file path (e.g. src/index.ts/). This conflicts with the currentNode logic in app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue, which the author explicitly documented as treating /src/index.ts/ as an "incorrect file path":

// - /src/index.ts/ - incorrect file path (but formally can exist as a directory)

When a user selects a version from VersionSelector while viewing a file, the navigation resolves to e.g. /package-code/pkg/v/2.0.0/src/index.ts/. In currentNode:

  • parts = ['src', 'index.ts', ''] (3 segments)
  • At i=1: index.ts is a file but isLast=false, so it continues; current = found.childrenundefined
  • At i=2: part='', isLast=true, but lastFound.type === 'file' (not 'directory') → the guard fails → returns null

Result: isViewingFile = false, and the template falls through to show the directory listing instead of the file viewer.

The simplest fix in currentNode is to extend the guard to also return lastFound when it is a file:

🐛 Proposed fix in currentNode
-if (!part && isLast && lastFound?.type === 'directory') return lastFound
+if (!part && isLast && lastFound) return lastFound

Alternatively, apply trailing-slash="remove" (or omit the prop) on VersionSelector links that contain file-path segments.

app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue (1)

315-373: ⚠️ Potential issue | 🟠 Major

Direct NuxtLink changes are correct — but note the cross-file regression.

The three trailing-slash="append" additions here are all on directory-type or package-page routes:

  • Line 316: package route ✓
  • Line 354: tree root (no file path) ✓
  • Line 365: intermediate breadcrumb items (always directories, since the last crumb renders as <span>) ✓

However, the currentNode computation (line 77–102) explicitly documents that a file path with a trailing slash (e.g. src/index.ts/) is treated as invalid and returns null. Since versionUrlPattern (line 58–65) forwards the currently-viewed file path to VersionSelector, and VersionSelector now has trailing-slash="append" on its version links, switching versions while on a file view will navigate to a URL that currentNode cannot resolve — causing the file viewer to silently fall back to the directory listing. See the comment raised in VersionSelector.spec.ts for the full analysis and proposed fix.

app/pages/~[username]/orgs.vue (1)

84-84: ⚠️ Potential issue | 🟠 Major

Remove stray unconditional error.value assignment.

Line 84 sets error.value to an error string during every component setup run, before loadOrgs() has a chance to execute. loadOrgs only resets error when isOwnProfile is true; for non-connected or non-owner users it returns early without clearing this stale value. The v-else-if="!isOwnProfile" guard in the template currently hides the error state for those users, but this is an accidental safeguard — the error should simply not be set.

🐛 Proposed fix
-error.value = $t('header.orgs_dropdown.error')
-
 // Load on mount and when connection status changes
 watch(isOwnProfile, loadOrgs, { immediate: true })
app/middleware/trailing-slash.global.ts (1)

1-9: ⚠️ Potential issue | 🟡 Minor

JSDoc comment is the inverse of the actual behaviour and references removed configuration.

The comment states the middleware "removes trailing slashes" and shows examples of slash removal (/package/vue/ → /package/vue), but the code appends them (line 23: to.path + '/'). It also claims the middleware "only runs in development" and that "Vercel handles this redirect via vercel.json", but the import.meta.dev guard was removed and the Vercel trailingSlash config was also removed according to the PR.

Please update the comment to reflect the current behaviour.

📝 Suggested comment update
 /**
- * Removes trailing slashes from URLs.
+ * Appends trailing slashes to URLs for consistency with trailingSlash: true in nuxt.config.
  *
- * This middleware only runs in development to maintain consistent behavior.
- * In production, Vercel handles this redirect via vercel.json.
+ * Ensures all page routes (except /package-code/) have a trailing slash.
+ * Skips payload requests on the server to avoid interfering with Nuxt data fetching.
  *
- * - /package/vue/ → /package/vue
- * - /docs/getting-started/?query=value → /docs/getting-started?query=value
+ * - /package/vue → /package/vue/
+ * - /docs/getting-started?query=value → /docs/getting-started/?query=value
  */
🧹 Nitpick comments (3)
nuxt.config.ts (1)

356-374: getISRConfig's fallback branch is now dead code.

All four package-route call sites now use getISRConfig(60) without the options argument, so the fallback branch (lines 360–368) is unreachable. Consider removing the ISRConfigOptions interface and the fallback logic to keep the helper lean, unless there are plans to reuse it elsewhere.

♻️ Proposed simplification
-interface ISRConfigOptions {
-  fallback?: 'html' | 'json'
-}
-function getISRConfig(expirationSeconds: number, options: ISRConfigOptions = {}) {
-  if (options.fallback) {
-    return {
-      isr: {
-        expiration: expirationSeconds,
-        fallback:
-          options.fallback === 'html' ? 'spa.prerender-fallback.html' : 'payload-fallback.json',
-        initialHeaders: options.fallback === 'json' ? { 'content-type': 'application/json' } : {},
-      } as { expiration: number },
-    }
-  }
-  return {
-    isr: {
-      expiration: expirationSeconds,
-    },
-  }
+function getISRConfig(expirationSeconds: number) {
+  return {
+    isr: {
+      expiration: expirationSeconds,
+    },
+  }
}
app/components/Link/Base.vue (1)

77-79: :trailing-slash binding is redundant when v-bind="props" is already in use.

v-bind="props" spreads the full Vue props object onto NuxtLink, which already includes the trailingSlash prop. The explicit :trailing-slash="trailingSlash" binding on line 79 therefore sets the same prop twice to the same value. Vue 3 silently resolves this by letting the last binding win, but in development mode this can trigger a duplicate-prop warning.

If the intention is to make the binding site explicit (consistent with how :to and :target are also explicitly re-bound), consider adding a comment. Otherwise the extra binding can be removed.

app/components/VersionSelector.vue (1)

442-445: navigateToVersion bypasses the trailing-slash normalisation added to NuxtLink

Keyboard navigation (Enter/Space in handleListboxKeydown) calls navigateToVersion, which invokes navigateTo(getVersionUrl(version)) without a trailing slash. The middleware will redirect to the correct URL, but this introduces a superfluous client-side redirect for every keyboard-driven version switch.

Consider appending the slash directly in navigateToVersion so it is consistent with the link-click path:

♻️ Suggested fix
 function navigateToVersion(version: string) {
   isOpen.value = false
-  navigateTo(getVersionUrl(version))
+  navigateTo(getVersionUrl(version).replace(/\/?$/, '/'))
 }

Copy link
Contributor

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
app/middleware/trailing-slash.global.ts (1)

1-9: ⚠️ Potential issue | 🟡 Minor

Stale JSDoc: the comment describes the opposite of what the code now does.

The docstring says "Removes trailing slashes" and states that Vercel handles this in production, but the middleware now appends trailing slashes and the Vercel trailingSlash setting has been removed. The examples are also inverted (e.g. /package/vue//package/vue is the old behaviour).

📝 Suggested docstring update
-/**
- * Removes trailing slashes from URLs.
- *
- * This middleware only runs in development to maintain consistent behavior.
- * In production, Vercel handles this redirect via vercel.json.
- *
- * - /package/vue/ → /package/vue
- * - /docs/getting-started/?query=value → /docs/getting-started?query=value
- */
+/**
+ * Appends trailing slashes to URLs for consistent routing.
+ *
+ * - /package/vue → /package/vue/
+ * - /docs/getting-started?query=value → /docs/getting-started/?query=value
+ *
+ * Paths under /package-code/ and /api/ are excluded.
+ */
test/e2e/docs.spec.ts (1)

82-84: ⚠️ Potential issue | 🟠 Major

URL assertion will likely fail after the trailing-slash change.

Line 84 asserts toHaveURL(/\/package\/ufo$/), which requires the URL to end with ufo (no trailing slash). After clicking the package link, the trailing-slash middleware will redirect to /package/ufo/, causing this assertion to fail. The regex should account for the trailing slash.

🐛 Proposed fix
-    // Should navigate to package page (URL ends with /ufo)
-    await expect(page).toHaveURL(/\/package\/ufo$/)
+    // Should navigate to package page
+    await expect(page).toHaveURL(/\/package\/ufo\/?$/)
test/e2e/create-command.spec.ts (1)

14-15: ⚠️ Potential issue | 🔴 Critical

Update the href selectors to include trailing slashes that the router appends.

Lines 15 and 31 assert a[href="/package/create-vite"] and a[href="/package/create-next-app"] without trailing slashes. The NuxtLink component rendering these links (in Terminal/Install.vue) uses trailing-slash="append" explicitly, which means the generated href attributes will be /package/create-vite/ and /package/create-next-app/ with trailing slashes. The current selectors will silently fail to match, and toBeAttached() will not find the elements.

Change the selectors to:

  • a[href="/package/create-vite/"]
  • a[href="/package/create-next-app/"]
🧹 Nitpick comments (1)
test/e2e/interactions.spec.ts (1)

76-77: Inconsistent: goto on line 77 still uses /search?q=vue without a trailing slash.

Line 54 was updated to /search/?q=vue but line 77 was not. The middleware will handle the redirect transparently so this won't break, but it's inconsistent with the rest of the file.

📝 Suggested fix for consistency
-    await goto('/search?q=vue', { waitUntil: 'hydration' })
+    await goto('/search/?q=vue', { waitUntil: 'hydration' })
ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1210053 and 07af3d2.

📒 Files selected for processing (8)
  • app/middleware/trailing-slash.global.ts
  • test/e2e/connector.spec.ts
  • test/e2e/create-command.spec.ts
  • test/e2e/docs.spec.ts
  • test/e2e/hydration.spec.ts
  • test/e2e/interactions.spec.ts
  • test/e2e/package-manager-select.spec.ts
  • test/e2e/url-compatibility.spec.ts

Copy link
Contributor

@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.

🧹 Nitpick comments (1)
test/e2e/docs.spec.ts (1)

84-84: Optional: $ anchor may be fragile if query params are ever present.

/\/package\/ufo\/$/ correctly asserts a trailing slash, but $ anchors to the very end of the full URL string. If Nuxt ever appends a query parameter (e.g. ?tab=readme) during navigation, this assertion will produce a false failure.

♻️ More resilient alternative
-    await expect(page).toHaveURL(/\/package\/ufo\/$/)
+    await expect(page).toHaveURL(/\/package\/ufo\/($|\?)/)

This still asserts a trailing slash while tolerating any subsequent query string.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c2f241 and 8862b9a.

📒 Files selected for processing (2)
  • test/e2e/create-command.spec.ts
  • test/e2e/docs.spec.ts

@alexdln alexdln marked this pull request as draft February 23, 2026 11:29
@alexdln
Copy link
Member Author

alexdln commented Feb 23, 2026

I was so close to the cause, but I chose the wrong conclusion.

It turns out the trailing slash only worked because ISR is strict about it, so pages with slashes were essentially ignored by nitro. And as a result, I mostly solved the problems that had been added because of this

The problem is rather that our condition for isr was incomplete (again due to strictness)

reopened #1604

@alexdln alexdln closed this Feb 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hydration mismatch on package pages

1 participant