← All posts
Guides7 min read

[GUIDES] · Jul 8, 2026 · 14:40

How to add live chat to Docusaurus (and let it answer from your docs)

The scripts array in docusaurus.config.js cannot run a chat snippet's init call. Here is the src/theme/Root.js setup that actually works, plus how to train the AI on the same docs the widget sits on.

Tm

The muro team

muro.chat

#docusaurus#live chat#documentation#react#widget#ai support

Someone is on your Docusaurus site right now, three clicks deep, reading a page that almost answers their question. If it does not quite get them there, they have two moves: open a GitHub issue and wait a day, or close the tab. A chat bubble in the corner gives them a third move, and on a docs site that bubble is unusually powerful, because the AI behind it can be trained on the exact pages your visitor is already reading.

This guide shows the honest ways to add live chat to Docusaurus. There is a trap here: the scripts array in docusaurus.config.js looks like the obvious home for a chat snippet, and it cannot actually do the job. We will cover why, then walk through the approach that works (a custom Root component), and finish with how to point muro's AI at your docs so the widget answers from them.

Why a docs site is the best chat surface you own

Think about who lands on docs. Not casual browsers. People mid-task, holding an error message, trying to make your product do something specific. That is the highest intent traffic you have, and it is also the moment support is cheapest to deliver: the visitor can tell you exactly what they tried, and the answer usually already exists two pages away.

  • Intent is high. Nobody reads an API reference for fun. A question asked from a docs page is specific and answerable.
  • Context is free. The conversation shows which page the visitor wrote from, so you skip the whole 'what were you trying to do' round trip.
  • The answer is usually in the docs already. Which is exactly what makes AI auto-replies work here. More on that below.

The alternative is what most open source and indie projects do: a 'file an issue' link. Issues are great for bugs. They are a terrible interface for 'which config option do I need', because the answer takes thirty seconds and the round trip takes a day.

What you are installing

muro's widget is a roughly 6 KB async loader. It does not block rendering and it will not move your Lighthouse score. On a plain HTML site, installation means pasting this before the closing head tag:

html<script>
  (function(w,d,s,o){
    w.MuroChat=o;w[o]=w[o]||function(){(w[o].q=w[o].q||[]).push(arguments)};
    var j=d.createElement(s);j.async=1;j.src='https://muro.chat/widget.js';
    d.getElementsByTagName('head')[0].appendChild(j);
  })(window,document,'script','muro');
  muro('init',{widgetId:'YOUR_WIDGET_ID'});
</script>

Docusaurus is not a plain HTML site, though. It is a React app that gets server-rendered to static HTML at build time and then hydrates in the browser. That changes where this snippet can live, and it is the exact spot where most tutorials get it wrong.

The trap: the scripts array cannot run the init call

docusaurus.config.js has a scripts option, and it is the first thing everyone reaches for. It accepts script URLs plus attributes:

js// docusaurus.config.js
module.exports = {
  scripts: [
    {src: 'https://muro.chat/widget.js', async: true},
  ],
};

This loads the file. It is also not enough. Look back at the snippet above: it does two jobs. It injects widget.js, and it calls muro('init', {widgetId: '...'}) so the widget knows which site it belongs to. The scripts array only handles the first job. It takes URLs and attributes, not inline JavaScript, so there is nowhere for the init call to go. You would ship the loader and the bubble would never appear.

The right way: a custom Root component

Docusaurus reserves src/theme/Root.js as a wrapper around the entire app. You do not need to run the swizzle command; if the file exists, Docusaurus picks it up. Root never unmounts during client-side navigation, which makes it the perfect home for a run-once effect. Create the file:

js// src/theme/Root.js
import React, {useEffect} from 'react';

export default function Root({children}) {
  useEffect(() => {
    if (window.MuroChat) return; // hot reload guard
    (function (w, d, s, o) {
      w.MuroChat = o;
      w[o] = w[o] || function () {
        (w[o].q = w[o].q || []).push(arguments);
      };
      var j = d.createElement(s);
      j.async = 1;
      j.src = 'https://muro.chat/widget.js';
      d.getElementsByTagName('head')[0].appendChild(j);
    })(window, document, 'script', 'muro');
    window.muro('init', {widgetId: 'YOUR_WIDGET_ID'});
  }, []);

  return <>{children}</>;
}

