Skip to content

Commit 33165db

Browse files
feat: unify skill routes with owner slugs
1 parent 4c10e48 commit 33165db

File tree

16 files changed

+133
-159
lines changed

16 files changed

+133
-159
lines changed

convex/httpApiV1.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
267267
owner: result.owner
268268
? {
269269
handle: result.owner.handle ?? null,
270+
userId: result.owner._id,
270271
displayName: result.owner.displayName ?? null,
271272
image: result.owner.image ?? null,
272273
}

convex/skills.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export const getBySlug = query({
127127
},
128128
owner: {
129129
handle: forkOfOwner?.handle ?? forkOfOwner?.name ?? null,
130+
userId: forkOfOwner?._id ?? null,
130131
},
131132
}
132133
: null,
@@ -138,6 +139,7 @@ export const getBySlug = query({
138139
},
139140
owner: {
140141
handle: canonicalOwner?.handle ?? canonicalOwner?.name ?? null,
142+
userId: canonicalOwner?._id ?? null,
141143
},
142144
}
143145
: null,

convex/telemetry.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,13 @@ export const getMyInstalled = query({
8888
lastSeenAt: number
8989
expiredAt?: number
9090
skills: Array<{
91-
skill: { slug: string; displayName: string; summary?: string; stats: unknown }
91+
skill: {
92+
slug: string
93+
displayName: string
94+
summary?: string
95+
stats: unknown
96+
ownerUserId: Id<'users'>
97+
}
9298
firstSeenAt: number
9399
lastSeenAt: number
94100
lastVersion?: string
@@ -105,7 +111,13 @@ export const getMyInstalled = query({
105111

106112
const filtered = includeRemoved ? installs : installs.filter((entry) => !entry.removedAt)
107113
const skills: Array<{
108-
skill: { slug: string; displayName: string; summary?: string; stats: unknown }
114+
skill: {
115+
slug: string
116+
displayName: string
117+
summary?: string
118+
stats: unknown
119+
ownerUserId: Id<'users'>
120+
}
109121
firstSeenAt: number
110122
lastSeenAt: number
111123
lastVersion?: string
@@ -121,6 +133,7 @@ export const getMyInstalled = query({
121133
displayName: skill.displayName,
122134
summary: skill.summary,
123135
stats: skill.stats,
136+
ownerUserId: skill.ownerUserId,
124137
},
125138
firstSeenAt: entry.firstSeenAt,
126139
lastSeenAt: entry.lastSeenAt,

src/components/SkillCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ type SkillCardProps = {
1212
}
1313

1414
export function SkillCard({ skill, badge, chip, summaryFallback, meta, href }: SkillCardProps) {
15-
const link = href ?? `/skills/${skill.slug}`
15+
const owner = encodeURIComponent(String(skill.ownerUserId))
16+
const link = href ?? `/${owner}/${skill.slug}`
1617
const badges = Array.isArray(badge) ? badge : badge ? [badge] : []
1718

1819
return (

src/components/SkillDetailPage.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ type SkillBySlugResult = {
2525
kind: 'fork' | 'duplicate'
2626
version: string | null
2727
skill: { slug: string; displayName: string }
28-
owner: { handle: string | null }
28+
owner: { handle: string | null; userId: Id<'users'> | null }
2929
} | null
3030
canonical: {
3131
skill: { slug: string; displayName: string }
32-
owner: { handle: string | null }
32+
owner: { handle: string | null; userId: Id<'users'> | null }
3333
} | null
3434
} | null
3535

@@ -81,33 +81,36 @@ export function SkillDetailPage({
8181
const isStaff = isModerator(me)
8282

8383
const ownerHandle = owner?.handle ?? owner?.name ?? null
84+
const ownerParam = ownerHandle ?? (owner?._id ? String(owner._id) : null)
8485
const wantsCanonicalRedirect = Boolean(
85-
ownerHandle &&
86+
ownerParam &&
8687
(redirectToCanonical ||
87-
(typeof canonicalOwner === 'string' && canonicalOwner && canonicalOwner !== ownerHandle)),
88+
(typeof canonicalOwner === 'string' && canonicalOwner && canonicalOwner !== ownerParam)),
8889
)
8990

9091
const forkOf = result?.forkOf ?? null
9192
const canonical = result?.canonical ?? null
9293
const forkOfLabel = forkOf?.kind === 'duplicate' ? 'duplicate of' : 'fork of'
9394
const forkOfOwnerHandle = forkOf?.owner?.handle ?? null
95+
const forkOfOwnerId = forkOf?.owner?.userId ?? null
9496
const canonicalOwnerHandle = canonical?.owner?.handle ?? null
97+
const canonicalOwnerId = canonical?.owner?.userId ?? null
9598
const forkOfHref = forkOf?.skill?.slug
96-
? buildSkillHref(forkOfOwnerHandle, forkOf.skill.slug)
99+
? buildSkillHref(forkOfOwnerHandle, forkOfOwnerId, forkOf.skill.slug)
97100
: null
98101
const canonicalHref =
99102
canonical?.skill?.slug && canonical.skill.slug !== forkOf?.skill?.slug
100-
? buildSkillHref(canonicalOwnerHandle, canonical.skill.slug)
103+
? buildSkillHref(canonicalOwnerHandle, canonicalOwnerId, canonical.skill.slug)
101104
: null
102105

103106
useEffect(() => {
104-
if (!wantsCanonicalRedirect || !ownerHandle) return
107+
if (!wantsCanonicalRedirect || !ownerParam) return
105108
void navigate({
106109
to: '/$owner/$slug',
107-
params: { owner: ownerHandle, slug },
110+
params: { owner: ownerParam, slug },
108111
replace: true,
109112
})
110-
}, [navigate, ownerHandle, slug, wantsCanonicalRedirect])
113+
}, [navigate, ownerParam, slug, wantsCanonicalRedirect])
111114

112115
const versionById = new Map<Id<'skillVersions'>, Doc<'skillVersions'>>(
113116
(diffVersions ?? versions ?? []).map((version) => [version._id, version]),
@@ -640,9 +643,9 @@ export function SkillDetailPage({
640643
)
641644
}
642645

643-
function buildSkillHref(ownerHandle: string | null, slug: string) {
644-
if (ownerHandle) return `/${ownerHandle}/${slug}`
645-
return `/skills/${slug}`
646+
function buildSkillHref(ownerHandle: string | null, ownerId: Id<'users'> | null, slug: string) {
647+
const owner = ownerHandle?.trim() || (ownerId ? String(ownerId) : 'unknown')
648+
return `/${owner}/${slug}`
646649
}
647650

648651
function formatConfigSnippet(raw: string) {

src/lib/og.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('og helpers', () => {
5050
const meta = buildSkillMeta({ slug: 'parser' })
5151
expect(meta.title).toBe('parser — MoltHub')
5252
expect(meta.description).toMatch(/MoltHub a fast skill registry/i)
53-
expect(meta.url).toContain('/skills/parser')
53+
expect(meta.url).toContain('/unknown/parser')
5454
expect(meta.owner).toBeNull()
5555
expect(meta.image).toContain('slug=parser')
5656
})
@@ -76,7 +76,7 @@ describe('og helpers', () => {
7676
ok: true,
7777
json: async () => ({
7878
skill: { displayName: 'Weather', summary: 'Forecasts' },
79-
owner: { handle: 'steipete' },
79+
owner: { handle: 'steipete', userId: 'users:1' },
8080
latestVersion: { version: '1.2.3' },
8181
}),
8282
}))
@@ -87,6 +87,7 @@ describe('og helpers', () => {
8787
displayName: 'Weather',
8888
summary: 'Forecasts',
8989
owner: 'steipete',
90+
ownerId: 'users:1',
9091
version: '1.2.3',
9192
})
9293
})

src/lib/og.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getMoltHubSiteUrl, getOnlyCrabsSiteUrl } from './site'
33
type SkillMetaSource = {
44
slug: string
55
owner?: string | null
6+
ownerId?: string | null
67
displayName?: string | null
78
summary?: string | null
89
version?: string | null
@@ -58,13 +59,14 @@ export async function fetchSkillMeta(slug: string) {
5859
if (!response.ok) return null
5960
const payload = (await response.json()) as {
6061
skill?: { displayName?: string; summary?: string | null } | null
61-
owner?: { handle?: string | null } | null
62+
owner?: { handle?: string | null; userId?: string | null } | null
6263
latestVersion?: { version?: string | null } | null
6364
}
6465
return {
6566
displayName: payload.skill?.displayName ?? null,
6667
summary: payload.skill?.summary ?? null,
6768
owner: payload.owner?.handle ?? null,
69+
ownerId: payload.owner?.userId ?? null,
6870
version: payload.latestVersion?.version ?? null,
6971
}
7072
} catch {
@@ -97,13 +99,15 @@ export async function fetchSoulMeta(slug: string) {
9799
export function buildSkillMeta(source: SkillMetaSource): SkillMeta {
98100
const siteUrl = getSiteUrl()
99101
const owner = clean(source.owner)
102+
const ownerId = clean(source.ownerId)
100103
const displayName = clean(source.displayName) || clean(source.slug)
101104
const summary = clean(source.summary)
102105
const version = clean(source.version)
103106
const title = `${displayName} — MoltHub`
104107
const description =
105108
summary || (owner ? `Agent skill by @${owner} on MoltHub.` : DEFAULT_DESCRIPTION)
106-
const url = owner ? `${siteUrl}/${owner}/${source.slug}` : `${siteUrl}/skills/${source.slug}`
109+
const ownerPath = owner || ownerId || 'unknown'
110+
const url = `${siteUrl}/${ownerPath}/${source.slug}`
107111
const imageParams = new URLSearchParams()
108112
imageParams.set('v', OG_SKILL_IMAGE_LAYOUT_VERSION)
109113
imageParams.set('slug', source.slug)

src/routeTree.gen.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { Route as SoulsIndexRouteImport } from './routes/souls/index'
2121
import { Route as SkillsIndexRouteImport } from './routes/skills/index'
2222
import { Route as UHandleRouteImport } from './routes/u/$handle'
2323
import { Route as SoulsSlugRouteImport } from './routes/souls/$slug'
24-
import { Route as SkillsSlugRouteImport } from './routes/skills/$slug'
2524
import { Route as CliAuthRouteImport } from './routes/cli/auth'
2625
import { Route as OwnerSlugRouteImport } from './routes/$owner/$slug'
2726

@@ -85,11 +84,6 @@ const SoulsSlugRoute = SoulsSlugRouteImport.update({
8584
path: '/souls/$slug',
8685
getParentRoute: () => rootRouteImport,
8786
} as any)
88-
const SkillsSlugRoute = SkillsSlugRouteImport.update({
89-
id: '/skills/$slug',
90-
path: '/skills/$slug',
91-
getParentRoute: () => rootRouteImport,
92-
} as any)
9387
const CliAuthRoute = CliAuthRouteImport.update({
9488
id: '/cli/auth',
9589
path: '/cli/auth',
@@ -112,7 +106,6 @@ export interface FileRoutesByFullPath {
112106
'/upload': typeof UploadRoute
113107
'/$owner/$slug': typeof OwnerSlugRoute
114108
'/cli/auth': typeof CliAuthRoute
115-
'/skills/$slug': typeof SkillsSlugRoute
116109
'/souls/$slug': typeof SoulsSlugRoute
117110
'/u/$handle': typeof UHandleRoute
118111
'/skills/': typeof SkillsIndexRoute
@@ -129,7 +122,6 @@ export interface FileRoutesByTo {
129122
'/upload': typeof UploadRoute
130123
'/$owner/$slug': typeof OwnerSlugRoute
131124
'/cli/auth': typeof CliAuthRoute
132-
'/skills/$slug': typeof SkillsSlugRoute
133125
'/souls/$slug': typeof SoulsSlugRoute
134126
'/u/$handle': typeof UHandleRoute
135127
'/skills': typeof SkillsIndexRoute
@@ -147,7 +139,6 @@ export interface FileRoutesById {
147139
'/upload': typeof UploadRoute
148140
'/$owner/$slug': typeof OwnerSlugRoute
149141
'/cli/auth': typeof CliAuthRoute
150-
'/skills/$slug': typeof SkillsSlugRoute
151142
'/souls/$slug': typeof SoulsSlugRoute
152143
'/u/$handle': typeof UHandleRoute
153144
'/skills/': typeof SkillsIndexRoute
@@ -166,7 +157,6 @@ export interface FileRouteTypes {
166157
| '/upload'
167158
| '/$owner/$slug'
168159
| '/cli/auth'
169-
| '/skills/$slug'
170160
| '/souls/$slug'
171161
| '/u/$handle'
172162
| '/skills/'
@@ -183,7 +173,6 @@ export interface FileRouteTypes {
183173
| '/upload'
184174
| '/$owner/$slug'
185175
| '/cli/auth'
186-
| '/skills/$slug'
187176
| '/souls/$slug'
188177
| '/u/$handle'
189178
| '/skills'
@@ -200,7 +189,6 @@ export interface FileRouteTypes {
200189
| '/upload'
201190
| '/$owner/$slug'
202191
| '/cli/auth'
203-
| '/skills/$slug'
204192
| '/souls/$slug'
205193
| '/u/$handle'
206194
| '/skills/'
@@ -218,7 +206,6 @@ export interface RootRouteChildren {
218206
UploadRoute: typeof UploadRoute
219207
OwnerSlugRoute: typeof OwnerSlugRoute
220208
CliAuthRoute: typeof CliAuthRoute
221-
SkillsSlugRoute: typeof SkillsSlugRoute
222209
SoulsSlugRoute: typeof SoulsSlugRoute
223210
UHandleRoute: typeof UHandleRoute
224211
SkillsIndexRoute: typeof SkillsIndexRoute
@@ -311,13 +298,6 @@ declare module '@tanstack/react-router' {
311298
preLoaderRoute: typeof SoulsSlugRouteImport
312299
parentRoute: typeof rootRouteImport
313300
}
314-
'/skills/$slug': {
315-
id: '/skills/$slug'
316-
path: '/skills/$slug'
317-
fullPath: '/skills/$slug'
318-
preLoaderRoute: typeof SkillsSlugRouteImport
319-
parentRoute: typeof rootRouteImport
320-
}
321301
'/cli/auth': {
322302
id: '/cli/auth'
323303
path: '/cli/auth'
@@ -346,7 +326,6 @@ const rootRouteChildren: RootRouteChildren = {
346326
UploadRoute: UploadRoute,
347327
OwnerSlugRoute: OwnerSlugRoute,
348328
CliAuthRoute: CliAuthRoute,
349-
SkillsSlugRoute: SkillsSlugRoute,
350329
SoulsSlugRoute: SoulsSlugRoute,
351330
UHandleRoute: UHandleRoute,
352331
SkillsIndexRoute: SkillsIndexRoute,

src/routes/dashboard.tsx

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function Dashboard() {
2424
}
2525

2626
const skills = mySkills ?? []
27-
const ownerHandle = me.handle ?? null
27+
const ownerHandle = me.handle ?? me.name ?? me.displayName ?? me._id
2828

2929
return (
3030
<main className="section">
@@ -63,19 +63,13 @@ function SkillCard({ skill, ownerHandle }: { skill: Doc<'skills'>; ownerHandle:
6363
return (
6464
<div className="dashboard-skill-card">
6565
<div className="dashboard-skill-info">
66-
{ownerHandle ? (
67-
<Link
68-
to="/$owner/$slug"
69-
params={{ owner: ownerHandle, slug: skill.slug }}
70-
className="dashboard-skill-name"
71-
>
72-
{skill.displayName}
73-
</Link>
74-
) : (
75-
<Link to="/skills/$slug" params={{ slug: skill.slug }} className="dashboard-skill-name">
76-
{skill.displayName}
77-
</Link>
78-
)}
66+
<Link
67+
to="/$owner/$slug"
68+
params={{ owner: ownerHandle ?? 'unknown', slug: skill.slug }}
69+
className="dashboard-skill-name"
70+
>
71+
{skill.displayName}
72+
</Link>
7973
<span className="dashboard-skill-slug">/{skill.slug}</span>
8074
{skill.summary && <p className="dashboard-skill-description">{skill.summary}</p>}
8175
<div className="dashboard-skill-stats">
@@ -89,19 +83,13 @@ function SkillCard({ skill, ownerHandle }: { skill: Doc<'skills'>; ownerHandle:
8983
<Upload className="h-3 w-3" aria-hidden="true" />
9084
New Version
9185
</Link>
92-
{ownerHandle ? (
93-
<Link
94-
to="/$owner/$slug"
95-
params={{ owner: ownerHandle, slug: skill.slug }}
96-
className="btn btn-ghost btn-sm"
97-
>
98-
View
99-
</Link>
100-
) : (
101-
<Link to="/skills/$slug" params={{ slug: skill.slug }} className="btn btn-ghost btn-sm">
102-
View
103-
</Link>
104-
)}
86+
<Link
87+
to="/$owner/$slug"
88+
params={{ owner: ownerHandle ?? 'unknown', slug: skill.slug }}
89+
className="btn btn-ghost btn-sm"
90+
>
91+
View
92+
</Link>
10593
</div>
10694
</div>
10795
)

src/routes/import.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type CandidatePreview = {
3838
}
3939

4040
function ImportGitHub() {
41-
const { isAuthenticated, isLoading } = useAuthStatus()
41+
const { isAuthenticated, isLoading, me } = useAuthStatus()
4242
const previewImport = useAction(api.githubImport.previewGitHubImport)
4343
const previewCandidate = useAction(api.githubImport.previewGitHubImportCandidate)
4444
const importSkill = useAction(api.githubImport.importGitHubSkill)
@@ -167,7 +167,8 @@ function ImportGitHub() {
167167
})
168168
const nextSlug = result.slug
169169
setStatus('Imported.')
170-
await navigate({ to: '/skills/$slug', params: { slug: nextSlug } })
170+
const ownerParam = me?.handle ?? (me?._id ? String(me._id) : 'unknown')
171+
await navigate({ to: '/$owner/$slug', params: { owner: ownerParam, slug: nextSlug } })
171172
} catch (e) {
172173
setError(e instanceof Error ? e.message : 'Import failed')
173174
setStatus(null)

0 commit comments

Comments
 (0)