React and Next.js
Mount the public loader once in a client-owned React or Next.js layout and clean it up deliberately.
Mount the public loader once in a client-owned React or Next.js layout and clean it up deliberately.
Prepare origins and scope#
Publish the site, enable embedding, and allowlist the exact local/preview/production origins. Decide whether the launcher should survive every client navigation (root layout) or exist only under selected routes (nested layout/component).
Next.js App Router#
Use next/script in a component rendered by the layout that owns the launcher. The slug is safe to expose; do not use an API key or secret environment variable.
import Script from "next/script";
export function AgentEmbed() {
return (
<Script
id="maf-agent"
src="https://makeagent.fast/embed.js"
strategy="afterInteractive"
data-agent="YOUR_SITE_SLUG"
data-position="right"
data-label="Ask us"
/>
);
}Render <AgentEmbed /> once in app/layout.tsx for a site-wide agent or a nested layout for a route group. Do not render a separate copy from every page component. afterInteractive avoids server-side DOM access and does not block initial HTML parsing.
In the Next.js Pages Router, render the same component once from pages/_app.tsx for site-wide behavior. Rendering it from an individual page is appropriate only when leaving that page should remove the agent; in that case use the explicit effect/cleanup pattern below because a loaded external script has already created DOM outside React's tree.
React without Next.js#
Create the script in an effect. A stable script ID prevents duplicate insertion under development Strict Mode. If the component is route-scoped, cleanup must remove both the script and the root created by the loader.
import { useEffect } from "react";
const slug = "YOUR_SITE_SLUG";
export function AgentEmbed() {
useEffect(() => {
if (document.getElementById(`maf-embed-root-${slug}`)) return;
const script = document.createElement("script");
script.id = "maf-agent";
script.src = "https://makeagent.fast/embed.js";
script.dataset.agent = slug;
script.dataset.position = "right";
script.async = true;
document.body.appendChild(script);
return () => {
script.remove();
document.getElementById(`maf-embed-root-${slug}`)?.remove();
};
}, []);
return null;
}For a site-wide launcher, mount this component above the router and omit route-driven unmounting. Keeping the root preserves the iframe/conversation across navigation.
Environment configuration#
A build-time public variable may hold the slug because it is not secret. Validate it before rendering and keep real MAF API keys/provider credentials server-only. A missing slug causes the loader to warn and exit rather than creating a launcher.
If your application serves different customer tenants from one React build, do not switch data-agent on an already-executed script. Give the current tenant one stable slug, remove the old slug's root during an authenticated tenant switch, and then mount a fresh script. Never let an untrusted query parameter choose a cross-tenant slug.
Verify hydration and navigation#
- Load a public route directly and confirm one launcher.
- Navigate client-side through routes that should retain it.
- For a nested/route-scoped install, navigate out and confirm both script/root disappear, then back and confirm one fresh launcher.
- Inspect Network/CSP and test a real message.
- Run the production build; development Strict Mode alone is not final behavior.
Troubleshooting#
| Symptom | Fix |
|---|---|
document is not defined | DOM code ran during SSR; move it into useEffect or use next/script |
| Two launchers/agents appear | Multiple slugs or components are mounted; keep one owner layout |
| Launcher remains after route exit | Remove maf-embed-root-SLUG in cleanup, not only the script |
| Launcher disappears on every navigation | Mount higher in the layout/router tree |
| Works locally but not preview/production | Allowlist each deployment's exact origin |
No npm React/Next adapter is currently published; these examples use the supported public REST-independent loader.