Next.js Integration
Integrate Ozwell into your Next.js application with proper handling for SSR and the App Router.
Installationβ
npm install @ozwell/react
# or
yarn add @ozwell/react
# or
pnpm add @ozwell/react
Quick Startβ
App Router (Next.js 13+)β
Create a client component for the chat widget:
// components/OzwellWidget.tsx
'use client';
import { OzwellChat } from '@ozwell/react';
export function OzwellWidget() {
return (
<OzwellChat
apiKey={process.env.NEXT_PUBLIC_OZWELL_API_KEY!}
agentId={process.env.NEXT_PUBLIC_OZWELL_AGENT_ID!}
/>
);
}
Add it to your root layout:
// app/layout.tsx
import { OzwellWidget } from '@/components/OzwellWidget';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<OzwellWidget />
</body>
</html>
);
}
Pages Routerβ
// pages/_app.tsx
import type { AppProps } from 'next/app';
import dynamic from 'next/dynamic';
// Dynamic import with SSR disabled
const OzwellChat = dynamic(
() => import('@ozwell/react').then((mod) => mod.OzwellChat),
{ ssr: false }
);
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<OzwellChat
apiKey={process.env.NEXT_PUBLIC_OZWELL_API_KEY!}
agentId={process.env.NEXT_PUBLIC_OZWELL_AGENT_ID!}
/>
</>
);
}
Environment Variablesβ
Create a .env.local file:
NEXT_PUBLIC_OZWELL_API_KEY=ozw_scoped_xxxxxxxx
NEXT_PUBLIC_OZWELL_AGENT_ID=agent_xxxxxxxx
β οΈ Only use
NEXT_PUBLIC_prefix for scoped API keys that are safe for client-side use.
Server-Side Considerationsβ
Why Client-Only?β
The Ozwell widget uses browser APIs (DOM, PostMessage) and must render client-side only. The 'use client' directive or dynamic(..., { ssr: false }) ensures this.
Hydration Safetyβ
The widget is rendered inside an iframe, so hydration mismatches are not a concern. However, if you're passing dynamic props:
'use client';
import { OzwellChat } from '@ozwell/react';
import { usePathname } from 'next/navigation';
import { useState, useEffect } from 'react';
export function OzwellWidget() {
const pathname = usePathname();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<OzwellChat
apiKey={process.env.NEXT_PUBLIC_OZWELL_API_KEY!}
agentId={process.env.NEXT_PUBLIC_OZWELL_AGENT_ID!}
context={{ page: pathname }}
/>
);
}
Examplesβ
With Authentication Contextβ
Pass user data from your auth provider:
// components/OzwellWidget.tsx
'use client';
import { OzwellChat } from '@ozwell/react';
import { useSession } from 'next-auth/react';
import { usePathname } from 'next/navigation';
export function OzwellWidget() {
const { data: session } = useSession();
const pathname = usePathname();
return (
<OzwellChat
apiKey={process.env.NEXT_PUBLIC_OZWELL_API_KEY!}
agentId={process.env.NEXT_PUBLIC_OZWELL_AGENT_ID!}
context={{
userId: session?.user?.id,
email: session?.user?.email,
page: pathname,
}}
/>
);
}
Route-Based Agent Selectionβ
Use different agents for different sections:
// components/OzwellWidget.tsx
'use client';
import { OzwellChat } from '@ozwell/react';
import { usePathname } from 'next/navigation';
const AGENTS = {
'/docs': 'agent_docs_xxxxxxxx',
'/support': 'agent_support_xxxxxxxx',
default: 'agent_general_xxxxxxxx',
};
export function OzwellWidget() {
const pathname = usePathname();
const agentId = Object.entries(AGENTS).find(
([path]) => pathname.startsWith(path)
)?.[1] ?? AGENTS.default;
return (
<OzwellChat
apiKey={process.env.NEXT_PUBLIC_OZWELL_API_KEY!}
agentId={agentId}
/>
);
}
Hide on Specific Routesβ
// components/OzwellWidget.tsx
'use client';
import { OzwellChat } from '@ozwell/react';
import { usePathname } from 'next/navigation';
const HIDDEN_ROUTES = ['/checkout', '/auth', '/admin'];
export function OzwellWidget() {
const pathname = usePathname();
const isHidden = HIDDEN_ROUTES.some(route =>
pathname.startsWith(route)
);
if (isHidden) return null;
return (
<OzwellChat
apiKey={process.env.NEXT_PUBLIC_OZWELL_API_KEY!}
agentId={process.env.NEXT_PUBLIC_OZWELL_AGENT_ID!}
/>
);
}
With Analytics (Vercel Analytics)β
Track chat lifecycle events (not contentβthat's private):
// components/OzwellWidget.tsx
'use client';
import { OzwellChat } from '@ozwell/react';
import { track } from '@vercel/analytics';
export function OzwellWidget() {
return (
<OzwellChat
apiKey={process.env.NEXT_PUBLIC_OZWELL_API_KEY!}
agentId={process.env.NEXT_PUBLIC_OZWELL_AGENT_ID!}
onOpen={() => track('chat_opened')}
onClose={() => track('chat_closed')}
onUserShare={(data) => {
// Only fires when user explicitly shares
track('user_shared_data', data);
}}
/>
);
}
Privacy Note: There is no
onMessagecallback. Conversation content is private between the user and Ozwell.
Middleware Considerationsβ
If you're using Next.js middleware for authentication, ensure the Ozwell embed domain is allowed:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Allow Ozwell iframe
response.headers.set(
'Content-Security-Policy',
"frame-src 'self' https://embed.ozwell.ai"
);
return response;
}
TypeScriptβ
import type { OzwellChatProps } from '@ozwell/react';
// Typed configuration
const ozwellConfig: Partial<OzwellChatProps> = {
theme: 'auto',
position: 'bottom-right',
primaryColor: '#4f46e5',
};
export function OzwellWidget() {
return (
<OzwellChat
apiKey={process.env.NEXT_PUBLIC_OZWELL_API_KEY!}
agentId={process.env.NEXT_PUBLIC_OZWELL_AGENT_ID!}
{...ozwellConfig}
/>
);
}
Troubleshootingβ
"Text content does not match server-rendered HTML"β
Ensure the component is client-only:
// β
App Router
'use client';
// β
Pages Router
const OzwellChat = dynamic(() => import('@ozwell/react'), { ssr: false });
Widget Not Appearing in Productionβ
- Verify environment variables are set in your deployment
- Check that
NEXT_PUBLIC_prefix is used - Rebuild after adding environment variables
CSP Errorsβ
Add Ozwell domains to your security headers in next.config.js:
// next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
frame-src 'self' https://embed.ozwell.ai;
connect-src 'self' https://api.ozwell.ai;
`.replace(/\s{2,}/g, ' ').trim()
}
];
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
];
},
};
Next Stepsβ
- React Integration β Core React documentation
- Iframe Details β Security deep-dive
- Backend API β Server-side integration