In a Laravel app, the tracking tags almost always live in one place: the <head> of the master Blade layout. That's convenient, and it means Google Analytics or a Meta Pixel renders into every server response and runs the instant the page loads, before a visitor has agreed to anything. Laravel's server-rendered model also gives you a second surface most consent guides ignore: server-side tracking, queued jobs firing conversion events, and pixels you send from PHP, none of which a browser-side banner can touch.
The compliance risk is real. France's CNIL fined SHEIN 150 million euros in September 2025, partly because trackers fired before any choice and continued after "Reject all" (CNIL). If your Laravel app serves the EU, the UK, or a US opt-out state, you need to control both surfaces.
Blade stacks give you script control
Blade layouts use @extends, @section, and @yield to compose pages. For scripts, the tool you want is stacks. Declare a named stack in the layout head with @stack('scripts'), then have any child view push into it with @push('scripts'), @prepend('scripts'), or the dedupe-safe @pushOnce and @prependOnce (Laravel Blade docs). That lets a single page add its own tracking without editing the global layout, and it keeps ordering predictable. Your own bundled assets come in through the @vite directive, which is separate from third-party tags.
Where the consent loader goes
Put the loader in the head of your master layout (commonly resources/views/layouts/app.blade.php), as the first script and before the @stack that pages push into. It has to initialize before the tags it controls.
<!-- resources/views/layouts/app.blade.php -->
<head>
<meta charset="utf-8">
<!-- FIRST, before @vite and any tracker -->
<script async src="https://cdn.cookiebeam.com/banner/YOUR_BANNER_ID/default/loader.js"></script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@stack('head-scripts')
</head>Replace YOUR_BANNER_ID with your banner's public ID from the dashboard.
Block trackers until consent
Load trackers in a disabled state and switch them on after opt-in. CookieBeam activates any script tagged type="text/plain" with a data-category once the visitor accepts. Push page-specific tags into the stack:
<!-- resources/views/products/show.blade.php -->
@pushOnce('head-scripts')
<script
type="text/plain"
data-category="analytics"
data-cookiebeam-managed="true"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX">
</script>
@endPushOnceThe browser ignores a text/plain script until the consent runtime enables it, so a rejected visit loads nothing. See how to block scripts before consent.
Reading the consent cookie in PHP (mind EncryptCookies)
Here's the Laravel-specific trap. The consent banner sets a plain cookie in the browser, but Laravel's EncryptCookies middleware expects every incoming cookie to be encrypted. When it hits a cookie it can't decrypt, it discards it, so $request->cookie('cookiebeam_consent') comes back null even though the browser is sending it. The fix is to exclude the consent cookie from encryption. In Laravel 11 and 12, do that in bootstrap/app.php:
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->encryptCookies(except: ['cookiebeam_consent']);
})Now you can read it and gate server-side work:
$raw = $request->cookie('cookiebeam_consent');
$analytics = false;
if ($raw) {
$data = json_decode($raw, true);
$analytics = ($data['analytics'] ?? false) === true;
}
// Always run the functional logic; only track with consent
if ($analytics) {
SendServerSideConversion::dispatch($order);
}Wrap this in middleware if you check it on many routes. The cookie reflects consent at request time, so pair server checks with the client-side blocking above. More on this split in server-side consent enforcement.
Consent Mode v2 with GTM
For Google Ads or GA4 on EEA or UK traffic, Consent Mode v2 has been required since March 2024. Set defaults to denied in the layout head before GTM loads:
<!-- resources/views/layouts/app.blade.php, after the loader -->
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
ad_storage: 'denied',
analytics_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
wait_for_update: 500
});
</script>
<!-- GTM container snippet goes here -->CookieBeam applies the defaults and fires the update on acceptance. The GTM consent setup guide covers the container side.
Livewire and Inertia
If you run Livewire or Inertia, remember that both update the page without a full reload after the first load. The consent loader in your master layout still initializes once, which is fine, but any component that mounts a tracker on render needs to check consent through the same cookiebeam:consent event rather than assuming a fresh page load gates it. Treat these like a single-page app for the interactive parts. The SPA consent guide covers that timing.
Policy and testing
Publish a cookie policy route and link it from the banner and footer, and keep the cookie table honest as you add integrations (a scanner does this automatically, see scanning versus manual audits). Then test:
- Load a fresh private window from an EU IP and confirm only necessary cookies exist before you interact. Laravel's own
XSRF-TOKENand session cookies are functional and expected. - Reject, and verify no
_gaor_fbpappears and no server-side conversion jobs dispatch. - Accept, and confirm tags load and Consent Mode reports granted.
CookieBeam blocks unknown scripts by default and stores a timestamped consent record, so a Laravel app clears the same audit a purpose-built platform would. Run a full consent audit after any new package, and check the GDPR cookie compliance checklist before launch.