Nexoply

How-to guides

How to add an AI chatbot to a Next.js site (App Router and Pages Router)

Add an AI chatbot to a Next.js site with next/script in app/layout.tsx or pages/_app.tsx. No npm package, SSR-safe, works with client-side navigation.

Updated September 30, 2026 6 min read

In short

Create a free Nexoply account and copy your chatbot key from the Chatbot page of your dashboard. In the App Router, add <Script src="https://nexoply.com/widget.js" data-chatbot={key} strategy="afterInteractive" /> from next/script inside <body> in app/layout.tsx; in the Pages Router, put the same <Script> in pages/_app.tsx. There's no npm package to install: the widget runs only in the browser, keeps the conversation across client-side navigation and doesn't touch your markup, so there are no hydration issues.

  • No npm package: one next/script tag in your root layout (App Router) or _app (Pages Router).
  • Keep the key in NEXT_PUBLIC_NEXOPLY_KEY; it's public, not a secret, and gives no access to your account.
  • Use strategy="afterInteractive" (the default choice) or "lazyOnload" to load it after everything else.
  • SSR-safe: it runs only in the browser, mounts in a Shadow DOM on document.body and survives route changes.
  • Open the chat from your own button with window.dispatchEvent(new Event("nexoply:open-chat")).

Before you start

  • A Nexoply account. Sign up free: the Free plan includes 100 conversations a month, with no card needed.
  • The business's information in Nexoply: services and prices, FAQs, policies, opening hours and service areas. The quickest way is to enter the website address and let Nexoply import the public pages (including pages rendered with JavaScript); you review the suggestions and nothing is added until you approve it.
  • Your chatbot key. In the dashboard, open Chatbot (click Create chatbot if you haven't yet). The install code there contains the key in its data-chatbot attribute.

For a plain HTML site, the install code would simply be pasted before </body>:

<script src="https://nexoply.com/widget.js" data-chatbot="YOUR-KEY" async></script>

In Next.js you don't edit HTML directly, so you add the same script with the built-in next/script component instead. The key is public: it only tells the widget which business it belongs to.

App Router: add the script to app/layout.tsx

The root layout wraps every page, so adding the script there puts the chat on the whole site. Import Script from next/script and place it inside <body>, after {children}:

// app/layout.tsx
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        {/* Nexoply chat widget: loads in the browser after hydration */}
        <Script
          src="https://nexoply.com/widget.js"
          data-chatbot={process.env.NEXT_PUBLIC_NEXOPLY_KEY}
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

Then put the key in an environment variable. The NEXT_PUBLIC_ prefix makes Next.js include it in the browser bundle, which is fine here because the key isn't a secret:

# .env.local (not committed to git)
NEXT_PUBLIC_NEXOPLY_KEY=YOUR-KEY

Pages Router: add the script to pages/_app.tsx

On the Pages Router, pages/_app.tsx wraps every page, so the same <Script> goes there, next to <Component {...pageProps} />. Use the same .env.local variable:

// 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://nexoply.com/widget.js"
        data-chatbot={process.env.NEXT_PUBLIC_NEXOPLY_KEY}
        strategy="afterInteractive"
      />
    </>
  );
}

If your project mixes both routers, add the script in both app/layout.tsx and pages/_app.tsx. Loading it twice is harmless: the widget mounts only once per chatbot key.

Choosing a strategy, and how it behaves

strategy="afterInteractive" loads the widget right after the page becomes interactive, so the chat button appears quickly. strategy="lazyOnload" waits until the browser is idle and everything else has loaded; choose it if you want to keep the first load as light as possible and don't mind the button appearing a moment later. Both keep the script off the critical rendering path. See the next/script documentation for details.

  • Client-side navigation. The widget keeps the conversation open while visitors move between pages with <Link> or the router, and re-checks your page rules when the URL changes, with no extra code.
  • SSR and hydration safe. It runs only in the browser and mounts on document.body, outside the markup React hydrates, so it never causes hydration mismatches.
  • Shadow DOM. Your Tailwind or global CSS doesn't leak into the chat, and its styles don't leak into your app. Re-renders never remove it.
  • StrictMode and Fast Refresh. Effects running twice in development or hot reloads can't create a second chat: it mounts once per key.
  • Storage. It sets no cookies; it uses localStorage for an anonymous visitor id and sessionStorage for the current conversation.

Open the chat from your own button

Want a "Chat with us" button in your hero or pricing section? Dispatch the nexoply:open-chat event on window. Because it uses onClick, it must be a client component:

"use client";
// components/ChatButton.tsx

export function ChatButton() {
  return (
    <button
      type="button"
      // opens the Nexoply chat window
      onClick={() => window.dispatchEvent(new Event("nexoply:open-chat"))}
    >
      Chat with us
    </button>
  );
}

Use <ChatButton /> in any page or server component. There's no API to remove the widget once loaded: to hide the message button on some pages (for example /checkout), add a page rule with "No button" under Chatbot → Customize → Message button → Different on some pages.

Test locally, allowed websites and CSP

Run next dev, open http://localhost:3000 and a chat button appears in the corner. Ask a real question, such as "Do you work on weekends?"; within seconds the conversation shows up under Conversations in your dashboard. Changes you make in the dashboard reach the site within about a minute, without a redeploy.

If your app sends a Content-Security-Policy header (for example from middleware or next.config), allow Nexoply in:

  • script-src https://nexoply.com: to load widget.js.
  • connect-src https://nexoply.com: for the chat's requests.
  • img-src https://nexoply.com: for images such as the logo and button icon.

Troubleshooting and next steps

  • No button, and data-chatbot is empty in the page source. The env var wasn't read: check the NEXT_PUBLIC_ prefix, restart next dev, and on your host add the variable and redeploy.
  • The script is in a single page instead of the layout. Put it in app/layout.tsx or pages/_app.tsx so it loads on every route.
  • The tag was changed. It must keep the data-chatbot attribute and a src containing widget.js.
  • Blocked by CSP or allowed websites. Look for errors in the browser console, and check the two sections above.
  • Chatbot switched off. Check that the Chatbot enabled switch is on.
  • Ad blockers. Rarely, an extension blocks chat widgets. Try a private window without extensions.

Once it works, make it fit the site under Chatbot → Customize: button icon, label, colours and position, or a side tab on the edge of the screen (such as "Help"); a different look on phones; and page rules for chosen pages (for example a side tab only on /pricing). See features and how it works, compare pricing, or read the React and Vue guides.

Frequently asked questions

Is there a Nexoply npm package for Next.js?

No, and you don't need one. The widget is always the script https://nexoply.com/widget.js with a data-chatbot attribute, added with next/script. All settings live in your Nexoply dashboard.

Does it work on Vercel?

Yes. It's just a script tag loaded in the visitor's browser: nothing runs on your server, and there are no API routes or server settings to add. Set NEXT_PUBLIC_NEXOPLY_KEY in your Vercel project's environment variables and redeploy.

Does it affect Core Web Vitals or performance?

It's designed not to. The script loads asynchronously after hydration (afterInteractive) or when the browser is idle (lazyOnload), so it never blocks rendering, and it mounts outside your page's layout.

App Router or Pages Router: which should I use?

Whichever your project already uses; the widget works the same with both. App Router: app/layout.tsx. Pages Router: pages/_app.tsx.

Can I load it only after cookie consent?

Yes. Render the <Script> only once the visitor has agreed, for example in a client component that checks your consent state. The widget itself sets no cookies.

Let your website answer the questions for you

Add your business info, test your assistant, and put it on your website - all on the free plan.