Skip to content

Commit b6b9d16

Browse files
authored
Merge branch 'danielroe:main' into main
2 parents 12a29e5 + 9cb5e99 commit b6b9d16

14 files changed

Lines changed: 196 additions & 48 deletions

app/components/ConnectorModal.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,17 @@ watch(open, isOpen => {
165165
{{ error }}
166166
</div>
167167

168+
<!-- Warning message -->
169+
<div
170+
role="alert"
171+
class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md"
172+
>
173+
<p class="font-mono text-sm text-fg font-bold">WARNING</p>
174+
<p class="text-sm text-fg-muted">
175+
This allows npmx to access your npm cli and any authenticated contexts.
176+
</p>
177+
</div>
178+
168179
<button
169180
type="submit"
170181
:disabled="!tokenInput.trim() || isConnecting"

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: 31 additions & 1 deletion
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,
@@ -67,7 +70,34 @@ const config = computed(() => ({
6770
</div>
6871
<div class="w-full">
6972
<ClientOnly>
70-
<VueUiSparkline :dataset :config />
73+
<div>
74+
<VueUiSparkline :dataset :config />
75+
</div>
76+
<template #fallback>
77+
<!-- Skeleton matching sparkline layout: title row + chart with data label -->
78+
<div class="min-h-[100px]">
79+
<!-- Title row: date range (24px height) -->
80+
<div class="h-6 flex items-center pl-3">
81+
<span class="skeleton h-3 w-36" />
82+
</div>
83+
<!-- Chart area: data label left, sparkline right -->
84+
<div class="aspect-[500/80] flex items-center">
85+
<!-- Data label (covers ~42% width) -->
86+
<div class="w-[42%] flex items-center pl-0.5">
87+
<span class="skeleton h-7 w-24" />
88+
</div>
89+
<!-- Sparkline area (~58% width) -->
90+
<div class="flex-1 flex items-end gap-0.5 h-4/5 pr-3">
91+
<span
92+
v-for="i in 16"
93+
:key="i"
94+
class="skeleton flex-1 rounded-sm"
95+
:style="{ height: `${25 + ((i * 7) % 50)}%` }"
96+
/>
97+
</div>
98+
</div>
99+
</div>
100+
</template>
71101
</ClientOnly>
72102
</div>
73103
</section>

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 }}
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)