Skip to main content
Engineering

How to Add an AI Chatbot to a Next.js App (App Router and Pages Router)

Cortexvia Engineering14 Jun 20264 min read

Next.js adds two wrinkles a plain React integration does not have: components render on the server by default, and next/script wants to manage script loading for you.

Get either wrong and you hit hydration errors or a widget that never appears. Here is the correct setup for both routers.

The two rules

1. The widget is browser-only. It touches window and the DOM. It cannot render on the server, so anything that mounts it must be a client component.

2. Let next/script load the SDK. Do not hand-inject a <script> tag. Next deduplicates, handles the loading strategy, and will not re-request on navigation.

App Router

Two pieces: a script tag in the root layout, and a client component that mounts.

app/layout.tsx, a server component, which is fine because next/script works from one:

tsx
import Script from "next/script";
import Chatbot from "./chatbot";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link
          rel="stylesheet"
          href="https://cdn.cortexvia.in/chatbotsdk-web.css"
        />
      </head>
      <body>
        {children}
        <Script
          src="https://cdn.cortexvia.in/chatbotsdk-web.iife.js"
          strategy="afterInteractive"
        />
        <Chatbot />
      </body>
    </html>
  );
}

app/chatbot.tsx, the client component:

tsx
"use client";

import { useEffect, useRef } from "react";

export default function Chatbot() {
  const containerRef = useRef<HTMLDivElement>(null);
  const handleRef = useRef<any>(null);

  useEffect(() => {
    let cancelled = false;

    function tryMount() {
      if (cancelled || handleRef.current) return;
      const sdk = (window as any).ChatbotSDK;
      if (!sdk?.mount || !containerRef.current) return false;

      handleRef.current = sdk.mount(containerRef.current, {
        chatbotKey: "ctx-cb-xxxxxxxxxxxxxxxx",
        config: { mode: "chatbot" },
      });
      return true;
    }

    if (!tryMount()) {
      const id = setInterval(() => {
        if (tryMount()) clearInterval(id);
      }, 200);
      return () => {
        cancelled = true;
        clearInterval(id);
        handleRef.current?.unmount();
        handleRef.current = null;
      };
    }

    return () => {
      cancelled = true;
      handleRef.current?.unmount();
      handleRef.current = null;
    };
  }, []);

  return <div ref={containerRef} />;
}

The polling exists because next/script with afterInteractive does not guarantee the SDK is on window by the time your effect runs. If you prefer, use onReady on the <Script> tag to flip a state flag instead; same result, more wiring.

Pages Router

Same idea, in pages/_app.tsx:

tsx
import Script from "next/script";
import Head from "next/head";
import Chatbot from "../components/chatbot";

export default function App({ Component, pageProps }) {
  return (
    <>
      <Head>
        <link
          rel="stylesheet"
          href="https://cdn.cortexvia.in/chatbotsdk-web.css"
        />
      </Head>
      <Component {...pageProps} />
      <Script
        src="https://cdn.cortexvia.in/chatbotsdk-web.iife.js"
        strategy="afterInteractive"
      />
      <Chatbot />
    </>
  );
}

The Chatbot component is identical, with no "use client" needed, since the Pages Router has no server components.

Which strategy?

next/script gives you four. For a chatbot:

afterInteractive is the right default. Loads once the page is interactive, so it does not compete with your app's own JavaScript.

lazyOnload loads during idle time. Lighter still, but the launcher appears noticeably late. Fine for low-traffic support surfaces.

beforeInteractive: do not. It blocks hydration for a support widget and actively hurts your Core Web Vitals for no benefit.

worker is experimental, and the widget needs DOM access. It will not work.

Why put it in the layout, not a page

Mount once at the layout level, not per page.

In the App Router, layouts persist across navigation while pages re-render. Put the chatbot in a page and every route change tears the widget down and rebuilds it: the conversation is lost and the launcher visibly flickers. In the layout, it survives navigation.

Avoiding hydration errors

The classic Next.js mistake is rendering something server-side that only exists client-side. Two rules keep you clear.

Never read window during render. Only inside useEffect. The code above follows this.

Render the same empty container on both sides. <div ref={containerRef} /> is identical on server and client; the widget only appears afterwards, in an effect. No mismatch.

If you see "Text content did not match", something in your component is reading browser state during render.

Inline mode needs a height

For an inline panel rather than a floating launcher:

tsx
<div ref={containerRef} style={{ height: "72vh" }} />

Inline mode fills its container. A container with no height renders as nothing, and you will think the integration failed when it is a CSS problem. This is the mode to use for a docs page or an AI search panel.

Checklist before shipping

  • Navigate between routes several times: exactly one launcher.
  • Network tab: SDK files load once, not per navigation.
  • No hydration warnings in the console.
  • The widget appears on a hard refresh of a deep route, not just from the home page.
  • Inline mode containers have a height.

Frequently asked questions

Does this work with Turbopack and React Server Components? Yes. The layout is a server component and only the Chatbot client component touches the browser. No server code imports the SDK.

Can I load the chatbot only on some routes? Render <Chatbot /> from a nested layout instead of the root layout. It mounts when a route inside that layout is active and unmounts cleanly when you leave.

Where do I get a key to test with? Create a chatbot on the free plan; the key appears in the Chatbots area once the first source has processed. The Next.js guide page has copy-paste versions of both routers.

Ready to Get Started?

Build your chatbot in minutes with Cortexvia. Free tier available.