Install live chat on Next.js
Use Next.js' next/script component to mount the message.com widget without breaking hydration. Patterns for App Router and Pages Router, plus how to push route changes and identify users on the client.
App Router (Next.js 13+)
Drop the snippet into app/layout.tsx. Use strategy="afterInteractive" so the bundle loads after hydration completes and never blocks Largest Contentful Paint.
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script id="message-widget" strategy="afterInteractive">
{`(function(d,s,o){
var j=d.createElement(s);j.async=1;j.src='https://app.message.com/widget.js';
j.dataset.workspace=o;
d.head.appendChild(j);
})(document,'script','YOUR_WORKSPACE_ID');`}
</Script>
</body>
</html>
);
}Do not paste raw <script> tags into JSX. React strips inline scripts during render. The next/script component is the correct primitive.
Pages Router
For projects still on the older Pages Router, mount the snippet from pages/_app.tsx.
// 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 id="message-widget" strategy="afterInteractive">
{`(function(d,s,o){
var j=d.createElement(s);j.async=1;j.src='https://app.message.com/widget.js';
j.dataset.workspace=o;
d.head.appendChild(j);
})(document,'script','YOUR_WORKSPACE_ID');`}
</Script>
</>
);
}Push route changes on SPA navigation
The widget bundle is route-agnostic and survives client-side navigation. You do not need to re-inject it. To keep pageUrl and route in sync (so agents see what page the visitor is currently on), call Message.update on every path change.
// app/components/MessageRouteSync.tsx
"use client";
import { usePathname } from "next/navigation";
import { useEffect } from "react";
declare global { interface Window { Message?: { update: (ctx: Record<string, unknown>) => void } } }
export default function MessageRouteSync() {
const pathname = usePathname();
useEffect(() => {
window.Message?.update({ pageUrl: window.location.href, route: pathname });
}, [pathname]);
return null;
}Mount <MessageRouteSync /> once in app/layout.tsx.
Identify the user
Compute the HMAC user-hash server-side (Route Handler or Server Action), pass it to the client component, then call Message.identify. See Identify logged-in users for the full HMAC pattern.
// app/components/MessageIdentify.tsx
"use client";
import { useEffect } from "react";
declare global {
interface Window {
Message?: { identify: (p: Record<string, unknown>) => void };
MessageOnReady?: () => void;
}
}
export default function MessageIdentify({ user }: { user: { email: string; name: string; id: string; userHash: string } }) {
useEffect(() => {
const identify = () => window.Message?.identify({
email: user.email,
name: user.name,
userId: user.id,
userHash: user.userHash,
});
if (window.Message) identify();
else window.MessageOnReady = identify;
}, [user]);
return null;
}Verify the install
- Run
npm run build && npm run start(ornext dev). - Open
localhost:3000in a private window. - The launcher appears in the bottom-right within two seconds.
- Check the browser console for the
[message]startup log. - Send a test message. The conversation lands in
app.message.com.
Common pitfalls
strategy="beforeInteractive". Avoid. It runs before hydration, which is unnecessary for a chat widget and hurts Core Web Vitals.- React Strict Mode double-fire. Strict Mode mounts components twice in development. The widget snippet itself guards against this; your identify code should too (check
window.Messagebefore calling). - Content Security Policy. If you ship a strict CSP, allowlist
app.message.comonscript-src,connect-src, andwss://app.message.comonconnect-src. See Widget installation for the full directive list. - Static export (
next export). Works. The widget is purely client-side, no server runtime required.