Gatsby is React, so the tempting move is to wrap a chat widget in a component, drop it in your layout, and let it mount. It works, until it does not: a third-party script that manipulates the DOM inside React's tree is a reliable source of hydration warnings and the occasional re-render that resets the widget. The clean way is to inject the snippet once, at the HTML level, outside React entirely. Gatsby gives you exactly that hook: onRenderBody in gatsby-ssr.js.
Why gatsby-ssr.js, not a component
A chat widget is not part of your app state, does not need props, and should mount exactly once for the life of the page. Rendering it through a React component ties its lifecycle to your route transitions and hydration; injecting it via gatsby-ssr.js puts a plain <script> in the document that Gatsby ships as static HTML. The widget then lives in its own Shadow DOM, next to your React root but never inside it, so React never tries to reconcile it.
Step 1: grab your widget ID
Sign up for muro (14-day free trial, no card), create a site, and copy the widget ID. Set the color, position, agent name and avatar while you are there so it matches your site.
Step 2: inject it from gatsby-ssr.js
Create or open gatsby-ssr.js at the root of your project and add an onRenderBody that appends the widget script to the end of the body:
js// gatsby-ssr.js
import React from 'react';
export const onRenderBody = ({ setPostBodyComponents }) => {
setPostBodyComponents([
<script
key="muro"
dangerouslySetInnerHTML={{
__html: `
(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'});
`,
}}
/>,
]);
};Replace YOUR_WIDGET_ID, restart gatsby develop, and the bubble appears on every page. Because it is injected server-side into the static HTML, it is present on first paint with no client-side flash, and there is no component to import into your layout.
Service workers and gatsby-plugin-offline
If you use gatsby-plugin-offline, the async widget script is fetched at runtime from muro.chat and is not part of your precached shell, so nothing special is needed: it loads on the first online visit and the offline shell simply renders without it. No configuration, no cache-busting.
Point the AI at your MDX docs
Gatsby's sweet spot is content-heavy sites, and a lot of them are documentation built from MDX. Paste your docs URL into muro and the AI reads your published pages and answers from them, so the same content that renders on the page also powers the chat. Readers get an answer in the widget without leaving the page they are on.
Offline? It becomes email
Anything the AI cannot handle is forwarded to your inbox; you reply from your mail client and it lands back in the chat. Add saved replies for the repeat questions and you are running support on minutes a day, with a REST API and webhooks when you want to wire conversations into your own tooling, the API docs have the details.
Ship it
Add gatsby-ssr.js with the onRenderBody above, drop in your widget ID, and point the AI at your docs. One script, injected once, outside React, on every page, no component, no hydration warnings, no re-render churn. See it working in the live demo, then add the file.