Skip to content

Commit 04e0417

Browse files
authored
feat(registry): add download tracking via Umami (#24)
- Add dynamic route handler at /r/[name] with generateStaticParams - Track registry downloads server-side via Umami API - Remove shadcn build from build script (dynamic route replaces static files) - Add getAllItemNames() and buildRegistryItemResponse() to registry utils Co-authored-by: Justin Levine <20596508+justinlevinedotme@users.noreply.github.com>
1 parent 393af2d commit 04e0417

4 files changed

Lines changed: 131 additions & 1 deletion

File tree

apps/docs/app/r/[name]/route.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Dynamic registry route with download tracking.
3+
*
4+
* Serves registry items at /r/[name].json with Umami event tracking.
5+
* Uses generateStaticParams for build-time route generation while
6+
* still running tracking code at request time.
7+
*/
8+
9+
import { type NextRequest, NextResponse } from "next/server"
10+
import { notFound } from "next/navigation"
11+
import { getAllItemNames, buildRegistryItemResponse } from "@/lib/registry"
12+
import { trackEvent } from "@/lib/umami"
13+
14+
interface RouteParams {
15+
params: Promise<{ name: string }>
16+
}
17+
18+
export async function GET(_request: NextRequest, { params }: RouteParams) {
19+
const { name } = await params
20+
21+
if (!name.endsWith(".json")) {
22+
return NextResponse.json(
23+
{ error: "Registry items must end with .json" },
24+
{ status: 400 }
25+
)
26+
}
27+
28+
const itemName = name.replace(".json", "")
29+
30+
// Track download in production (non-blocking)
31+
if (process.env.NODE_ENV === "production") {
32+
trackEvent({
33+
name: "registry-download",
34+
url: `/r/${name}`,
35+
data: { component: itemName },
36+
})
37+
}
38+
39+
try {
40+
const item = buildRegistryItemResponse(itemName)
41+
42+
if (!item) {
43+
notFound()
44+
}
45+
46+
return NextResponse.json(item)
47+
} catch (error) {
48+
console.error(`Failed to serve registry item: ${itemName}`, error)
49+
notFound()
50+
}
51+
}
52+
53+
export async function generateStaticParams() {
54+
const names = getAllItemNames()
55+
return names.map((name) => ({ name: `${name}.json` }))
56+
}

apps/docs/lib/registry.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,34 @@ export function getRegistryItemSources(
6363
content: readRegistryFileSource(file.path),
6464
}))
6565
}
66+
67+
/**
68+
* Get all registry item names for static route generation.
69+
*/
70+
export function getAllItemNames(): string[] {
71+
return (registryData.items as RegistryItem[]).map((item) => item.name)
72+
}
73+
74+
/**
75+
* Build a full registry item JSON response with inlined file contents.
76+
* Used by the /r/[name] route handler.
77+
*/
78+
export function buildRegistryItemResponse(name: string): object | null {
79+
const item = getRegistryItem(name)
80+
if (!item) return null
81+
82+
const filesWithContent = item.files.map((file) => {
83+
const filePath = join(process.cwd(), file.path)
84+
const content = readFileSync(filePath, "utf-8")
85+
return {
86+
...file,
87+
content,
88+
}
89+
})
90+
91+
return {
92+
$schema: "https://ui.shadcn.com/schema/registry-item.json",
93+
...item,
94+
files: filesWithContent,
95+
}
96+
}

apps/docs/lib/umami.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Umami server-side tracking for registry downloads.
3+
*
4+
* Sends events to Umami without blocking the response.
5+
* Requires UMAMI_HOST_URL and UMAMI_WEBSITE_ID env vars.
6+
*/
7+
8+
interface TrackEventOptions {
9+
name: string
10+
url?: string
11+
data?: Record<string, string | number | boolean>
12+
}
13+
14+
const UMAMI_HOST_URL = process.env.UMAMI_HOST_URL
15+
const UMAMI_WEBSITE_ID = process.env.UMAMI_WEBSITE_ID
16+
17+
export async function trackEvent({ name, url, data }: TrackEventOptions): Promise<void> {
18+
if (!UMAMI_HOST_URL || !UMAMI_WEBSITE_ID) {
19+
return
20+
}
21+
22+
try {
23+
await fetch(`${UMAMI_HOST_URL}/api/send`, {
24+
method: "POST",
25+
headers: {
26+
"Content-Type": "application/json",
27+
"User-Agent": "jalco-ui/registry",
28+
},
29+
body: JSON.stringify({
30+
type: "event",
31+
payload: {
32+
website: UMAMI_WEBSITE_ID,
33+
hostname: "ui.justinlevine.me",
34+
url: url ?? "/",
35+
name,
36+
data,
37+
},
38+
}),
39+
})
40+
} catch {
41+
// Silently fail — don't block registry responses for analytics
42+
}
43+
}

apps/docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"packageManager": "pnpm@10.30.3",
66
"scripts": {
77
"dev": "tsx scripts/generate-preview-imports.ts && next dev",
8-
"build": "shadcn build && tsx scripts/build-registry-index.ts && tsx scripts/generate-preview-imports.ts && next build",
8+
"build": "tsx scripts/build-registry-index.ts && tsx scripts/generate-preview-imports.ts && next build",
99
"start": "next start",
1010
"lint": "eslint .",
1111
"registry:build": "shadcn build",

0 commit comments

Comments
 (0)