Next.js Integration
Integrate Ozwell into your Next.js application with proper handling for SSR and the App Router.
The examples explicitly set widgetUrl="https://api.ozwell.ai/widget/frame/".
This selects the production loader, iframe, and default API endpoint. The current
React wrapper still defaults to the development host when this prop is omitted.
| Environment | widgetUrl |
|---|---|
| Production | https://api.ozwell.ai/widget/frame/ |
| Development | https://ozwellapi.os.mieweb.org/widget/frame/ |
| Local reference server | http://localhost:3000/widget/frame/ |
The proposed page-control option
is a future plain-loader OzwellChatConfig setting. It is not implemented and is
not a React prop. Current page interactions require explicit tools and
onToolCall handlers; see React tool handling.
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
widgetUrl="https://api.ozwell.ai/widget/frame/"
apiKey={process.env.NEXT_PUBLIC_OZWELL_AGENT_KEY!}
/>
);
}
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
widgetUrl="https://api.ozwell.ai/widget/frame/"
apiKey={process.env.NEXT_PUBLIC_OZWELL_AGENT_KEY!}
/>
</>
);
}
Environment Variablesβ
For an agent-configured widget, create a .env.local file with an agent key from
the selected deployment:
NEXT_PUBLIC_OZWELL_AGENT_KEY=agnt_key-your-agent-key
NEXT_PUBLIC_ values are exposed to the browser. Use only an agent key approved
for that site. The key selects the agent; agentId is reserved and currently
ignored. Omit apiKey when using visitor sign-in on a deployment that enables it.
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. If you need to defer configuration until the component mounts:
'use client';
import { OzwellChat } from '@ozwell/react';
import { useState, useEffect } from 'react';
export function OzwellWidget() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<OzwellChat
widgetUrl="https://api.ozwell.ai/widget/frame/"
apiKey={process.env.NEXT_PUBLIC_OZWELL_AGENT_KEY!}
/>
);
}
Examplesβ
With Authentication Contextβ
The React wrapper does not implement a context prop. Expose only the required
session or page data through an explicit read-only tool and onToolCall handler;
see React tool handling.
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': 'agnt_key-docs-agent-key',
'/support': 'agnt_key-support-agent-key',
default: 'agnt_key-general-agent-key',
};
export function OzwellWidget() {
const pathname = usePathname();
const apiKey = Object.entries(AGENTS).find(
([path]) => pathname.startsWith(path)
)?.[1] ?? AGENTS.default;
return (
<OzwellChat
widgetUrl="https://api.ozwell.ai/widget/frame/"
apiKey={apiKey}
/>
);
}
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
widgetUrl="https://api.ozwell.ai/widget/frame/"
apiKey={process.env.NEXT_PUBLIC_OZWELL_AGENT_KEY!}
/>
);
}
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
widgetUrl="https://api.ozwell.ai/widget/frame/"
apiKey={process.env.NEXT_PUBLIC_OZWELL_AGENT_KEY!}
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://api.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
widgetUrl="https://api.ozwell.ai/widget/frame/"
apiKey={process.env.NEXT_PUBLIC_OZWELL_AGENT_KEY!}
{...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' https://api.ozwell.ai 'unsafe-eval' 'unsafe-inline';
frame-src 'self' https://api.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