Free15 minutesintermediate

Next.js Integration

Integrate Zenovay with Next.js - App Router, Pages Router, and server-side tracking support.

nextjsreactintegrationssrapp-router
Last updated: January 15, 2025

Integrate Zenovay analytics into your Next.js application with support for both App Router and Pages Router using the tracking script tag.

Installation

No npm package is needed. Add the Zenovay tracking script to your app using Next.js Script component or a standard <script> tag.

App Router Setup (Next.js 13+)

Add to Root Layout

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://api.zenovay.com/z.js"
          data-tracking-code="YOUR_TRACKING_CODE"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

With Environment Variable

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://api.zenovay.com/z.js"
          data-tracking-code={process.env.NEXT_PUBLIC_ZENOVAY_TRACKING_CODE}
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

Client Component for Events

Create a client component for tracking custom events:

// components/Analytics.tsx
'use client';

import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';

export function Analytics() {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    // Track page view on route change (SPA navigation)
    if (window.zenovay) {
      window.zenovay('page');
    }
  }, [pathname, searchParams]);

  return null;
}

Add to layout:

// app/layout.tsx
import Script from 'next/script';
import { Analytics } from '@/components/Analytics';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Analytics />
        <Script
          src="https://api.zenovay.com/z.js"
          data-tracking-code="YOUR_TRACKING_CODE"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

Pages Router Setup

Add to _app.tsx

// pages/_app.tsx
import Script from 'next/script';
import type { AppProps } from 'next/app';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <Script
        src="https://api.zenovay.com/z.js"
        data-tracking-code="YOUR_TRACKING_CODE"
        strategy="afterInteractive"
      />
    </>
  );
}

Or use Script in _document.tsx

// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document';
import Script from 'next/script';

export default function Document() {
  return (
    <Html>
      <Head />
      <body>
        <Main />
        <NextScript />
        <Script
          src="https://api.zenovay.com/z.js"
          data-tracking-code="YOUR_TRACKING_CODE"
          strategy="afterInteractive"
        />
      </body>
    </Html>
  );
}

Event Tracking

Client Component

'use client';

export function SignupButton() {
  const handleClick = () => {
    if (window.zenovay) {
      window.zenovay('track', 'signup_click', {
        plan: 'pro',
        source: 'pricing'
      });
    }
  };

  return (
    <button onClick={handleClick}>
      Start Free Trial
    </button>
  );
}

Server Action Tracking

For server-side event tracking, use the tracking endpoint directly:

// app/actions.ts
'use server';

import { headers } from 'next/headers';

export async function submitForm(formData: FormData) {
  // Process form
  const email = formData.get('email');
  const headersList = headers();

  // Track on server using the tracking endpoint
  await fetch('https://api.zenovay.com/e/YOUR_TRACKING_CODE', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Forwarded-For': headersList.get('x-forwarded-for') || '',
    },
    body: JSON.stringify({
      type: 'form_submitted',
      url: headersList.get('referer') || '',
      props: {
        form: 'contact',
        has_email: !!email,
      },
    }),
  });

  return { success: true };
}

User Identification

After Authentication

'use client';

import { useEffect } from 'react';
import { useSession } from 'next-auth/react';

export function UserIdentifier() {
  const { data: session } = useSession();

  useEffect(() => {
    if (session?.user && window.zenovay) {
      window.zenovay('identify', session.user.id, {
        email: session.user.email,
        name: session.user.name
      });
    }
  }, [session]);

  return null;
}

Revenue Tracking

E-commerce Checkout

'use client';

import { useEffect } from 'react';

export function OrderConfirmation({ order }) {
  useEffect(() => {
    if (window.zenovay) {
      window.zenovay('revenue', order.total, 'USD', {
        order_id: order.id,
        items: order.items
      });
    }
  }, [order.id]);

  return (
    <div>
      <h1>Order Confirmed!</h1>
    </div>
  );
}

Environment Variables

Setup

# .env.local
NEXT_PUBLIC_ZENOVAY_TRACKING_CODE=your-tracking-code
ZENOVAY_API_KEY=your-api-key  # For server-side

Usage

