USE CASE
Next.js AI support chat widget
Add Deacon to a Next.js app with next/script in your root layout. It answers visitors from your docs and says so when it doesn’t know.
Published
Deacon is an AI support chat that goes into a Next.js app as one next/script element in the root layout. It replies within seconds from your docs and pages. A question they don’t cover gets an honest “I don’t know” and a box for the person’s email, or, once your app passes Deacon the logged-in user, a reply to them in one tap. When they leave an address that way, you hear about it by email, unless you’ve already seen it in Deacon’s dashboard, and your reply from there shows up in their chat. On the free plan that’s 50 answers a month, with no card and no end date.
We read the Next.js documentation on 25 October 2026, when 16.4.3 was the current release. There’s a Markdown copy of this page for coding agents.
What you need first
You need a Deacon account, which is free, and its key. Sign up, then copy the pk_ value of data-key from the snippet on the Install page. The app’s domain has to be on Install’s list as well, and the site you gave at sign-up is there already, subdomains and www included. Nothing comes from npm. next/script ships with Next.js.
When your home page is a Next.js build, Install says it “looks like it’s built with Next.js” and that your coding agent can add it for you. Its Copy the prompt hands Claude Code, Cursor or Codex the tag and your key, and the agent places it and checks it’s live.
Which router does your app use?
An app folder, or src/app, means the App Router. A pages folder means the Pages Router. Some apps have both while they move across, and then Deacon goes in both files, because the root layout only wraps app routes. Deacon ignores a second copy of its script, so nothing loads twice.
The root layout, in the App Router
Add two lines to app/layout.tsx, the import at the top and the <Script> element after {children}. Keep everything else your layout already has, such as fonts, metadata and providers.
// 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://heydeacon.com/widget/v1/loader.js"
data-key="pk_your_widget_key"
strategy="afterInteractive"
/>
</body>
</html>
);
}
In a JavaScript project the file is app/layout.js, and the function takes ({ children }) without the type.
Three things in Next.js’s guide to scripts make this work. A script in the root layout loads for every route. Next.js “will ensure the script will only load once, even if a user navigates between multiple pages”. And attributes the component doesn’t use itself, such as data-key, are forwarded to the final script tag. afterInteractive is already the default, and the Script reference says it loads “early but after some hydration on the page occurs”.
The Pages Router
A Pages Router app has no root layout, so the same element goes in pages/_app.tsx, after the page component, as the Pages Router guide shows. Again, the import and the <Script> are the new lines.
// pages/_app.tsx
import type { AppProps } from 'next/app';
import Script from 'next/script';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<Script
src="https://heydeacon.com/widget/v1/loader.js"
data-key="pk_your_widget_key"
strategy="afterInteractive"
/>
</>
);
}
In pages/_app.js, leave out the import type line and : AppProps.
Seeing it on localhost
- Run
npm run devand openhttp://localhost:3000. Nothing shows the first time, because localhost isn’t listed yet. Open Install from the top of Deacon’s dashboard, and within a few seconds it reads “Found the code on localhost:3000”, and one click adds it. - Reload. The chat button sits in the corner, labelled Help unless Appearance says otherwise.
- Ask it something your docs cover. Chats from localhost never count towards your plan.
- Optionally stop
npm run dev, then runnpm run buildandnpm run start, which serves the production build on the same port, so the same entry covers it.
After you deploy, Install lists the domain as Live with the time Deacon was last seen there. A preview deploy runs on an address of its own, and Deacon stays hidden there until that address is on the list too. On the live site, each client-side route change counts as a page view, and an open chat stays open.
An agent without a browser can ask Deacon instead. The answer’s latest is the newest address to load the script, and refused a development address that wasn’t on the list. Deacon’s install guide walks through it.
curl -s "https://heydeacon.com/api/widget/seen?k=pk_your_widget_key"
Try Deacon for free
Install DeaconPassing the logged-in user from your session
In an app people log in to, pass Deacon the logged-in user’s id, email and name. Their name then shows on the conversation, and when Deacon can’t answer, they reply with one tap instead of typing an address. These details only label the chat. Deacon doesn’t verify them and doesn’t show them to the AI.
The root layout is a server component, so it can read your session and print the details above the script. It prints null when nobody is logged in, as on the page a log-out lands on, which starts the next person on that computer fresh.
// app/layout.tsx
import Script from 'next/script';
import { auth } from '@/auth'; // or however your app reads its session
const safe = (value: unknown) => JSON.stringify(value).replace(/</g, '\\u003c');
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const session = await auth();
const user = session?.user
? { id: session.user.id, email: session.user.email, name: session.user.name }
: null;
return (
<html lang="en">
<body>
{children}
<script dangerouslySetInnerHTML={{ __html: `window.deaconUser = ${safe(user)};` }} />
<Script
src="https://heydeacon.com/widget/v1/loader.js"
data-key="pk_your_widget_key"
strategy="afterInteractive"
/>
</body>
</html>
);
}
JSON.stringify alone leaves </script> as it is, so a name containing it would end the inline script early. The .replace turns every < into \u003c, which the browser reads back as the same character.
Logins that happen without a page load
If someone can log in or out while the root layout stays on screen, call Deacon from the code that notices, in a client component.
// wherever your app hears a log-in or a log-out
// Auth.js types email and name as nullable, and Deacon takes a string or nothing
window.deaconUser = { id: user.id, email: user.email ?? undefined, name: user.name ?? undefined };
window.Deacon?.identify(window.deaconUser);
// on log-out, so the next person on this computer starts fresh
window.deaconUser = null;
window.Deacon?.identify(null);
declare global {
interface Window {
deaconUser?: { id?: string | number; email?: string; name?: string } | null;
Deacon?: {
identify(user: { id?: string | number; email?: string; name?: string } | null): void;
open(): void;
};
}
}
Setting window.deaconUser covers the case where this runs before Deacon’s script, and the optional call covers the case where it runs after. Deacon’s install guide shows how to pass a user’s plan too. Install’s “Do your users log in?” card confirms once it has seen a logged-in page, and a test on your own machine counts.
What visitors get, and what you see
People who would have opened a ticket get an answer on the page they’re on. When someone asks for a person, Deacon tells them the team has been told, and your reply arrives in that same chat under your name.
On your side, questions Deacon couldn’t answer queue up with an Answer this button, and Topics counts how many people asked about each thing, with bug reports and feature asks picked out. So you learn what people are stuck on, even the ones who never write to you.
Next.js questions
The chat button never shows. Where do I look?
In the browser console, Deacon explains itself in a line starting [Deacon]. “Unknown widget key” means data-key doesn’t match Install’s snippet. “isn’t in your allowed domains” means this address, often a Vercel preview, isn’t on the list. No [Deacon] line means the script never ran, unless the button is hidden in Appearance, so check the Network tab for loader.js, and that the <Script> is in a layout this route uses. A Content Security Policy is the other usual cause. One that blocks the script shows as the browser’s own error naming heydeacon.com, and one that blocks only the chat frame leaves the button in place but the chat blank when opened, with a [Deacon] line saying the frame “did not load”. script-src, connect-src and frame-src each need https://heydeacon.com beside 'self', and style-src needs 'unsafe-inline' for the styles the script writes into its own part of the page, which a nonce can’t reach.
Next.js suggests lazyOnload for chat plugins. Why afterInteractive?
The Script reference does list chat support plugins under lazyOnload, which waits for the browser to be idle. Deacon works that way too. But the same script records your site’s JavaScript errors, and it can’t record one thrown before it has loaded, so afterInteractive starts it sooner.
Try it on your own documentation
Add your help pages, ask it the question you know your docs cannot answer, and watch it say so.
The free plan covers 50 answers a month on one seat, and the paid plans are on the pricing page.
Free plan, no card.
READ NEXT
AI customer support for SaaS without a support team
Which questions AI should answer at a small SaaS, which must still reach you, and how to set one up on your site and inside your signed-in app.
How to add a chatbot to your website in 4 steps
How to add a chatbot to your website in four steps, where its script tag goes on each builder and framework, and how to tell it who’s logged in.
What to do in the week after launching a website
The checks to run before you post the link, the four numbers worth watching on launch day and what to do with the questions your first visitors send.