Bento Next.js guide
Install Bento’s Next.js SDK, wire up analytics in either router, and extend tracking with server-side endpoints plus TypeScript safety.
Whether you ship on the App Router or the classic Pages Router, Bento’s SDK drops straight into your layout. Use it to track page views, identify users, fire custom events, and bridge into Node/Edge APIs for secure workflows.
Getting Started
Beginner Guide
Intermediate Guide
Advanced Guide
Reference
Getting started
Install packages
Install the client-side SDK for analytics plus the optional Node SDK when you want authenticated API routes and server actions.
Using npm
bashnpm install @bentonow/bento-nextjs-sdk --saveUsing npm
bashnpm install @bentonow/bento-node-sdk --saveConfigure environment
Store your Site UUID, publishable key, and optional secret key in `.env`. Generate keys from Bento > Account > Team Settings > Private API Keys.
Environment Configuration
tsxNEXT_PUBLIC_BENTO_SITE_ID=your_site_uuid
BENTO_PUBLISHABLE_KEY=your_publishable_key
BENTO_SECRET_KEY=your_secret_keyDrop analytics into your layout
The SDK ships two components—one for the App Router and one for the Pages Router. Each watches route changes and emits `view` events automatically.
Next.js 13+ (App Router)
app/layout.tsx
tsximport { BentoAnalytics } from '@bentonow/bento-nextjs-sdk/analytics'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<BentoAnalytics
siteUuid={process.env.NEXT_PUBLIC_BENTO_SITE_ID!}
userEmail="" // Will be set dynamically for logged-in users
/>
{children}
</body>
</html>
)
}Next.js 12 (Pages Router)
pages/_app.tsx
tsximport { BentoLegacyAnalytics } from '@bentonow/bento-nextjs-sdk/analytics/legacy'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<BentoLegacyAnalytics
siteUuid={process.env.NEXT_PUBLIC_BENTO_SITE_ID!}
userEmail=""
/>
<Component {...pageProps} />
</>
)
}Beginner guide
Page view tracking
The analytics component wires into `usePathname()` (App Router) or `router.events` (Pages Router). Use the hook snippets below when you need manual control.
Track a simple page view
tsx'use client'
import { useBentoAnalytics } from '@bentonow/bento-nextjs-sdk/analytics'
export function PageTracker({ userEmail }: { userEmail?: string }) {
useBentoAnalytics(userEmail)
return null
}Track a simple page view
tsximport { useBentoLegacyAnalytics } from '@bentonow/bento-nextjs-sdk/analytics/legacy'
export function PageTracker({ userEmail }: { userEmail?: string }) {
useBentoLegacyAnalytics(userEmail)
return null
}User identification
Pass the signed-in user’s email to connect devices and sessions. Use the dynamic pattern inside layouts or call `window.bento.identify()` manually when auth state changes.
Dynamic User Identification
tsximport { useUser } from '@/hooks/useUser' // Your auth hook
import { BentoAnalytics } from '@bentonow/bento-nextjs-sdk/analytics'
export default function Layout({ children }) {
const { user } = useUser()
return (
<html>
<body>
<BentoAnalytics
siteUuid={process.env.NEXT_PUBLIC_BENTO_SITE_ID!}
userEmail={user?.email || ''}
/>
{children}
</body>
</html>
)
}Manual User Identification
tsx// Identify user after login
useEffect(() => {
if (user?.email && window.bento) {
window.bento.identify(user.email)
}
}, [user])Event tracking
Track button clicks, onboarding milestones, or anything else with the client helper. The provided React hook ensures events are debounced and SSR-safe.
Basic Event Tracking
tsxconst handleSignup = () => {
window.bento?.track('signup', {
source: 'landing_page',
plan: 'free'
})
}
const handlePurchase = () => {
window.bento?.track('purchase', {
product_id: 'pro_plan',
amount: 99,
currency: 'USD'
})
}React Hook for Event Tracking
tsximport { useCallback } from 'react'
export function useBentoTracking() {
const track = useCallback((event: string, data?: Record<string, any>) => {
if (typeof window !== 'undefined' && window.bento) {
window.bento.track(event, data)
}
}, [])
const tag = useCallback((tagName: string) => {
if (typeof window !== 'undefined' && window.bento) {
window.bento.tag(tagName)
}
}, [])
return { track, tag }
}Usage
tsximport { useBentoTracking } from '@/hooks/useBentoTracking'
export function SignupForm() {
const { track } = useBentoTracking()
const handleSubmit = (formData) => {
track('form_submission', {
form_type: 'signup',
utm_source: formData.utm_source
})
}
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
</form>
)
}Intermediate guide
Server-side integration
Use the Node SDK inside API routes, server actions, or edge functions when you need to mutate subscribers or track secure events.
Server-Side Configuration
tsximport { Analytics } from '@bentonow/bento-node-sdk'
export const bento = new Analytics({
siteUuid: process.env.NEXT_PUBLIC_BENTO_SITE_ID!,
publishableKey: process.env.BENTO_PUBLISHABLE_KEY!,
secretKey: process.env.BENTO_SECRET_KEY!
})API Route Examples App Router
tsximport { bento } from '@/lib/bento'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
try {
const { email, event, data } = await request.json()
await bento.V1.track({
email,
type: event,
fields: data
})
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json({ error: 'Tracking failed' }, { status: 500 })
}
}API Route Examples Pages Router
tsximport { bento } from '@/lib/bento'
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
const { email, firstName, lastName } = req.body
await bento.V1.importSubscribers({
subscribers: [{
email,
firstName,
lastName,
tags: ['newsletter_signup']
}]
})
res.status(200).json({ success: true })
} catch (error) {
res.status(500).json({ error: 'Subscription failed' })
}
}Advanced configuration
Control how and when the Bento script loads. Lazy-load it, target specific segments, or skip it entirely in dev and staging builds.
Custom Script Loading
tsx'use client'
import Script from 'next/script'
import { useEffect, useState } from 'react'
interface BentoScriptProps {
siteUuid: string
userEmail?: string
onLoad?: () => void
}
export function BentoScript({ siteUuid, userEmail, onLoad }: BentoScriptProps) {
const [isLoaded, setIsLoaded] = useState(false)
useEffect(() => {
if (isLoaded && userEmail && window.bento) {
window.bento.identify(userEmail)
}
}, [isLoaded, userEmail])
return (
<Script
src={`https://fast.bentonow.com?site_uuid=${siteUuid}`}
strategy="afterInteractive"
onLoad={() => {
setIsLoaded(true)
onLoad?.()
}}
/>
)
}Conditional Loading for Segments
tsxexport function shouldLoadBento(): boolean {
// Don't load in development
if (process.env.NODE_ENV !== 'production') return false
// Don't load for admin users
const isAdmin = /* your admin check */
if (isAdmin) return false
return true
}Conditional Load when in Production
tsximport { shouldLoadBento } from '@/lib/shouldLoadBento'
export default function Layout({ children }) {
return (
<html>
<body>
{shouldLoadBento() && (
<BentoAnalytics siteUuid={process.env.NEXT_PUBLIC_BENTO_SITE_ID!} />
)}
{children}
</body>
</html>
)
}TypeScript support
Import the provided types to strongly type events and payloads. Typed helpers keep analytics predictable across large codebases.
Type Definitions
tsxdeclare global {
interface Window {
bento?: {
view: () => void
identify: (email: string) => void
track: (event: string, data?: Record<string, any>) => void
tag: (tag: string) => void
updateFields: (fields: Record<string, any>) => void
showChat: () => void
hideChat: () => void
openChat: () => void
closeChat: () => void
getEmail: () => string | undefined
spamCheck: (email: string) => Promise<boolean>
}
}
}Usage
tsximport { useCallback } from 'react'
import type { BentoEvent } from '@/types/events'
export function useTypedBentoTracking() {
const track = useCallback((event: BentoEvent) => {
if (typeof window !== 'undefined' && window.bento) {
const { type, ...data } = event
window.bento.track(type, data)
}
}, [])
return { track }
}Advanced guide
Custom events
Model ecommerce funnels, onboarding journeys, or feature adoption with bespoke events so automations can react instantly.
Get all existing tags
tsxconst trackPurchase = (orderData: {
orderId: string
items: Array<{
productId: string
name: string
price: number
quantity: number
}>
total: number
email: string
}) => {
window.bento?.track('purchase', {
unique: orderData.orderId, // Prevent counting orders multiple times
value: {
amount: orderData.total,
currency: 'USD'
},
details: {
items: orderData.items,
order_id: orderData.orderId
}
})
}Track user progression
tsxconst trackOnboarding = (step: string, completed: boolean) => {
window.bento?.track('onboarding_step', {
step_name: step,
completed,
timestamp: new Date().toISOString()
})
}Track feature usage
tsxconst trackFeatureUsage = (feature: string, context?: Record<string, any>) => {
window.bento?.track('feature_used', {
feature_name: feature,
...context
})
}Best practices
- Lazy-load analytics when possible to keep TTI fast.
- Batch similar events when you expect high frequency interactions.
- Debounce handlers so scroll or drag events don’t spam Bento.
Debounced Tracking
tsximport { debounce } from 'lodash'
const debouncedTrack = debounce((event: string, data: any) => {
window.bento?.track(event, data)
}, 300)
export { debouncedTrack }Error Handling
tsxexport function safeTrack(event: string, data?: Record<string, any>) {
try {
if (typeof window !== 'undefined' && window.bento) {
window.bento.track(event, data)
}
} catch (error) {
console.warn('Bento tracking failed:', error)
// Optional: Send to error monitoring service
}
}API reference
| Method | Description | Example |
|---|---|---|
window.bento.view() | Track a page view manually. | window.bento.view() |
window.bento.identify(email) | Identify a visitor. | window.bento.identify("user@example.com") |
window.bento.track(event, data) | Track a custom event. | window.bento.track("signup", { plan: "pro" }) |
window.bento.tag(tag) | Apply a tag. | window.bento.tag("premium_user") |
| Component | Props | Description |
|---|---|---|
| BentoAnalytics | siteUuid, userEmail? | App Router analytics component. |
| BentoLegacyAnalytics | siteUuid, userEmail? | Pages Router analytics component. |
| Hook | Parameters | Returns | Description |
|---|---|---|---|
| useBentoAnalytics | userEmail? | void | App Router helper. |
| useBentoLegacyAnalytics | userEmail? | void | Pages Router helper. |
Troubleshooting
| Issue | Solution |
|---|---|
| Script not loading | Confirm `NEXT_PUBLIC_BENTO_SITE_ID` is set and available in the browser. |
| Events not tracking | Guard on `window.bento` before calling methods and ensure analytics is mounted once. |
| TypeScript errors | Import types from `@bentonow/bento-nextjs-sdk/types`. |
| Hydration mismatches | Wrap analytics in a dynamic import with `ssr: false` where needed. |
Debugging
tsxif (typeof window !== 'undefined') {
window.addEventListener('load', () => {
console.log('Bento loaded:', !!window.bento)
if (window.bento) {
console.log('Current user:', window.bento.getEmail())
}
})
}FAQ
Can I use the App Router and Pages Router simultaneously?
Pick the component that matches your routing system—`BentoAnalytics` for App Router, `BentoLegacyAnalytics` for Pages Router.
How should I handle logout?
Call `window.bento.identify('')` or pass an empty string to the component’s `userEmail` prop to clear context.
Is the SDK compatible with React Strict Mode?
Yes. Event handlers are debounced so interactions don’t double fire during dev-only renders.
Can I customize the script loading strategy?
Absolutely. Wrap Bento in the Next.js `Script` component and choose `lazyOnload`, `afterInteractive`, or even `beforeInteractive` when you need earliest tracking.
How do I track events from server components?
Server components can’t call client-side APIs directly. Send data to a client component or trigger the Node SDK from a server action.
Server Component
tsxasync function ServerComponent() {
const data = await fetchData()
return <ClientTracker data={data} />
}Client Component
tsx'use client'
function ClientTracker({ data }) {
useEffect(() => {
window.bento?.track('data_loaded', { count: data.length })
}, [data])
return null
}Need the original Markdown? Open raw file