Skip to content

Commit f5fc930

Browse files
committed
refactor: change routing and support 'flat' routes
1 parent 0cf26b0 commit f5fc930

10 files changed

Lines changed: 130 additions & 37 deletions

File tree

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/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>
Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@ import type {
55
PackageFileContentResponse,
66
} from '#shared/types'
77
8-
const route = useRoute('package-code-path')
8+
definePageMeta({
9+
name: 'code',
10+
alias: ['/package/code/:path(.*)*'],
11+
})
12+
13+
const route = useRoute('code')
914
const router = useRouter()
1015
1116
// Parse package name, version, and file path from URL
1217
// Patterns:
13-
// /package/code/nuxt/v/4.2.0 → packageName: "nuxt", version: "4.2.0", filePath: null (show tree)
14-
// /package/code/nuxt/v/4.2.0/src/index.ts → packageName: "nuxt", version: "4.2.0", filePath: "src/index.ts"
15-
// /package/code/@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", version: "1.0.0", filePath: null
18+
// /code/nuxt/v/4.2.0 → packageName: "nuxt", version: "4.2.0", filePath: null (show tree)
19+
// /code/nuxt/v/4.2.0/src/index.ts → packageName: "nuxt", version: "4.2.0", filePath: "src/index.ts"
20+
// /code/@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", version: "1.0.0", filePath: null
1621
const parsedRoute = computed(() => {
17-
const segments = Array.isArray(route.params.path) ? route.params.path : [route.params.path ?? '']
22+
const segments = route.params.path || []
1823
1924
// Find the /v/ separator for version
2025
const vIndex = segments.indexOf('v')
@@ -75,8 +80,8 @@ const availableVersions = computed(() => {
7580
// Version switch handler
7681
function switchVersion(newVersion: string) {
7782
const newPath = filePath.value
78-
? `/package/code/${packageName.value}/v/${newVersion}/${filePath.value}`
79-
: `/package/code/${packageName.value}/v/${newVersion}`
83+
? `/code/${packageName.value}/v/${newVersion}/${filePath.value}`
84+
: `/code/${packageName.value}/v/${newVersion}`
8085
router.push(newPath)
8186
}
8287
@@ -207,7 +212,7 @@ const breadcrumbs = computed(() => {
207212
208213
// Navigation helper - build URL for a path
209214
function getCodeUrl(path?: string): string {
210-
const base = `/package/code/${packageName.value}/v/${version.value}`
215+
const base = `/code/${packageName.value}/v/${version.value}`
211216
return path ? `${base}/${path}` : base
212217
}
213218
@@ -219,6 +224,15 @@ const orgName = computed(() => {
219224
return match ? match[1] : null
220225
})
221226
227+
// Build route object for package link (with optional version)
228+
function packageRoute(ver?: string | null) {
229+
const segments = packageName.value.split('/')
230+
if (ver) {
231+
segments.push('v', ver)
232+
}
233+
return { name: 'package' as const, params: { package: segments } }
234+
}
235+
222236
// Format file size
223237
function formatBytes(bytes: number): string {
224238
if (bytes < 1024) return `${bytes} B`
@@ -257,6 +271,19 @@ async function copyPermalink() {
257271
await navigator.clipboard.writeText(url.toString())
258272
}
259273
274+
// Canonical URL for this code page
275+
const canonicalUrl = computed(() => {
276+
let url = `https://npmx.dev/code/${packageName.value}/v/${version.value}`
277+
if (filePath.value) {
278+
url += `/${filePath.value}`
279+
}
280+
return url
281+
})
282+
283+
useHead({
284+
link: [{ rel: 'canonical', href: canonicalUrl }],
285+
})
286+
260287
useSeoMeta({
261288
title: () => {
262289
if (filePath.value) {
@@ -276,7 +303,7 @@ useSeoMeta({
276303
<!-- Package info and navigation -->
277304
<div class="flex items-center gap-2 mb-3 flex-wrap">
278305
<NuxtLink
279-
:to="`/package/${packageName}${version ? `/v/${version}` : ''}`"
306+
:to="packageRoute(version)"
280307
class="font-mono text-lg font-medium hover:text-fg transition-colors"
281308
>
282309
<span v-if="orgName" class="text-fg-muted">@{{ orgName }}/</span
@@ -337,7 +364,7 @@ useSeoMeta({
337364
<!-- Error: no version -->
338365
<div v-if="!version" class="container py-20 text-center">
339366
<p class="text-fg-muted mb-4">Version is required to browse code</p>
340-
<NuxtLink :to="`/package/${packageName}`" class="btn"> Go to package </NuxtLink>
367+
<NuxtLink :to="packageRoute()" class="btn"> Go to package </NuxtLink>
341368
</div>
342369

343370
<!-- Loading state -->
@@ -349,9 +376,7 @@ useSeoMeta({
349376
<!-- Error state -->
350377
<div v-else-if="treeStatus === 'error'" class="container py-20 text-center" role="alert">
351378
<p class="text-fg-muted mb-4">Failed to load files for this package version</p>
352-
<NuxtLink :to="`/package/${packageName}${version ? `/v/${version}` : ''}`" class="btn">
353-
Back to package
354-
</NuxtLink>
379+
<NuxtLink :to="packageRoute(version)" class="btn"> Back to package </NuxtLink>
355380
</div>
356381

357382
<!-- Main content: file tree + file viewer -->

app/pages/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ defineOgImageComponent('Default')
9898
:key="pkg"
9999
>
100100
<NuxtLink
101-
:to="`/package/${pkg}`"
101+
:to="{ name: 'package', params: { package: [pkg] } }"
102102
class="link-subtle font-mono text-sm inline-flex items-center gap-2 group"
103103
>
104104
<span

server/utils/code-highlight.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,10 @@ function linkifyImports(html: string, options?: LinkifyOptions): string {
184184
const dep = dependencies?.[packageName]
185185
if (dep) {
186186
// Link to code browser with resolved version
187-
return `/package/code/${packageName}/v/${dep.version}`
187+
return `/code/${packageName}/v/${dep.version}`
188188
}
189189
// Fall back to package page if not a known dependency
190-
return `/package/${packageName}`
190+
return `/${packageName}`
191191
}
192192

193193
// Match: from keyword span followed by string span containing module specifier

server/utils/import-resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export function createImportResolver(
212212
return (specifier: string) => {
213213
const resolved = resolveRelativeImport(specifier, currentFile, files)
214214
if (resolved) {
215-
return `/package/code/${packageName}/v/${version}/${resolved.path}`
215+
return `/code/${packageName}/v/${version}/${resolved.path}`
216216
}
217217
return null
218218
}

0 commit comments

Comments
 (0)