← All posts
Guides7 min read

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

Django live chat: add a widget to base.html without touching Channels

One snippet in templates/base.html before </head> covers every page that extends it, the admin excludes itself, and a small context flag keeps staging out of your production inbox.

Tm

The muro team

muro.chat

#django#live chat#python#widget install#ai support#templates

Your Django app is live, and support currently means a contact form wired to send_mail, plus the occasional user replying to a password reset email and hoping a human sees it. So you search for django live chat and every tutorial wants the same project: install Channels, migrate to ASGI, stand up Redis for the channel layer, write a consumer and a routing file, and then build the inbox where you would actually answer people. That is a multi-week side quest for a feature that is not your product. Here is the ten minute version instead: one script tag in templates/base.html, an AI that answers from your docs, and email fallback for when you are heads down.

Build it with Channels, or embed it?

Django makes real-time chat look buildable because Channels is genuinely good. The tutorial chat app with a WebSocket consumer, an ASGI application, and a Redis channel layer is a classic for a reason. But as a support channel, the socket is maybe 10 percent of the work. The other 90 percent never makes it into tutorials:

  • →an inbox where you read, assign, and answer conversations (with auth, search, and state)
  • →visitor identity and message persistence across sessions and devices
  • →notifications for when you are not staring at the inbox, which is most of the time
  • →email fallback for visitors who close the tab before you reply
  • →spam filtering and all the abuse cases you have not met yet

If chat is your product, build it with Channels and enjoy every minute. If chat is just how customers reach you, embed a hosted widget and get back to your actual roadmap. The rest of this guide is the embed path with muro: a roughly 9 KB async widget on the front, a shared inbox with AI on the back. No pip install, no ASGI migration, no Redis, no new deployment surface.

Step 1: grab your widget ID

Sign up for muro (14-day free trial, no charge today), create a site for your app, and copy the widget ID from the install screen. Two minutes in the settings pays off later: set the widget color, position, agent name, and avatar so it looks native to your design, and pick from 12+ languages if your audience is not English-first.

Step 2: paste the snippet into templates/base.html

The install is one edit to the layout every page already extends. In a typical project that is templates/base.html, sitting in the project-level template directory you declared in the TEMPLATES setting (usually "DIRS": [BASE_DIR / "templates"]). If your project is organized differently, the right file is simply the one that contains your </head> tag and gets pulled in by {% extends "base.html" %} everywhere else. Paste this right before </head>:

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>

Replace YOUR_WIDGET_ID, deploy, done. Every template that extends base.html now has chat: the landing page, the dashboard, the billing views, all of it. The script loads async so it does not block rendering, and because it is pure client-side JavaScript it does not care whether you serve WSGI under gunicorn or ASGI under uvicorn. Your requirements.txt does not change by a single line.

One widget ID per environment, none on staging

Hardcoding the widget ID works right up until your staging server starts feeding test chats into your production inbox. The Django-shaped fix is to read the ID from settings and expose it through a tiny context processor:

python# settings.py
import os

MURO_WIDGET_ID = os.environ.get("MURO_WIDGET_ID", "")

# core/context_processors.py
from django.conf import settings

def muro(request):
    return {"MURO_WIDGET_ID": settings.MURO_WIDGET_ID}

Register "core.context_processors.muro" in the context_processors list under OPTIONS in your TEMPLATES setting, then wrap the snippet in base.html:

