Add live chat to Laravel

In Laravel, the snippet goes in the Blade layout every page uses, with the website ID read from your config. If you use Livewire's wire:navigate, a few more lines keep the bubble on screen between pages.

Tested with Laravel 13.39 on PHP 8.5 on 2 November 2026, with and without Livewire 4.4 and a Content Security Policy.

Before you start

HTML
<script>window.$pidge = []; window.PIDGE_WEBSITE_ID = "YOUR-WEBSITE-ID";</script>
<script src="https://app.pidge.chat/client/l.js" async></script>

Your own copy is in the Pidge inbox under Websites, in Code for your website, with your website ID already filled in. If you've set up a chat domain, it loads from there instead.

You can paste those two lines into your layout as they are. The steps below keep the ID in your config instead, so staging and production can use different websites.

1. Put the website ID in your config

Add the ID to .env:

PIDGE_WEBSITE_ID=YOUR-WEBSITE-ID

Then add a pidge entry to config/services.php, next to the others in the array:

'pidge' => [
    'website_id' => env('PIDGE_WEBSITE_ID'),
],

Read it with config() in your views, not env(). If you run php artisan config:cache when you deploy, a change to .env only shows up after you run it again.

2. Add the snippet to your layout

Open your shared layout and paste this just before </body>:

@if (config('services.pidge.website_id'))
    <script>window.$pidge = window.$pidge || []; window.PIDGE_WEBSITE_ID = @json(config('services.pidge.website_id'));</script>
    <script src="https://app.pidge.chat/client/l.js" async></script>
@endif

If your pages use more than one layout, add it to each, or move it into a partial and @include it.

If you use Livewire's wire:navigate

A wire:navigate link swaps the page's <body> without a full page load. The chat bubble lives in the body, so it disappears after the first click. Pressing back can also bring back an old copy of it. Use this version in the layout instead of the one above:

@if (config('services.pidge.website_id'))
    <script data-navigate-once>window.$pidge = window.$pidge || []; window.PIDGE_WEBSITE_ID = @json(config('services.pidge.website_id'));</script>
    <script src="https://app.pidge.chat/client/l.js" async data-navigate-once></script>
    <script data-navigate-once>
        document.addEventListener("livewire:navigating", () => {
            window.__pidgeChat = document.querySelector("[data-pidge]");
        });
        document.addEventListener("livewire:navigated", () => {
            const chat = window.__pidgeChat;
            if (!chat) return;
            document.querySelectorAll("[data-pidge]").forEach((el) => el !== chat && el.remove());
            if (!chat.isConnected) document.body.append(chat);
        });
    </script>
@endif

Links without wire:navigate are normal page loads and don't need this.

Open the chat from a button

<button type="button" onclick="window.$pidge.push(['do', 'chat:open'])">Chat with us</button>

With a Content Security Policy, an inline onclick is blocked. Add the listener in a script with your nonce instead, as in the next section. The widget commands list everything else you can push, like ["set", "user:nickname", [name]] for a logged-in customer. For emails your team can trust, sign them on the server: see signed visitor emails.

If your app sets a Content Security Policy

Laravel doesn't send a CSP on its own. If you've added one, the browser needs to allow the chat's script, its connection and its inline styles, and the first line of the snippet needs your nonce. This middleware is the one we tested, built on Laravel's Vite::useCspNonce():

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Vite;
use Symfony\Component\HttpFoundation\Response;

class ContentSecurityPolicy
{
    public function handle(Request $request, Closure $next): Response
    {
        Vite::useCspNonce();

        $pidge = 'https://app.pidge.chat';
        $pidgeSocket = preg_replace('/^http/', 'ws', $pidge);

        return $next($request)->withHeaders([
            'Content-Security-Policy' => "default-src 'self'; "
                ."script-src 'self' 'nonce-".Vite::cspNonce()."' $pidge; "
                ."connect-src 'self' $pidge $pidgeSocket; "
                ."style-src 'self' 'unsafe-inline'",
        ]);
    }
}

Register it for web routes in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->web(append: [
        \App\Http\Middleware\ContentSecurityPolicy::class,
    ]);
})

Then add the nonce to the first script in your layout:

<script nonce="{{ Vite::cspNonce() }}">window.$pidge = window.$pidge || []; window.PIDGE_WEBSITE_ID = @json(config('services.pidge.website_id'));</script>

If you already have a policy, add these sources to it rather than replacing it. We haven't tested image attachments under a CSP. If they don't show, the browser console will name the directive to change.

List your domains in Pidge

The chat only connects on domains you've listed for the website. In the inbox, open Websites and put each domain on its own line in Domains. Subdomains are included, so example.com also covers www.example.com.

If people visit your app at a second address your host gave it, list that full address too.

To try the chat while you develop, add localhost to the list too. Pidge matches the host name, so any port works. 127.0.0.1 isn't accepted, and that's the address php artisan serve prints, so change it to localhost in the browser. A .test address from Herd or Valet has to be listed on its own.

Check that it works

  1. Open your site in a private window, so you're seeing what a visitor sees.
  2. The chat bubble should be in the bottom corner. Send yourself a message and answer it from the inbox.
  3. In the inbox, the website's Not installed yet label goes away after the first visit from a listed domain.

If the bubble shows but the chat won't connect, the domain you're on isn't in the website's list in Pidge. If there's no bubble at all, view the page source and look for PIDGE_WEBSITE_ID. If it's missing, the config value is empty: check .env and run php artisan config:cache again if you use it.

With wire:navigate, also click through two or three pages, press back, and check there's still one bubble.

Laravel's own guides: laravel.com/docs/13.x/configuration and livewire.laravel.com/docs/navigate

Other platforms

Stuck, or did a step look different on your screen? Tell us in the chat on this page.

Get your snippet

Start free

The free plan covers one website and two people replying. No card needed.