// Client-side (in layout)
<Script
  src="https://api.zenovay.com/z.js"
  data-tracking-code={process.env.NEXT_PUBLIC_ZENOVAY_TRACKING_CODE}
  strategy="afterInteractive"
/>

// Server-side (in API routes or server actions)
const response = await fetch('https://api.zenovay.com/e/YOUR_TRACKING_CODE', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ type: 'page_view', url: '/page' })
});

Server-Side Analytics

API Route Handler

// app/api/track/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();

  await fetch('https://api.zenovay.com/e/YOUR_TRACKING_CODE', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Forwarded-For': request.headers.get('x-forwarded-for') || '',
    },
    body: JSON.stringify({
      type: body.type || 'page_view',
      url: body.url || '/',
      props: body.props,
    })
  });

  return NextResponse.json({ success: true });
}

Track in Server Component

For server components, use the tracking endpoint:

// app/products/[id]/page.tsx
import { headers } from 'next/headers';

export default async function ProductPage({ params }) {
  const headersList = headers();

  // Track server-side pageview using the tracking endpoint
  await fetch('https://api.zenovay.com/e/YOUR_TRACKING_CODE', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Forwarded-For': headersList.get('x-forwarded-for') || '',
      'User-Agent': headersList.get('user-agent') || '',
    },
    body: JSON.stringify({
      type: 'page_view',
      url: `/products/${params.id}`,
    })
  });

  const product = await getProduct(params.id);

  return <ProductDetails product={product} />;
}

Middleware Tracking

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Track in middleware for all requests
  fetch('https://api.zenovay.com/e/YOUR_TRACKING_CODE', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Forwarded-For': request.headers.get('x-forwarded-for') || '',
      'User-Agent': request.headers.get('user-agent') || '',
    },
    body: JSON.stringify({
      type: 'page_view',
      url: request.nextUrl.pathname,
      referrer: request.headers.get('referer') || '',
    })
  }).catch(() => {}); // Fire and forget

  return NextResponse.next();
}

export const config = {
  matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
};

Route Groups Analytics

// app/(marketing)/layout.tsx
import Script from 'next/script';

export default function MarketingLayout({ children }) {
  return (
    <>
      {children}
      <Script
        src="https://api.zenovay.com/z.js"
        data-tracking-code="MARKETING_TRACKING_CODE"
        strategy="afterInteractive"
      />
    </>
  );
}

// app/(app)/layout.tsx
import Script from 'next/script';

export default function AppLayout({ children }) {
  return (
    <>
      {children}
      <Script
        src="https://api.zenovay.com/z.js"
        data-tracking-code="APP_TRACKING_CODE"
        strategy="afterInteractive"
      />
    </>
  );
}

TypeScript Types

// types/zenovay.d.ts
interface ZenovayFunction {
  (command: 'track', name: string, properties?: Record<string, unknown>): void;
  (command: 'identify', userId: string, traits?: Record<string, unknown>): void;
  (command: 'goal', name: string, properties?: Record<string, unknown>): void;
  (command: 'page'): void;
  (command: 'revenue', amount: number, currency: string, meta?: Record<string, unknown>): void;
}

declare global {
  interface Window {
    zenovay: ZenovayFunction;
  }
}

export {};

Common Patterns

With Next-Auth

// app/providers.tsx
'use client';

import { SessionProvider } from 'next-auth/react';

export function Providers({ children }) {
  return (
    <SessionProvider>
      {children}
    </SessionProvider>
  );
}

ISR/SSG Pages

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function BlogPost({ params }) {
  // Static generation - no server tracking here
  // Tracking happens on client via the Zenovay script tag
  const post = await getPost(params.slug);

  return <Article post={post} />;
}

Troubleshooting

Script Not Loading

Check:

  • Script is in layout/document
  • No ad blocker interference
  • Tracking code is correct

No Page Views

Ensure:

  • Script loads after body
  • Not in development mode (unless intended)
  • Route changes detected

Hydration Issues

Use afterInteractive strategy:

<Script
  src="https://api.zenovay.com/z.js"
  data-tracking-code="YOUR_TRACKING_CODE"
  strategy="afterInteractive"
/>

Next Steps

Was this article helpful?