Before you start
- A Laravel app with a Blade layout that every page uses, through
@extendsor a layout component. A fresh Laravel 13 app only haswelcome.blade.php, so if that's all you have, the snippet goes there. - Your website ID from Websites in the Pidge inbox. This is the plain snippet it comes in:
<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
@jsonprints the ID as a quoted JavaScript string.- The
@ifleaves the chat out whereverPIDGE_WEBSITE_IDisn't set, for example on a local copy where you don't want it. window.$pidge || []keeps any commands your own scripts pushed before this line ran.
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
- It moves the same chat into the new page, so a conversation that's open stays open, and removes the old copy that back and forward can restore.
data-navigate-oncestops Livewire running these scripts again on every page, which would add the listeners again each time.@persistdoesn't help here: it keeps elements written in the layout, and the bubble is added by the script after the page loads.
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>
connect-srcneeds the chat's address twice: once as it is, and once withwss://for the live connection. Thepreg_replaceline does that.- Without
'unsafe-inline'instyle-src, the bubble loads with no styling and fills the page. - The loader line doesn't need a nonce, because its address is in
script-src.
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
- Open your site in a private window, so you're seeing what a visitor sees.
- The chat bubble should be in the bottom corner. Send yourself a message and answer it from the inbox.
- 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
- WordPress
- Shopify
- Squarespace
- Wix
- Webflow
- Ghost
- Framer
- Next.js
- Astro
- React
- Vue and Nuxt
- Gatsby
- Hugo
- Jekyll
- Django
- Carrd
- Plain HTML
Stuck, or did a step look different on your screen? Tell us in the chat on this page.