Most “nextjs chatbot” results are templates for building your own chat app, such as the Vercel AI chatbot template. Use one if the chat is your product. If you need a support or sales assistant on an existing Next.js site, embedding is faster. Here’s how to do it with Feedbot (we make it).
What you need
- A Feedbot bot and its bot id from Dashboard → Bot → Install. The Free plan is enough to test.
- Next.js 13 or later. Both routers are covered below.
App Router: next/script
Add the script to the root layout:
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <Script src="https://w.feedbotai.com/v1/feedbot.js" data-bot-id="YOUR_BOT_ID" strategy="lazyOnload" /> </body> </html> );}Which strategy to use, per the next/script reference:
| Strategy | When it loads | Use for the chatbot? |
|---|---|---|
lazyOnload |
Browser idle time, after all page resources | Yes. Recommended. |
afterInteractive (default) |
After some hydration | Fine if you want the button sooner. |
beforeInteractive |
Before any Next.js code | No. Meant for critical scripts like consent managers. |
worker |
In a web worker (experimental) | No. Doesn’t work with the App Router. |
The loader itself is about 6 KB gzipped and fetches the chat only on hover, click or idle, so lazyOnload costs nothing noticeable.
App Router: @feedbotai/react
The package wraps the same loader in a component and adds a hook.
pnpm add @feedbotai/reactimport { FeedbotWidget } from "@feedbotai/react";
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <FeedbotWidget botId={process.env.NEXT_PUBLIC_FEEDBOT_BOT_ID!} /> </body> </html> );}FeedbotWidget is a client component, so it’s safe to render from this server component. To open the chat from your own button, use the hook in a client component:
"use client";import { useFeedbot } from "@feedbotai/react";
export function HelpButton() { const { sendMessage } = useFeedbot(); return <button onClick={() => sendMessage("How do I upgrade?")}>Ask about upgrading</button>;}Pages Router
Put the script in pages/_app.tsx so it loads on every page:
import type { AppProps } from "next/app";import Script from "next/script";
export default function App({ Component, pageProps }: AppProps) { return ( <> <Component {...pageProps} /> <Script src="https://w.feedbotai.com/v1/feedbot.js" data-bot-id="YOUR_BOT_ID" strategy="lazyOnload" /> </> );}<FeedbotWidget> works in _app.tsx the same way.
Identify signed-in users with a server-side hash
- Copy the secret from Bot → Install → Identity verification into
FEEDBOT_IDENTITY_SECRET. NoNEXT_PUBLIC_prefix: it must stay on the server. - Compute the hash and pass it with the user:
import { createHmac } from "node:crypto";import { FeedbotWidget } from "@feedbotai/react";import { getCurrentUser } from "@/lib/auth"; // your auth
function feedbotHash(userId: string) { return createHmac("sha256", process.env.FEEDBOT_IDENTITY_SECRET!).update(userId).digest("hex");}
export default async function RootLayout({ children }: { children: React.ReactNode }) { const user = await getCurrentUser(); // null when logged out return ( <html lang="en"> <body> {children} <FeedbotWidget botId={process.env.NEXT_PUBLIC_FEEDBOT_BOT_ID!} user={user ? { id: user.id, email: user.email, name: user.name } : null} userHash={user ? feedbotHash(user.id) : undefined} metadata={user ? { plan: user.plan } : undefined} /> </body> </html> );}When user turns into null on logout, the component calls reset, so the next person on that device starts fresh.
One trade-off: reading the session (cookies) in the root layout makes every route dynamic. If you want marketing pages to stay static, put this in the layout of your signed-in area only (for example app/(app)/layout.tsx), or return { user, userHash } from a route handler and fetch it in a client component. In the Pages Router, compute the hash in getServerSideProps or an API route. Details in Identify users.
Show the chat on some pages only
Place the <Script> or <FeedbotWidget> in a nested layout instead of the root one, for example app/(marketing)/layout.tsx or app/(app)/layout.tsx. With lazyOnload or afterInteractive, the script loads only when a page under that layout is opened.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
No button, console says data-bot-id is required |
Env variable undefined in the browser | Client-side variables need the NEXT_PUBLIC_ prefix. Rebuild after changing it. |
| Chat button appears late | lazyOnload waits for idle time |
Expected. Switch to afterInteractive if you want it sooner. |
| Requests rejected on preview deployments | Preview domain not in Allowed domains | Add the preview hostname. Don’t add vercel.app itself: subdomains of a listed domain are allowed, so that would open your bot to every Vercel site. |
| Blocked by CSP | Strict Content-Security-Policy |
Allow https://w.feedbotai.com in script-src and https://api.feedbotai.com in connect-src. |
| User unverified | Hash of a different value, or old secret | Hash exactly the id you pass, with the current secret. Redeploy after Rotate. |
Train it on your docs
The Free plan learns from text and FAQ. From Starter, import your site or docs by URL or sitemap.xml (Next.js apps that render content client-side are read in a real browser) and upload files. See knowledge base.
Next steps
- Install docs for every option, including inline mode.
- JavaScript API for events and client-side actions.
- Plain React without Next.js? See the React chatbot guide. Using Nuxt? See Vue.