Skip to content

Commit f9a2da1

Browse files
authored
Merge branch 'danielroe:main' into main
2 parents dc2823c + 0eca591 commit f9a2da1

18 files changed

Lines changed: 170 additions & 49 deletions

.oxlintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"rules": {
1010
"no-console": "warn",
1111
"no-await-in-loop": "off",
12-
"unicorn/no-array-sort": "off"
12+
"unicorn/no-array-sort": "off",
13+
"no-restricted-globals": "error"
1314
},
1415
"ignorePatterns": [
1516
".output/**",

app/components/PackageCard.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function formatDate(dateStr: string): string {
2323
<template>
2424
<article class="group card-interactive">
2525
<NuxtLink
26-
:to="`/package/${result.package.name}`"
26+
:to="{ name: 'package', params: { package: result.package.name.split('/') } }"
2727
:prefetch-on="prefetch ? 'visibility' : 'interaction'"
2828
class="block focus:outline-none decoration-none"
2929
>
@@ -78,7 +78,7 @@ function formatDate(dateStr: string): string {
7878
>
7979
<li v-for="keyword in result.package.keywords.slice(0, 5)" :key="keyword">
8080
<NuxtLink
81-
:to="`/search?q=keywords:${encodeURIComponent(keyword)}`"
81+
:to="{ name: 'search', query: { q: `keywords:${keyword}` } }"
8282
class="tag decoration-none"
8383
>
8484
{{ keyword }}

app/components/PackageDependencies.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function truncateVersion(version: string, maxLength = 20): string {
7171
class="flex items-center justify-between py-1 text-sm gap-2"
7272
>
7373
<NuxtLink
74-
:to="`/package/${dep}`"
74+
:to="{ name: 'package', params: { package: dep.split('/') } }"
7575
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate min-w-0"
7676
>
7777
{{ dep }}
@@ -111,7 +111,7 @@ function truncateVersion(version: string, maxLength = 20): string {
111111
>
112112
<div class="flex items-center gap-2 min-w-0">
113113
<NuxtLink
114-
:to="`/package/${peer.name}`"
114+
:to="{ name: 'package', params: { package: peer.name.split('/') } }"
115115
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate"
116116
>
117117
{{ peer.name }}

app/components/PackageDownloadStats.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const config = computed(() => ({
2424
theme: 'dark', // enforced dark mode for now
2525
style: {
2626
backgroundColor: 'transparent',
27+
animation: {
28+
show: false,
29+
},
2730
area: {
2831
color: '#6A6A6A',
2932
useGradient: false,

app/components/PackageVersions.vue

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { PackumentVersion, PackageVersionInfo } from '#shared/types'
3+
import type { RouteLocationRaw } from 'vue-router'
34
45
const props = defineProps<{
56
packageName: string
@@ -56,6 +57,14 @@ function compareVersions(a: string, b: string): number {
5657
return 0
5758
}
5859
60+
// Build route object for package version link
61+
function versionRoute(version: string): RouteLocationRaw {
62+
return {
63+
name: 'package',
64+
params: { package: [...props.packageName.split('/'), 'v', version] },
65+
}
66+
}
67+
5968
// Get prerelease channel or empty string for stable
6069
function getPrereleaseChannel(version: string): string {
6170
const parsed = parseVersion(version)
@@ -333,7 +342,7 @@ function formatDate(dateStr: string): string {
333342
<div class="flex-1 flex items-center justify-between py-1.5 text-sm gap-2 min-w-0">
334343
<div class="flex items-center gap-2 min-w-0">
335344
<NuxtLink
336-
:to="`/package/${packageName}/v/${row.primaryVersion.version}`"
345+
:to="versionRoute(row.primaryVersion.version)"
337346
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate"
338347
>
339348
{{ row.primaryVersion.version }}
@@ -374,7 +383,7 @@ function formatDate(dateStr: string): string {
374383
>
375384
<div class="flex items-center gap-2 min-w-0">
376385
<NuxtLink
377-
:to="`/package/${packageName}/v/${v.version}`"
386+
:to="versionRoute(v.version)"
378387
class="font-mono text-xs text-fg-subtle hover:text-fg-muted transition-colors duration-200 truncate"
379388
>
380389
{{ v.version }}
@@ -452,10 +461,11 @@ function formatDate(dateStr: string): string {
452461
<div v-else class="flex items-center gap-2 py-1">
453462
<span class="w-3" />
454463
<NuxtLink
455-
:to="`/package/${packageName}/v/${group.versions[0]?.version}`"
464+
v-if="group.versions[0]"
465+
:to="versionRoute(group.versions[0].version)"
456466
class="font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200"
457467
>
458-
{{ group.versions[0]?.version }}
468+
{{ group.versions[0].version }}
459469
</NuxtLink>
460470
<span
461471
v-if="group.versions[0]?.tag"
@@ -474,7 +484,7 @@ function formatDate(dateStr: string): string {
474484
>
475485
<div class="flex items-center gap-2 min-w-0">
476486
<NuxtLink
477-
:to="`/package/${packageName}/v/${v.version}`"
487+
:to="versionRoute(v.version)"
478488
class="font-mono text-xs text-fg-subtle hover:text-fg-muted transition-colors duration-200 truncate"
479489
>
480490
{{ v.version }}

app/composables/useNpmRegistry.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,14 @@ export function usePackageWeeklyDownloadEvolution(
171171
async () => {
172172
const packageName = toValue(name)
173173
const { weeks = 12, endDate } = toValue(options) ?? {}
174-
const end = endDate ? new Date(`${endDate}T00:00:00.000Z`) : new Date()
174+
175+
const today = new Date()
176+
const yesterday = new Date(
177+
Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() - 1),
178+
)
179+
180+
const end = endDate ? new Date(`${endDate}T00:00:00.000Z`) : yesterday
181+
175182
const start = addDays(end, -(weeks * 7) + 1)
176183
const startIso = toIsoDateString(start)
177184
const endIso = toIsoDateString(end)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Redirect legacy URLs to canonical paths (client-side only)
3+
*
4+
* - /package/* → /*
5+
* - /package/code/* → /code/*
6+
* - /org/* → /@*
7+
*/
8+
export default defineNuxtRouteMiddleware(to => {
9+
// Only redirect on client-side to avoid breaking crawlers mid-transition
10+
if (import.meta.server) return
11+
12+
const path = to.path
13+
14+
// /package/code/* → /code/*
15+
if (path.startsWith('/package/code/')) {
16+
return navigateTo(path.replace('/package/code/', '/code/'), { replace: true })
17+
}
18+
19+
// /package/* → /*
20+
if (path.startsWith('/package/')) {
21+
return navigateTo(path.replace('/package/', '/'), { replace: true })
22+
}
23+
24+
// /org/* → /@*
25+
if (path.startsWith('/org/')) {
26+
return navigateTo(path.replace('/org/', '/@'), { replace: true })
27+
}
28+
})
Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
<script setup lang="ts">
22
import { formatNumber } from '#imports'
33
4-
const route = useRoute('org-name')
4+
definePageMeta({
5+
name: 'org',
6+
alias: ['/org/:org()'],
7+
})
8+
9+
const route = useRoute('org')
510
6-
const orgName = computed(() => route.params.name)
11+
const orgName = computed(() => route.params.org)
712
813
const { isConnected } = useConnector()
914
@@ -26,6 +31,13 @@ const packageCount = computed(() => scopedPackages.value.length)
2631
2732
const activeTab = ref<'members' | 'teams'>('members')
2833
34+
// Canonical URL for this org page
35+
const canonicalUrl = computed(() => `https://npmx.dev/@${orgName.value}`)
36+
37+
useHead({
38+
link: [{ rel: 'canonical', href: canonicalUrl }],
39+
})
40+
2941
useSeoMeta({
3042
title: () => `@${orgName.value} - npmx`,
3143
description: () => `npm packages published by the ${orgName.value} organization`,
Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22
import { joinURL } from 'ufo'
33
import type { PackumentVersion, NpmVersionDist } from '#shared/types'
44
5-
const route = useRoute('package-name')
5+
definePageMeta({
6+
name: 'package',
7+
alias: ['/package/:package(.*)*'],
8+
})
9+
10+
const route = useRoute('package')
611
712
// Parse package name and optional version from URL
813
// Patterns:
9-
// /package/nuxt → packageName: "nuxt", requestedVersion: null
10-
// /package/nuxt/v/4.2.0 → packageName: "nuxt", requestedVersion: "4.2.0"
11-
// /package/@nuxt/kit → packageName: "@nuxt/kit", requestedVersion: null
12-
// /package/@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", requestedVersion: "1.0.0"
14+
// /nuxt → packageName: "nuxt", requestedVersion: null
15+
// /nuxt/v/4.2.0 → packageName: "nuxt", requestedVersion: "4.2.0"
16+
// /@nuxt/kit → packageName: "@nuxt/kit", requestedVersion: null
17+
// /@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", requestedVersion: "1.0.0"
1318
const parsedRoute = computed(() => {
14-
const segments = Array.isArray(route.params.name) ? route.params.name : [route.params.name ?? '']
19+
const segments = route.params.package || []
1520
1621
// Find the /v/ separator for version
1722
const vIndex = segments.indexOf('v')
@@ -220,6 +225,16 @@ onMounted(() => {
220225
nextTick(checkDescriptionOverflow)
221226
})
222227
228+
// Canonical URL for this package page
229+
const canonicalUrl = computed(() => {
230+
const base = `https://npmx.dev/${packageName.value}`
231+
return requestedVersion.value ? `${base}/v/${requestedVersion.value}` : base
232+
})
233+
234+
useHead({
235+
link: [{ rel: 'canonical', href: canonicalUrl }],
236+
})
237+
223238
useSeoMeta({
224239
title: () => (pkg.value?.name ? `${pkg.value.name} - npmx` : 'Package - npmx'),
225240
description: () => pkg.value?.description ?? '',
@@ -246,7 +261,7 @@ defineOgImageComponent('Package', {
246261
<h1 class="font-mono text-2xl sm:text-3xl font-medium">
247262
<NuxtLink
248263
v-if="orgName"
249-
:to="`/org/${orgName}`"
264+
:to="{ name: 'org', params: { org: orgName } }"
250265
class="text-fg-muted hover:text-fg transition-colors duration-200"
251266
>@{{ orgName }}</NuxtLink
252267
><span v-if="orgName">/</span
@@ -432,7 +447,10 @@ defineOgImageComponent('Package', {
432447
</li>
433448
<li v-if="displayVersion">
434449
<NuxtLink
435-
:to="`/package/code/${pkg.name}/v/${displayVersion.version}`"
450+
:to="{
451+
name: 'code',
452+
params: { path: [...pkg.name.split('/'), 'v', displayVersion.version] },
453+
}"
436454
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
437455
>
438456
<span class="i-carbon-code w-4 h-4" />
@@ -560,7 +578,7 @@ defineOgImageComponent('Package', {
560578
</h2>
561579
<ul class="flex flex-wrap gap-1.5 list-none m-0 p-0">
562580
<li v-for="keyword in displayVersion.keywords.slice(0, 15)" :key="keyword">
563-
<NuxtLink :to="`/search?q=keywords:${encodeURIComponent(keyword)}`" class="tag">
581+
<NuxtLink :to="{ name: 'search', query: { q: `keywords:${keyword}` } }" class="tag">
564582
{{ keyword }}
565583
</NuxtLink>
566584
</li>

0 commit comments

Comments
 (0)