Skip to content

Comments

fix: refactor routing to fix errors#1575

Open
alexdln wants to merge 11 commits intonpmx-dev:mainfrom
alexdln:chore/test-route-overriting
Open

fix: refactor routing to fix errors#1575
alexdln wants to merge 11 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

🤞🤞🤞

@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 22, 2026 11:23pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 22, 2026 11:23pm
npmx-lunaria Ignored Ignored Feb 22, 2026 11:23pm

Request Review

@codecov
Copy link

codecov bot commented Feb 22, 2026

Codecov Report

❌ Patch coverage is 21.05263% with 15 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 50.00% 1 Missing and 2 partials ⚠️
app/pages/package/[[org]]/[name].vue 0.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@alexdln alexdln changed the title chore: check routing configuration fix: refactor routing to fix errors Feb 22, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

This PR implements consistent trailing slash handling across the application. It adds a new trailingSlash prop to the LinkBase component supporting 'append' or 'remove' values (defaulting to 'append'), updates the global trailing-slash middleware to append slashes on the server-side, adds the trailing-slash attribute to numerous NuxtLink usages throughout the codebase, enables trailingSlash: true in the Nuxt configuration, and removes conflicting trailing slash configuration from Vercel config. Additionally, it refactors version resolution to use useAsyncData instead of useFetch and simplifies error handling in the package page.

Possibly related PRs

Suggested labels

front

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description directly addresses the changeset, explaining routing issues, version loading logic refactoring, trailing slash handling via middleware, and payload path configuration changes.
Linked Issues check ✅ Passed The PR comprehensively addresses the linked issue #1558 (hydration mismatch) through refactored version loading logic, enabled trailing slash handling via middleware, and removal of payload caching mismatches.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing routing and hydration issues: version loading refactoring, trailing slash configuration across components, middleware updates, and test adjustments all align with stated objectives.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 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(/\/?$/, '/'))
 }

trailingSlash?: 'append' | 'remove' | undefined
}>(),
{ variant: 'link', size: 'medium' },
{ variant: 'link', size: 'medium', trailingSlash: 'append' },
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Default 'append' modifies external link href attributes.

External links in NuxtLink now correctly use the effectiveTrailingSlash value when computing the href value. This means trailing-slash="append" is not limited to internal routes — it is also applied to absolute URLs. With the default set to 'append', every external link rendered through LinkBase (e.g. https://github.com/user/repo) will silently have a trailing slash appended to its href (https://github.com/user/repo/). For most HTML page links this is benign, but it can break direct links to specific resources (raw file downloads, badge endpoints, etc.).

Conditional application based on isLinkExternal prevents external URLs from being mutated:

🐛 Proposed fix
-    :trailing-slash="trailingSlash"
+    :trailing-slash="isLinkExternal ? undefined : trailingSlash"

Also applies to: 79-79

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