diff --git a/server/middleware/canonical-redirects.global.ts b/server/middleware/canonical-redirects.global.ts index e9139a8493..89f1d2a813 100644 --- a/server/middleware/canonical-redirects.global.ts +++ b/server/middleware/canonical-redirects.global.ts @@ -36,13 +36,37 @@ const pages = [ const cacheControl = 's-maxage=3600, stale-while-revalidate=36000' export default defineEventHandler(async event => { + const [path = '/', query] = event.path.split('?') + + if (query) { + const params = new URLSearchParams(query) + + switch (params.get('activeTab')) { + case 'versions': { + // /package/name?activeTab=versions → /package/name/versions + // /package/@scope/name?activeTab=versions → /package/@scope/name/versions + + const pkgPathMatch = path.match(/^\/package\/((?:@[^/]+\/)?[^/]+)$/) + if (pkgPathMatch) { + params.delete('activeTab') + const remaining = params.toString() + setHeader(event, 'cache-control', cacheControl) + return sendRedirect( + event, + `/package/${pkgPathMatch[1]}/versions` + (remaining ? '?' + remaining : ''), + 301, + ) + } + break + } + } + } + const routeRules = getRouteRules(event) if (Object.keys(routeRules).length > 1) { return } - const [path = '/', query] = event.path.split('?') - // username if (path.startsWith('/~') || path.startsWith('/_')) { return diff --git a/test/e2e/url-compatibility.spec.ts b/test/e2e/url-compatibility.spec.ts index ce5e403bcd..4c66f849dc 100644 --- a/test/e2e/url-compatibility.spec.ts +++ b/test/e2e/url-compatibility.spec.ts @@ -120,6 +120,25 @@ test.describe('npmjs.com URL Compatibility', () => { }) }) + test.describe('npmjs.com activeTab=versions Compatibility', () => { + test('/package/vue?activeTab=versions → /package/vue/versions', async ({ page, goto }) => { + await goto('/package/vue?activeTab=versions', { waitUntil: 'domcontentloaded' }) + + await expect(page).toHaveURL(/\/package\/vue\/versions$/) + await expect(page.locator('h1')).toContainText('Version History') + }) + + test('/package/@nuxt/kit?activeTab=versions → /package/@nuxt/kit/versions', async ({ + page, + goto, + }) => { + await goto('/package/@nuxt/kit?activeTab=versions', { waitUntil: 'domcontentloaded' }) + + await expect(page).toHaveURL(/\/package\/@nuxt\/kit\/versions$/) + await expect(page.locator('h1')).toContainText('Version History') + }) + }) + test.describe('Edge Cases', () => { test('package name with dots: /package/lodash.merge', async ({ page, goto }) => { await goto('/package/lodash.merge', { waitUntil: 'domcontentloaded' })