From 1eb7f42bcbe16623b33bb9783f4a378a6043fb7d Mon Sep 17 00:00:00 2001 From: Justin Levine <20596508+justinlevinedotme@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:15:03 -0700 Subject: [PATCH] feat(registry): add download tracking via Umami - 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 --- apps/docs/app/r/[name]/route.ts | 56 +++++++++++++++++++++++++++++++++ apps/docs/lib/registry.ts | 31 ++++++++++++++++++ apps/docs/lib/umami.ts | 43 +++++++++++++++++++++++++ apps/docs/package.json | 2 +- 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 apps/docs/app/r/[name]/route.ts create mode 100644 apps/docs/lib/umami.ts diff --git a/apps/docs/app/r/[name]/route.ts b/apps/docs/app/r/[name]/route.ts new file mode 100644 index 0000000..e45d10e --- /dev/null +++ b/apps/docs/app/r/[name]/route.ts @@ -0,0 +1,56 @@ +/** + * Dynamic registry route with download tracking. + * + * Serves registry items at /r/[name].json with Umami event tracking. + * Uses generateStaticParams for build-time route generation while + * still running tracking code at request time. + */ + +import { type NextRequest, NextResponse } from "next/server" +import { notFound } from "next/navigation" +import { getAllItemNames, buildRegistryItemResponse } from "@/lib/registry" +import { trackEvent } from "@/lib/umami" + +interface RouteParams { + params: Promise<{ name: string }> +} + +export async function GET(_request: NextRequest, { params }: RouteParams) { + const { name } = await params + + if (!name.endsWith(".json")) { + return NextResponse.json( + { error: "Registry items must end with .json" }, + { status: 400 } + ) + } + + const itemName = name.replace(".json", "") + + // Track download in production (non-blocking) + if (process.env.NODE_ENV === "production") { + trackEvent({ + name: "registry-download", + url: `/r/${name}`, + data: { component: itemName }, + }) + } + + try { + const item = buildRegistryItemResponse(itemName) + + if (!item) { + notFound() + } + + return NextResponse.json(item) + } catch (error) { + console.error(`Failed to serve registry item: ${itemName}`, error) + notFound() + } +} + +export async function generateStaticParams() { + const names = getAllItemNames() + return names.map((name) => ({ name: `${name}.json` })) +} diff --git a/apps/docs/lib/registry.ts b/apps/docs/lib/registry.ts index 03b8bd9..7a6da4e 100644 --- a/apps/docs/lib/registry.ts +++ b/apps/docs/lib/registry.ts @@ -63,3 +63,34 @@ export function getRegistryItemSources( content: readRegistryFileSource(file.path), })) } + +/** + * Get all registry item names for static route generation. + */ +export function getAllItemNames(): string[] { + return (registryData.items as RegistryItem[]).map((item) => item.name) +} + +/** + * Build a full registry item JSON response with inlined file contents. + * Used by the /r/[name] route handler. + */ +export function buildRegistryItemResponse(name: string): object | null { + const item = getRegistryItem(name) + if (!item) return null + + const filesWithContent = item.files.map((file) => { + const filePath = join(process.cwd(), file.path) + const content = readFileSync(filePath, "utf-8") + return { + ...file, + content, + } + }) + + return { + $schema: "https://ui.shadcn.com/schema/registry-item.json", + ...item, + files: filesWithContent, + } +} diff --git a/apps/docs/lib/umami.ts b/apps/docs/lib/umami.ts new file mode 100644 index 0000000..db0602d --- /dev/null +++ b/apps/docs/lib/umami.ts @@ -0,0 +1,43 @@ +/** + * Umami server-side tracking for registry downloads. + * + * Sends events to Umami without blocking the response. + * Requires UMAMI_HOST_URL and UMAMI_WEBSITE_ID env vars. + */ + +interface TrackEventOptions { + name: string + url?: string + data?: Record +} + +const UMAMI_HOST_URL = process.env.UMAMI_HOST_URL +const UMAMI_WEBSITE_ID = process.env.UMAMI_WEBSITE_ID + +export async function trackEvent({ name, url, data }: TrackEventOptions): Promise { + if (!UMAMI_HOST_URL || !UMAMI_WEBSITE_ID) { + return + } + + try { + await fetch(`${UMAMI_HOST_URL}/api/send`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "jalco-ui/registry", + }, + body: JSON.stringify({ + type: "event", + payload: { + website: UMAMI_WEBSITE_ID, + hostname: "ui.justinlevine.me", + url: url ?? "/", + name, + data, + }, + }), + }) + } catch { + // Silently fail — don't block registry responses for analytics + } +} diff --git a/apps/docs/package.json b/apps/docs/package.json index 2d83ff0..1faf549 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -5,7 +5,7 @@ "packageManager": "pnpm@10.30.3", "scripts": { "dev": "tsx scripts/generate-preview-imports.ts && next dev", - "build": "shadcn build && tsx scripts/build-registry-index.ts && tsx scripts/generate-preview-imports.ts && next build", + "build": "tsx scripts/build-registry-index.ts && tsx scripts/generate-preview-imports.ts && next build", "start": "next start", "lint": "eslint .", "registry:build": "shadcn build",