diff --git a/apps/docs/app/r/[name]/route.ts b/apps/docs/app/r/[name]/route.ts
index e45d10e..82f4c13 100644
--- a/apps/docs/app/r/[name]/route.ts
+++ b/apps/docs/app/r/[name]/route.ts
@@ -1,7 +1,7 @@
/**
* Dynamic registry route with download tracking.
*
- * Serves registry items at /r/[name].json with Umami event tracking.
+ * Serves registry items at /r/[name].json with OpenPanel event tracking.
* Uses generateStaticParams for build-time route generation while
* still running tracking code at request time.
*/
@@ -9,7 +9,7 @@
import { type NextRequest, NextResponse } from "next/server"
import { notFound } from "next/navigation"
import { getAllItemNames, buildRegistryItemResponse } from "@/lib/registry"
-import { trackEvent } from "@/lib/umami"
+import { trackEvent } from "@/lib/openpanel"
interface RouteParams {
params: Promise<{ name: string }>
@@ -31,7 +31,6 @@ export async function GET(_request: NextRequest, { params }: RouteParams) {
if (process.env.NODE_ENV === "production") {
trackEvent({
name: "registry-download",
- url: `/r/${name}`,
data: { component: itemName },
})
}
diff --git a/apps/docs/components/analytics.tsx b/apps/docs/components/analytics.tsx
index 23090f9..6925e53 100644
--- a/apps/docs/components/analytics.tsx
+++ b/apps/docs/components/analytics.tsx
@@ -1,14 +1,14 @@
"use client"
-import { useEffect } from "react"
-import { op } from "@/lib/openpanel"
+import { OpenPanelComponent } from "@openpanel/nextjs"
export function Analytics() {
- useEffect(() => {
- // OpenPanel auto-tracks screen views, outgoing links, and data-track attributes
- // Accessing op ensures the client is initialized on mount
- void op
- }, [])
-
- return null
+ return (
+
+ )
}
diff --git a/apps/docs/lib/analytics.ts b/apps/docs/lib/analytics.ts
index c24a854..ad837bd 100644
--- a/apps/docs/lib/analytics.ts
+++ b/apps/docs/lib/analytics.ts
@@ -1,13 +1,13 @@
-declare global {
- interface Window {
- umami?: {
- track: (event: string, properties?: Record) => void
- }
- }
-}
+/**
+ * Client-side analytics helper for OpenPanel.
+ *
+ * Provides a simple track function that can be called from anywhere.
+ * Uses the global OpenPanel instance injected by OpenPanelComponent.
+ */
export function track(event: string, properties?: Record) {
- if (typeof window !== "undefined" && window.umami) {
- window.umami.track(event, properties)
+ if (typeof window !== "undefined" && "op" in window) {
+ const op = window.op as { track: (event: string, properties?: Record) => void }
+ op.track(event, properties)
}
}
diff --git a/apps/docs/lib/openpanel.ts b/apps/docs/lib/openpanel.ts
index fec5822..6b4efe0 100644
--- a/apps/docs/lib/openpanel.ts
+++ b/apps/docs/lib/openpanel.ts
@@ -1,8 +1,33 @@
-import { OpenPanel } from "@openpanel/web"
-
-export const op = new OpenPanel({
- clientId: process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID ?? "",
- trackScreenViews: true,
- trackOutgoingLinks: true,
- trackAttributes: true,
-})
+/**
+ * OpenPanel server-side tracking for registry downloads.
+ *
+ * Sends events to OpenPanel without blocking the response.
+ * Requires NEXT_PUBLIC_OPENPANEL_CLIENT_ID and OPENPANEL_CLIENT_SECRET env vars.
+ */
+
+import { OpenPanel } from "@openpanel/nextjs"
+
+const clientId = process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID
+const clientSecret = process.env.OPENPANEL_CLIENT_SECRET
+
+export const opServer =
+ clientId && clientSecret
+ ? new OpenPanel({ clientId, clientSecret })
+ : null
+
+interface TrackEventOptions {
+ name: string
+ data?: Record
+}
+
+export async function trackEvent({ name, data }: TrackEventOptions): Promise {
+ if (!opServer) {
+ return
+ }
+
+ try {
+ await opServer.track(name, data ?? {})
+ } catch {
+ // Silently fail — don't block registry responses for analytics
+ }
+}
diff --git a/apps/docs/lib/umami.ts b/apps/docs/lib/umami.ts
deleted file mode 100644
index db0602d..0000000
--- a/apps/docs/lib/umami.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * 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 bbcd11e..2731e76 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -15,7 +15,7 @@
},
"dependencies": {
"@chenglou/pretext": "^0.0.4",
- "@openpanel/web": "^1.4.0",
+ "@openpanel/nextjs": "^1.5.0",
"@orama/orama": "^3.1.18",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5c8714b..8a6746b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,9 +17,9 @@ importers:
'@chenglou/pretext':
specifier: ^0.0.4
version: 0.0.4
- '@openpanel/web':
- specifier: ^1.4.0
- version: 1.4.0
+ '@openpanel/nextjs':
+ specifier: ^1.5.0
+ version: 1.5.0(next@16.2.1(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@orama/orama':
specifier: ^3.1.18
version: 3.1.18
@@ -909,6 +909,13 @@ packages:
'@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
+ '@openpanel/nextjs@1.5.0':
+ resolution: {integrity: sha512-CuEUdYb8LZxrsoO1+6EXKaCp6tJtEeQzStaEUVBwpewV50mp0pOf3JOboH4K61nIBy3CDLLpbnzUw3iMgrqSYQ==}
+ peerDependencies:
+ next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
'@openpanel/sdk@1.3.0':
resolution: {integrity: sha512-VK/1oawBjGdxA+oYtqcWlNXlLT1zRJ9tslHoMvqqsqlcLNOhH26ltcHpyGp5RhtIF7uIkCltiicALfFN7fyldw==}
@@ -5684,6 +5691,13 @@ snapshots:
'@open-draft/until@2.1.0': {}
+ '@openpanel/nextjs@1.5.0(next@16.2.1(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@openpanel/web': 1.4.0
+ next: 16.2.1(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
'@openpanel/sdk@1.3.0': {}
'@openpanel/web@1.4.0':