html{% if MURO_WIDGET_ID %}
  {# the muro snippet from step 2 goes here #}
{% endif %}

Inside the snippet, change the init line to muro('init',{widgetId:'{{ MURO_WIDGET_ID }}'}) so the template fills in whatever the environment provides. Production sets the MURO_WIDGET_ID environment variable, staging leaves it unset, and the widget simply does not render there. If you would rather test the full flow before shipping, create a second site in muro instead (the Solo plan covers 2 projects) and give staging its own ID, so test conversations land in their own inbox.

Tidier: keep it in a partial

If you do not want vendor JavaScript inlined in your layout, put the guarded snippet in templates/partials/muro.html and drop {% include "partials/muro.html" %} before </head>. One file owns the whole integration, and future you knows exactly where to look when rotating IDs or removing it.

htmx and CSP wrinkles

Two real-world notes. If you sprinkle htmx over your Django templates, nothing special is needed: the widget loads once in the head, and htmx swaps fragments without full page reloads, so it just keeps working (including with hx-boost). And if you enforce a Content Security Policy through django-csp, allow https://muro.chat in your script sources, and in your connect sources too, since the widget calls home to deliver messages.

✦ ✦ ✦

Now make it answer without you

Installing the widget is the easy part, same as the WebSocket consumer would have been. The reason to bother is what happens when a visitor types. In the muro dashboard, train the AI by importing a URL: paste your docs or FAQ address and it ingests the content. From then on it answers grounded on your published help articles and product context, not a general-purpose chatbot doing improv on your pricing page. Feed it:

  • →your docs or help center URL (re-import when things change)
  • →your FAQ page, especially pricing, billing, and account questions
  • →product context: what the app does, what the plans include, where the limits are

The part that keeps it trustworthy: when the AI is unsure, it hands the conversation to a human. Refund requests, account problems, and visitors who are clearly angry get escalated instead of answered with confident hallucination. For a solo Django dev that is exactly the right split. The AI absorbs the how-do-I-rotate-my-API-key traffic, and you take the conversations where being human is the feature.

Reply-by-email, for when you are deep in a migration

The failure mode of every chat install is the abandoned bubble: someone writes, nobody answers, and the widget starts doing negative marketing for you. muro's answer for small teams is email. Conversations the AI hands off get forwarded to your inbox, you reply from your mail client, and the reply lands back in the visitor's chat window as if you had been sitting in the dashboard the whole time. No extra tab, no separate app to check.

Add a few saved replies for the questions you answer weekly, an automation rule or two for the repeat patterns, and the support surface of your Django app runs on minutes a day. And because you build things for a living, none of it is a black box: there is a full REST API and webhooks with per-site scoped keys, so you can pipe new conversations into your own admin, a Slack channel, or a management command that runs on cron. The API docs cover all of it.

Ship it

That is the whole job: create a site in muro, paste the snippet into templates/base.html before </head>, add the context processor so staging stays quiet, and point the AI at your docs. The admin excludes itself, every page that extends your base is covered, and your users get answers at 3 a.m. without you writing a single consumer. Poke at the widget in the live demo, then go make the edit to base.html.

✦ Frequently asked

Questions people ask.

01How do I pass the logged-in Django user into the chat so they don't retype their email?+

Call muro('identify', ...) right after the init line, filled from the template context. Wrap it in {% if user.is_authenticated %} and pass email: '{{ request.user.email|escapejs }}' plus an optional name. Identified visitors skip the pre-chat email form, their history follows them across devices, and the visitor.identified webhook fires. Only send server-rendered session values, never anything the visitor can edit client-side.

02Will the widget slow down my Django pages or break collectstatic?+

No on both counts. The script is served from muro.chat, so it never enters your staticfiles pipeline: nothing to run through collectstatic, no WhiteNoise manifest entry, nothing for ManifestStaticFilesStorage to hash or fail on. It is roughly 9 KB gzipped, loads async so it stays off the render path, and draws inside a Shadow DOM, so it cannot collide with your Bootstrap or Tailwind CSS.

03Do I need a Python developer to install this?+

You need someone who can edit a template and deploy, not a Python developer. There is no pip package, no migration, no settings rewrite required for the basic install, and requirements.txt does not change by a line. Once the snippet ships in templates/base.html, anyone on your team can work the inbox from the dashboard, since every plan includes unlimited agents with no per-seat cost.

04Is there a free plan, or what does live chat actually cost per month?+

There is no free-forever plan. You start a 14-day trial that requires a card (no charge today, cancel in one click before day 14, otherwise it auto-charges). Solo is $19/mo for 2 projects with 1,000 managed AI credits, Fleet is $59/mo for unlimited projects with 3,000. Unlimited agents on both, no per-resolution AI fees, or paste your own Anthropic key for unlimited AI at zero markup.

05Does the chat widget set cookies, and what do I put in my privacy policy?+

The widget writes no cookies on your domain. It keeps a visitor token in localStorage so a conversation survives a reload. Data is hosted in the EU, with a signed DPA and a public sub-processor list, so list muro as a processor in your policy. If you run a consent banner, gate the {% include "partials/muro.html" %} behind the consent check rather than rendering it unconditionally.

06Will this work with Wagtail, or with a Django API and a separate React frontend?+

Wagtail behaves the same way: page templates extend your project's base.html, so the single edit covers every published page, and the Wagtail admin renders from its own template tree, so the bubble stays out of it. If Django is only a DRF backend behind a React or Vue app, the snippet belongs in the frontend's index.html, not in a Django template at all.

✦ 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, so you can browse every install guide.

Tm

✎ Written by

The muro team

muro.chat