Install guide
Installing a chat assistant in a Next.js project
Next.js has two answers depending on which router you are on, and both are one line. The part that costs people an afternoon is not the install. It is the development server double invoking effects, and preview deployments answering on origins the allowlist has never seen.
The tag
<script src="https://cdn.askably.xyz/w.js" data-key="pk_live_YOUR_KEY" defer></script>Your own key replaces the placeholder. You will find it on the install tab of the assistant.
The origin the page is served from has to be on the allowlist for that assistant, or nothing renders and the browser console says which origin was refused. An apex domain and its www are two different origins to a browser, so list both, along with any staging or preview host you want it to work on.
Where it goes on Next.js
app/layout.tsx in the App Router, or pages/_document.tsx in the Pages Router. Not a client component that can unmount.
Any Next.js app where you own the root layout or the custom document, which is every Next.js app. There is nothing to unlock and no plugin to add.
Step by step
- 1Open app/layout.tsx and find the body element the root layout returns.
- 2Import the script component from next/script and render it as the last child of body, with src pointing at the loader, strategy set to afterInteractive, and the data-key attribute carrying your publishable key.
- 3If you would rather not use the script component, a plain script element with defer does the same job in the same position, because the loader defers either way.
- 4On the Pages Router, do the same thing in pages/_document.tsx, after the NextScript element inside the body.
- 5Restart the development server, load any route, and confirm the launcher appears in the corner once the page settles.
- 6Add every origin the app answers on to the allowlist before you share the link: production, www, and any preview hostname you actually open.
What goes wrong on Next.js
Do not mount it from an effect
React development mode runs effects twice on purpose, so an effect that appends the tag appends it twice. The loader refuses the second install because it checks for its own global before doing anything, which means you get one widget rather than two. You also get a stray script element in the DOM and a pattern that breaks properly the moment somebody copies it into a component that remounts on navigation.
The layout renders the tag as markup, once, with no effect involved. That is the whole reason it is the right place.
beforeInteractive is only honoured in the root
The script component accepts a beforeInteractive strategy, and Next.js only honours it in app/layout.tsx or pages/_document.tsx. Using it inside a page or a client component fails the build with a message saying exactly that.
There is no reason to reach for it here anyway. The loader waits for the document to be ready before it starts, so running it earlier buys nothing and delays your own content.
Preview deployments are separate origins
Every preview build answers on its own hostname, and a hostname is an origin. The tag is present, the browser console names the refused origin, and nothing renders. It looks like the install failed on that branch.
Add the preview hostnames you actually use to the allowlist, or accept that previews will not show the widget and verify on production instead.
Origins to allowlist
These are the origins a Next.js site is typically served from. List every one you want the assistant to answer on, including the ones only you visit.
- https://yourdomain.com
- https://preview.yourdomain.com
- http://localhost:3000
How the tag behaves, wherever you put it
The rest of this is the same on every platform, so it is worth reading once rather than on each guide.
| One tag, nothing else | There is no second file, no stylesheet and no package to install. The tag is the whole install. |
|---|---|
| Where it goes | Immediately before the closing body tag. It works in the head too, because it defers, but the body keeps it out of the way of anything that measures render order. |
| data-key is required | Without it the loader stops and writes a warning to the browser console rather than rendering anything. |
| It finds itself | The loader reads the tag it was loaded from, and falls back to the first tag on the page carrying a data-key. Platforms that move or re-inject the tag do not break it. |
| It installs once | A second copy of the tag on the same page is ignored, so a template that renders twice does not produce two launchers. |
| Assets follow the tag | Everything else it needs is fetched from the same origin the tag was served from, so a proxy or a private domain needs no extra configuration. |
| The panel loads late | Only the small launcher is on the page at first. The conversation panel is fetched the first time a visitor opens it, in its own frame. |
| Styles cannot collide | The launcher renders inside a closed shadow root and the panel inside a frame, so nothing on the host page can restyle either, and neither can restyle the host page. |
Set up on Next.js for a particular trade
The tag is the same everywhere. What is not the same is which of your pages the assistant can read once it is running, and on Next.js that depends on what kind of site this is. These go one level further than this guide.
- For a SaaS companyThe root layout is the documented place for the tag, and on a subscription product it is also the signed in dashboard. Move it down a level.
- For a fintech appThe policy decides whether the tag runs at all. Script and frame directives both matter, and a report only header proves nothing.
Taking it off again
Delete the script element from app/layout.tsx or pages/_document.tsx and redeploy. A visitor already mid-session keeps the widget until they reload, because client side navigation does not tear down an installed loader.
Before you remove it
- Removing the tag stops the widget on that site. It does not delete the assistant, its material, or the enquiries it has already collected.
- If you are moving the widget to a different host rather than retiring it, add the new origin to the allowlist before you remove the old tag, or there is a window where neither works.
Questions
- Does client side navigation reinstall it?
- No, and it does not need to. The root layout is not remounted when you navigate between routes, so the tag is rendered once for the life of the document and the loader stays installed. Nothing re-runs and nothing is duplicated.
- App Router or Pages Router, does it change the install?
- Only which file you edit. app/layout.tsx and pages/_document.tsx are the same idea, the one place that owns the html and body elements. The tag itself is identical in both.
- Can I put it in a client component instead?
- You can, but you are choosing a harder problem. A client component can unmount, and then you need the destroy method on the global to tear the widget down cleanly. The layout has none of that ambiguity.
- Why does it work locally but not on the deployed site?
- Nearly always the allowlist. Open the console on the deployed page and read the refused origin it names. Apex and www count separately, so add both if the site answers on both.
Keep reading
- Install on Reactindex.html at the project root for a Vite app, or public/index.html for Create React App. Immediately before the closing body tag.
- Install on Vueindex.html at the project root, after the div the app mounts into and immediately before the closing body tag.
- Install on Nuxtnuxt.config.ts, under app, then head, then the script array. Or a useHead call in layouts/default.vue.
- Every install guideThe same one line of HTML, and where it goes on each platform.
Try it on your own material
Upload a document or point it at your site, paste one line of HTML, then ask it something only your business could answer.