2026-09-20 · Updated 2026-09-10 · 10 min read · CanWeChat Engineering
Installing the CanWeChat widget in a Next.js app
Load the script once in the root layout, lock your domain, and call identify after auth. No React wrapper required.
Next.js apps already carry React on the client for your product UI. That does not mean your live chat vendor should ship a second React tree for a bubble. CanWeChat’s public widget is a vanilla IIFE with Shadow DOM. Install it like any third-party script—once—and call a small JS API after authentication.
This tutorial targets App Router and Pages Router maintainers who want a correct, domain-locked install without inventing a canwechat-react package.
Quick answer
- Prefer the data-site attribute with your public key (pk_live_…).
- Load the script once from app/layout.tsx (or _document) with afterInteractive.
- Lock allowed domains in CanWeChat Settings before going public.
- Call CanWeChat.identify after your own auth resolves.
- Use open, prefill, page, and destroy from the documented API—do not fork the widget.
Prerequisites
- A CanWeChat workspace (3-day free trial, then $19/month Pro).
- One website per workspace and a chosen reply destination.
- Your public site key from the install screen.
- Production and preview hostnames you will allowlist.
App Router install
Add the script in the root layout so every route gets the bubble without remounting a client provider. next/script with strategy="afterInteractive" is the usual choice.
// app/components/canwechat-widget.tsx
import Script from "next/script";
export function CanWeChatWidget() {
const siteKey = process.env.NEXT_PUBLIC_CANWECHAT_SITE_KEY;
if (!siteKey) return null;
return (
<Script
src="https://canwechat.dev/api/widget.js"
strategy="afterInteractive"
data-site={siteKey}
/>
);
}
// app/layout.tsx
import { CanWeChatWidget } from "./components/canwechat-widget";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<CanWeChatWidget />
</body>
</html>
);
}Pages Router install
On Pages Router, put the same Script in pages/_document.tsx or a top-level layout component that mounts once. Avoid dropping the script inside pages that unmount on client navigations if you can help it—reload thrash helps nobody.
// pages/_document.tsx
import { Html, Head, Main, NextScript } from "next/document";
import Script from "next/script";
export default function Document() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
<Script
src="https://canwechat.dev/api/widget.js"
strategy="afterInteractive"
data-site={process.env.NEXT_PUBLIC_CANWECHAT_SITE_KEY}
/>
</body>
</Html>
);
}Identify after auth
The loader exposes a stub so you can call methods before boot finishes; calls queue and replay. After your session is known, identify the visitor. Type the global lightly or use optional chaining.
"use client";
import { useEffect } from "react";
declare global {
interface Window {
canwechat?: {
identify: (input: {
userId: string;
email?: string;
name?: string;
}) => void;
open: () => void;
prefill: (text: string) => void;
page: (url?: string) => void;
destroy: () => void;
};
}
}
export function CanWeChatIdentify({
user,
}: {
user: { id: string; email?: string; name?: string } | null;
}) {
useEffect(() => {
if (!user) return;
window.canwechat?.identify({
userId: user.id,
email: user.email,
name: user.name,
});
}, [user]);
return null;
}Open from a pricing CTA
"use client";
export function AskSalesButton() {
return (
<button
type="button"
onClick={() => {
window.canwechat?.prefill("I have a question about Pro.");
window.canwechat?.open();
}}
>
Ask sales
</button>
);
}Lock domains before launch
Public keys are public. Domain allowlisting is what stops someone from hotlinking your bubble on a phishing clone. In Settings, add your production host and any stable preview hosts you truly need. Wildcard carefully. Re-test the widget after allowlist changes.
Environment variables and previews
- Store the public key in NEXT_PUBLIC_CANWECHAT_SITE_KEY—never put bot tokens or webhook secrets in NEXT_PUBLIC_*.
- Vercel preview URLs change often; either allow a documented preview pattern or disable the widget on ephemeral previews.
- Do not commit .env files with production secrets; the site key is public but still belongs in env for rotation hygiene.
CSP and performance notes
If you ship a strict Content-Security-Policy, allow the widget script origin explicitly. The runtime talks to CanWeChat APIs on the same product host family—extend connect-src accordingly when your policy defaults to deny. Load with async / afterInteractive so the bubble is not on the critical rendering path of your hero.
Because the widget uses Shadow DOM, your global Tailwind preflight will not restyle the chat panel. Brand colors and launcher copy still come from CanWeChat dashboard settings—keep marketing CSS focused on your site, not on fighting an embed.
API surface you should use
- identify — attach your user id/email/name after login.
- open / close / toggle / isOpen — drive the panel from CTAs.
- prefill — seed the composer from pricing or docs deep links.
- page — update page context on client-side route changes if you care about URL accuracy.
- destroy — rare; tear down on logout pages that must drop the bubble entirely.
- on / ready — subscribe to widget events without scraping the DOM.
Troubleshooting
- Bubble missing: script blocked by CSP—allow https://canwechat.dev in script-src.
- identify no-ops: calling before the script tag exists; keep the layout Script mounted.
- 403 / disabled site: domain not allowlisted or workspace trial/entitlement inactive.
- Styles look wrong on host page: unlikely with Shadow DOM—check you did not copy vendor CSS into globals.
- Duplicate bubbles: Script rendered in both layout and a page component.
Production checklist
- data-site set from env in root layout only.
- CSP updated.
- Allowed domains include production hostname.
- identify wired after auth.
- Reply destination chosen (inbox/Slack/Discord/Telegram).
- Test send on production; confirm agent reply reaches the widget.
- Optional: webhook relay verified with HMAC for CRM sync.
The CanWeChat angle
Your Next.js app can stay on React. Our dashboard does too. The visitor-facing file stays a small async script so marketing routes do not hydrate a chat SPA. Behind it: Workers, D1, Durable Objects, and queues. Pricing stays simple—3-day trial, then $19/month for one site and one active reply destination.
Limitations
- There is no official React provider; that is intentional.
- Server Components cannot call window.canwechat—use a client child for identify/open.
- We do not claim SOC 2 or a public SLA in this install guide; see /security for implementation boundaries.
- Heavy customization belongs in dashboard settings and the documented JS API, not a forked bundle.
When the checklist is green, you are done: one script, one key attribute, one place your team answers.
Next step
Continue with the most relevant CanWeChat guide for this topic.