It is the same loader from the HTML snippet, wrapped in a useEffect with an empty dependency array. Three properties of this setup do the heavy lifting:

  • It is SSR-safe. useEffect never runs during the static build, so npm run build will not blow up with window is not defined.
  • It runs once. After the initial load Docusaurus navigates client-side, and Root stays mounted the whole time. The widget loads on the first page and survives every navigation after that, so an open conversation follows the visitor across pages.
  • It is still async. Same ~6 KB non-blocking loader, just injected from React instead of raw HTML.

To get your widget ID: sign up (14-day free trial, no card needed), create a site, and copy the ID from the installation screen. Paste it into Root.js, run npm run start, and the bubble appears in the corner of every page. That includes versioned docs and every locale if you use i18n, because Root wraps all of it.

Using TypeScript?

Same file as src/theme/Root.tsx. Type the props as {children: React.ReactNode} and keep everything else identical.

The fallback: headTags

Recent Docusaurus versions also support a headTags option in the config, which renders extra tags into the document head. The documented shape is a tag name plus attributes, which is great for meta and link tags but leaves no obvious home for the inline init call, the same problem the scripts array has. Plugins get a more flexible injectHtmlTags lifecycle that can inject inline scripts, but writing a plugin to paste a snippet is overkill. Root.js stays the better choice: the logic lives in one obvious file, and it is plain React.

Point the AI at the docs it lives in

Here is the part that makes Docusaurus a special case rather than just another install guide. Your docs site is a pile of answers you already wrote. muro's AI auto-reply is grounded on your published help content and product context, and you train it by importing a URL. So you point it at the same Docusaurus site the widget sits on.

  1. 01In the muro dashboard, import your docs URL so the AI ingests your published pages.
  2. 02Add a short product context note: what the product does, who it is for, what you charge.
  3. 03Test it with the last ten real questions from your GitHub issues or your inbox.

Now the loop closes. A visitor on your getting-started page asks 'does this work with pnpm', and the AI answers from the installation page they had not found yet, in seconds, at 2 a.m. your time. Not a canned deflection macro. An actual answer sourced from your own writing.

One habit worth building: when you rewrite a chunk of the docs, import the URL again so the AI answers from the current version, not the one from three releases ago.

Make it look native

Docusaurus sites have a strong visual identity, usually built around one primary color. The widget should not fight it. muro lets you set the accent color, position, size, agent name, and avatar, so the bubble can match your theme's primary color and sit bottom-right without covering the table of contents. The widget ships in 12+ languages; if your docs are localized, set the widget language to match your default locale.

And if compliance matters to your audience: muro is EU-hosted with GDPR export and erasure built in, which gives you a one-line answer the next time a user asks where their chat data lives.

Gotchas worth knowing

  • Keep the loader inside useEffect. Move it to module scope in Root.js and it executes during the static build, where window does not exist, and npm run build fails.
  • Dev server double-inject. Hot reload can re-run effects in development. The if (window.MuroChat) return guard at the top of the effect makes that harmless.
  • It appears everywhere. Root wraps the blog, the docs, versioned docs, and every locale. That is usually what you want; if you need per-route logic you can branch on window.location.pathname inside the effect, but start without it.
  • Conversations are not locked in the widget. Everything is reachable over the REST API and webhooks (API docs) if you later want to pipe questions into a triage script or a metrics dashboard.

What it costs

muro's Solo plan is $19/mo for up to 2 projects with unlimited agents; there is no per-seat pricing, so adding a co-maintainer to the inbox costs nothing. Solo includes 1,000 managed AI credits a month (1 credit = 1 AI action), and if you would rather run unlimited AI, paste your own Claude (Anthropic) API key and muro uses it at zero markup. Fleet is $59/mo for unlimited projects. Details on pricing.

✦ ✦ ✦

The whole setup is maybe twenty minutes: create a site in muro, drop Root.js into your theme folder, paste your widget ID, and import your docs URL so the AI answers from the pages your users are already reading. If you want to poke a working widget first, the live demo is one click. Your docs already do the answering. This just puts them in the conversation.

✦ Try it

One support inbox for all your projects, one flat price.

muro is live chat, an AI that answers from your own docs, and a shared inbox for every site you run. See it both sides in the live demo, check the flat pricing, or see how muro compares. It's the same snippet on every platform — browse every install guide.

Tm

✎ Written by

The muro team

muro.chat