jeffersongoncalves/laravel-cookie-consent
A Laravel package for adding a customizable cookie consent banner to your site, helping you meet GDPR and CCPA requirements. Settings are stored in the database via spatie/laravel-settings and rendered with simple Blade includes.
1. Install and migrate — both steps are mandatory.
composer require jeffersongoncalves/laravel-cookie-consent
php artisan migrate
Unlike most banner packages, there is no config file. All 24 settings live in the database via spatie/laravel-settings (group cookie_consent), and the package auto-loads its own settings migrations from database/settings. Skip the migrate step and the banner will either not render or blow up resolving CookieConsentSettings — so this must be in your deploy script, not just your local setup.
2. Wire the two partials into your layout. Order matters — CSS in <head>, JS init at the end of <body>:
{{-- resources/views/layouts/app.blade.php --}}
<head>
...
@include('cookie-consent::cookie-consent-head')
</head>
<body>
{{ $slot }}
@include('cookie-consent::cookie-consent-body')
</body>
That's the whole integration. If your app has multiple layouts (marketing, auth, app shell), each one needs both includes — a banner that appears on the marketing site but not in the authenticated app is an audit finding waiting to happen.
3. Where to look first. There are only four things that matter:
| Path | Why you care |
|---|---|
resources/views/cookie-consent-head.blade.php |
CSS <link> from css_url |
resources/views/cookie-consent-body.blade.php |
The window.cookieconsent.initialise({...}) call built from settings |
src/Settings/CookieConsentSettings.php |
Typed properties; source of truth |
src/helpers.php |
cookie_consent_settings() global helper |
Publish the views before you start tweaking, so you have something concrete to diff against:
php artisan vendor:publish --tag=cookie-consent-views
Only publish the settings migrations (--tag=cookie-consent-settings-migrations) if you actually need to alter defaults for every environment — see Gotchas.
4. First real use case. The defaults ship a banner with a "Learn more" link whose content_href is null — i.e. a compliance banner with no path to your policy. Fix that immediately, plus the legal entity's copy, from a seeder/console command or a one-off tinker session:
$settings = cookie_consent_settings();
$settings->content_href = route('legal.cookies'); // or '/privacy'
$settings->content_message = 'We use cookies to analyse traffic and improve your experience.';
$settings->content_policy = 'Cookie Policy';
$settings->position = 'bottom-right';
$settings->popup_background = '#111827';
$settings->highlight_background = '#f8e71c';
$settings->save();
Note save() — settings are a persisted object, not a config array. Nothing changes until you call it.
The v2 rewrite moved everything into the database specifically so settings can change at runtime. The idiomatic pattern is a settings page (Filament, Nova, plain Blade form) that does exactly what the snippet above does. Keep it out of config/ and out of .env — there is no config file to put it in anyway.
// app/Filament/Pages/CookieConsentSettingsPage.php (sketch)
protected function mutateFormDataBeforeSave(array $data): array
{
$settings = cookie_consent_settings();
foreach ($data as $key => $value) {
$settings->{$key} = $value;
}
$settings->save();
return $data;
}
Validate aggressively here: position and theme are passed straight into Osano's initialiser and only accept its enumerated values (top-left, top-right, bottom-left, bottom-right; block, edgeless, classic). A typo in an admin form silently produces an unstyled or mispositioned banner.
This is the single most important integration point, and the README doesn't spell it out: the package renders a banner, it does not block anything. Osano CookieConsent v3 writes a cookieconsent_status cookie with allow, deny, or dismiss. Your analytics/marketing tags must check it themselves:
<script>
(function () {
var ok = document.cookie.split('; ').some(function (c) {
var [k, v] = c.split('=');
return k === 'cookieconsent_status' && (v === 'allow' || v === 'dismiss');
});
if (!ok) return;
// load GA4 / Pixel / Hotjar here, or inject the script tag
})();
</script>
Because v3 fires no callbacks through a Laravel-friendly API, the pragmatic pattern is to hook Osano's own event and re-run your loader:
window.cookieconsent.initialise({ onStatusChange: function () { location.reload(); } });
A reload after consent is crude but bulletproof and avoids re-plumbing every tag loader. For a heavier marketing stack, push to dataLayer instead and let GTM's consent triggers do the work.
The defaults point at cdn.jsdelivr.net. That's a third-party request fired before the user has consented, which some DPOs object to, and it complicates a strict CSP. Override both URLs to local copies:
$settings->css_url = asset('vendor/cookieconsent/cookieconsent.min.css');
$settings->js_url = asset('vendor/cookieconsent/cookieconsent.min.js');
$settings->save();
Copy the two files from the npm package (cookieconsent@3) into public/vendor/cookieconsent/. This also removes a runtime dependency on jsDelivr availability — your compliance banner should never be the thing that fails to load.
Settings resolution goes through spatie's settings cache, so reading cookie_consent_settings() in a Blade view is cheap. But since the partials are rendered per-request into your layout, a full-page cache keyed only on URL will serve a stale banner config after an admin change. Either exclude pages containing the banner from page cache, or bust the cache from your settings page's save handler.
For reproducible deploys, drive the settings from a seeder rather than clicking through an admin panel in each environment:
// database/seeders/CookieConsentSettingsSeeder.php
public function run(): void
{
$s = cookie_consent_settings();
$s->content_href = config('app.url') . '/legal/cookies';
$s->content_message = __('consent.message');
$s->position = 'bottom-right';
$s->save();
}
Call it from your deploy script after php artisan migrate. Keep it idempotent (assign, then save()).
CookieConsentSettings has a single fixed group (cookie_consent), so there is exactly one banner configuration per database. If you run several brands or tenants off one app, you'll need to either extend the settings class with a dynamic group name per tenant, or resolve the copy at render time by overriding the published views to read from a tenant-aware source. Plan for this early — retrofitting it after the views are published and edited is annoying.
Laravel 11 is gone. v4.0.0 requires laravel/framework: ^12.61.1|^13.12.0, specifically to pull in fixes for GHSA-crmm-hgp2-wgrp and GHSA-5vg9-5847-vvmq. If you're pinned to L11 for other reasons, you're stuck on v3.x and missing those patches. Audit this before composer update surprises you mid-sprint.
Editing the settings table by hand doesn't take effect. Spatie caches the settings object. Writing SQL directly against the settings table requires:
php artisan settings:clear-cache
Always prefer $settings->save() from code. This is the #1 source of "I changed it but the banner is the same" confusion.
Publishing views is a one-way door for that file. Once cookie-consent-views is published, package updates to those Blade files stop reaching you. Pin the package major version when you publish, and diff the upstream views on upgrade. Same for the settings migrations — if you publish them, later versions that add properties (v3.0.0 added eight content_* fields) won't auto-migrate.
XSS surface in the content settings. content_close defaults to the raw HTML entity ❌ (a ❌ glyph), which strongly implies the body view emits content unescaped into the JS config. Any admin-editable content field — content_header, content_message, content_link — is therefore a stored-XSS vector if untrusted users can reach the settings page. Restrict that page to super-admins, or sanitise with strip_tags/Purifier before assigning.
content_href defaults to null. An unlinked "Learn more" is a common GDPR/CCPA finding. Set it, and also decide whether you want content_target = '_blank' (the default) — linking to your own policy in a new tab is unnecessary and hurts nothing but UX.
Nothing is recorded server-side. The package gives you a banner and a browser cookie, full stop. "Prove this user consented, and to what version of the policy" requires your own table plus a POST from the consent callback. If your compliance posture demands an audit trail, budget for that work separately — don't assume the package covers it.
No category granularity. You get dismiss / allow / deny only. There is no "necessary vs. functional vs. marketing" toggle set, and no way to re-open preferences without the user clearing cookies or you adding a "Manage cookies" button that calls window.cookieconsent.show(). If your cookie policy lists multiple categories, the banner and the policy will contradict each other. Either simplify the policy or swap to an Osano v4-style config with categories.
Position/theme values are unvalidated pass-throughs. A position of bottom (rather than bottom-left) is silently ignored by Osano and the banner sticks to its default position. Add an in: validation rule on any admin form.
Debugging a banner that won't appear:
view-source, not DevTools' inspector).settings table has a cookie_consent group row — if migrations never ran, the settings class throws on resolve.cookieconsent_status cookie; once set, the banner is suppressed by design. Test in a private window.script-src/style-src — the CDN defaults are the usual culprit, and the failure is silent in the console unless you're watching.js_url 404s, window.cookieconsent is undefined and nothing renders.Extension points, ranked by durability. Overriding the two published Blade views is the only real extension seam — there's no event system, no middleware, no manager class to swap. That means: keep your edits in a thin wrapper (e.g. a custom x-cookie-consent Blade component in your app that includes the package partial and layers your own JS on top) rather than heavily editing the vendor files. That way a package major bump is a 5-minute diff, not a rewrite.
Testing. The package's own suite is composer test; in your app, add a smoke test that renders a page containing the layout and asserts the banner markup is present, and a settings test that assigns + saves + re-resolves to catch cache/group regressions. Use RefreshDatabase and make sure your test suite runs the package's database/settings migrations before any view rendering happens — otherwise every layout-touching feature test fails for a non-obvious reason.
How can I help you explore Laravel packages today?