← All posts
Integrations6 min read

[INTEGRATIONS] · Jun 21, 2026 · 11:34

How to Add Live Chat to a Nuxt App (Nuxt 3, SSR-Safe)

Two clean, SSR-safe ways to load a chat widget in Nuxt 3: an inline loader in nuxt.config.ts, or a client plugin in plugins/.

Tm

The muro team

muro.chat

#Nuxt#Vue#Live Chat#SSR#Integrations
How to Add Live Chat to a Nuxt App (Nuxt 3, SSR-Safe)

Nuxt 3 renders on the server first, then hydrates on the client. That single fact is why a lot of third-party chat snippets misbehave when you paste them in: they reach for window or document during SSR, throw document is not defined, or trigger hydration mismatches because the DOM the server produced no longer matches what the browser builds. A chat widget should never run on the server. It only has a job once there is a real browser with a real <head> to attach to.

The good news is that the muro widget loader is tiny (around 6 KB) and written to append itself to the document head on the client only. So the whole task in Nuxt comes down to one decision: where do you put the loader so it runs in the browser and never during server rendering? Below are the two clean ways to do it. Start with the nuxt.config.ts approach, since it is the least code and works for almost every app. Reach for the plugin only when you need conditional logic.

What you need first

You need a Nuxt 3 project and a muro widget id. The id comes from your muro dashboard once you create a project. If you do not have an account yet, you can start free (14-day trial, no card) and grab the id, or poke at a live demo first to see what the widget feels like. Everywhere below, replace YOUR_WIDGET_ID with that id.

Nuxt lets you inject <script> tags into the document head globally through app.head in your config. Because the muro loader's own body only touches document and window when it actually executes in the browser, and Nuxt does not run inline head scripts during server render, this is SSR-safe by construction. The script string is shipped to the client and runs there. Paste the exact loader as the innerHTML of a head script entry:

ts// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          // Loads the ~6 KB muro widget on the client and appends it to <head>.
          innerHTML: `(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'});`,
          // Nuxt sanitizes innerHTML by default; opt out so the loader survives intact.
          tagPosition: 'bodyClose'
        }
      ]
    }
  }
})

tagPosition: 'bodyClose' renders the loader just before </body> so it does not block first paint. The loader then creates its own <script> and appends it to the head exactly as written, which keeps the queue stub (muro('init', ...)) available before widget.js finishes downloading. That stub is why the init call can sit right next to the loader and not race it.

Option 2: a client plugin in plugins/

If you want the widget to load conditionally (only on certain routes, only for logged-in users, only after consent), build it in a plugin. Any file in plugins/ whose name ends in .client.ts runs in the browser only, so there is zero SSR risk and no sanitization to fight. This is the more explicit, more controllable path.

ts// plugins/muro.client.ts
export default defineNuxtPlugin(() => {
  // .client.ts means this never executes during SSR.
  const w = window as any
  const o = 'muro'
  w.MuroChat = o
  w[o] = w[o] || function () { (w[o].q = w[o].q || []).push(arguments) }

  const j = document.createElement('script')
  j.async = true
  j.src = 'https://muro.chat/widget.js'
  document.head.appendChild(j)

  w[o]('init', { widgetId: 'YOUR_WIDGET_ID' })
})

The .client.ts suffix is doing the load-bearing work here. Do not drop it. A plain plugins/muro.ts would run on the server too, where window and document do not exist, and you would be back to the document is not defined crash. With the suffix, Nuxt only ever bundles and runs this on the client.

Loading the widget on some routes only

Plenty of teams do not want chat on the marketing landing page but do want it inside the app, or vice versa. With the plugin approach you can gate it with a simple check before you append the script:

ts// plugins/muro.client.ts (route-gated)
export default defineNuxtPlugin((nuxtApp) => {
  const route = useRoute()
  // Skip chat on the public marketing pages, load it everywhere else.
  if (route.path === '/' || route.path.startsWith('/pricing')) return

  const w = window as any, o = 'muro'
  w[o] = w[o] || function () { (w[o].q = w[o].q || []).push(arguments) }
  const j = document.createElement('script')
  j.async = true
  j.src = 'https://muro.chat/widget.js'
  document.head.appendChild(j)
  w[o]('init', { widgetId: 'YOUR_WIDGET_ID' })
})

Verify it works

  • Run npm run dev and open your app. The chat launcher should appear in the corner within a second or two.
  • Open DevTools, Network tab, and confirm a request to widget.js from muro.chat returns 200.
  • Hard refresh and watch the console. No document is not defined and no Vue hydration mismatch warnings means your SSR boundary is correct.
  • Run npm run build && npm run preview to confirm it also behaves in the production server build, not just dev.

What you get once it loads

The widget gives your Nuxt app a shared inbox across every project you run, so support for all of your sites lands in one place at one flat price. The AI answers from your own docs and hands off to a human when it is unsure, which keeps a small team from drowning. If you expect real volume, read why AI customer support gets expensive before you pick a plan, then consider bringing your own AI key so the AI runs unlimited at zero markup.

That is the whole integration. One inline loader in nuxt.config.ts for the common case, or a .client.ts plugin when you need control, both SSR-safe by design. When you are ready to point it at production, double check your widget id and review the flat pricing so you know what your project count puts you on.

✦ 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