freshheads/cookie-guard-bundle
Symfony bundle from Freshheads that adds cookie consent guarding to your app, helping ensure cookies and related scripts only load when visitors permit. Simple Composer install and configuration for PHP projects.
Read this first: this is a Symfony bundle, not a Laravel package. There is no service provider, no config publish tag, and Composer will not give you anything usable in a Laravel app. What you actually get from freshheads/cookie-guard-bundle is a pattern plus two small pieces of code worth porting:
src/ — a Twig extension that wraps template blocks so their contents are emitted as <script type="text/plain" data-cookie-guard="…"> instead of executable script tags.Resources/public/js/ — a small vanilla-JS runtime that reads the consent cookie and, on acceptance, clones each inert script back into the DOM as a real text/javascript node.The repo was archived in Nov 2022 and has zero dependents, so treat it as reference source, not a dependency. Clone it, read those two files, then reimplement ~60 lines in Blade + a JS module.
Minimal steps in a Laravel app:
git clone https://github.com/freshheads/FHCookieGuardBundle /tmp/cgb
# read: src/*Extension*.php and Resources/public/js/*
Then:
resources/js/cookie-guard.js and import it from app.js.cookie_guard=analytics,media).First real use case: a YouTube embed and a Google Analytics snippet that must not fire until the visitor clicks "Accept". Wrap both, verify in DevTools that no request leaves the browser before consent.
// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Blade;
Blade::directive('cookieGuard', fn ($group) =>
"<?php \$__g = $group; ?><script type=\"text/plain\" data-cookie-guard=\"<?= e(\$__g) ?>\">");
Blade::directive('endCookieGuard', fn () => '</script>');
@cookieGuard('analytics')
ga('create', 'UA-XXXX', 'auto');
@endCookieGuard
Server-render the placeholder, activate client-side. The whole trick is the type="text/plain" attribute: the browser parses the tag but never executes it. The moment consent arrives, you rebuild it. Keep the source-of-truth consent in a cookie so a page reload doesn't flash ungated content.
The activation loop (port of the bundle's runtime):
// resources/js/cookie-guard.js
const COOKIE_NAME = 'cookie_guard';
export function acceptedGroups() {
const entry = document.cookie.split('; ').find(c => c.startsWith(COOKIE_NAME + '='));
return entry ? decodeURIComponent(entry.split('=')[1]).split(',') : [];
}
export function activateGroups(groups = acceptedGroups(), scope = document) {
scope.querySelectorAll('script[type="text/plain"][data-cookie-guard]').forEach(old => {
if (!groups.includes(old.dataset.cookieGuard)) return;
const s = document.createElement('script');
[...old.attributes].forEach(a => {
if (['type', 'data-cookie-guard'].includes(a.name)) return;
s.setAttribute(a.name, a.value);
});
s.text = old.text;
s.async = false; // preserve execution order for `src` scripts
old.replaceWith(s);
});
}
Non-script embeds. Iframes and images don't need type juggling — just defer src:
<iframe data-cookie-guard="media"
data-src="https://www.youtube.com/embed/{{ $video }}"
title="Video" loading="lazy"></iframe>
document.querySelectorAll('iframe[data-cookie-guard]').forEach(el => {
if (acceptedGroups().includes(el.dataset.cookieGuard)) el.src = el.dataset.src;
});
Wire it to the banner, not to page load only. Call activateGroups() once on DOMContentLoaded (returning visitors with the cookie already set get their scripts immediately) and again after the user accepts, so nothing requires a reload. Dispatch a custom event from your banner and listen for it:
document.addEventListener('cookie-guard:changed', () => activateGroups());
Group naming mirrors your consent categories, not your vendors: analytics, marketing, media. If your banner only has a single yes/no, one group (all) is enough — the bundle's config in DependencyInjection/Configuration.php shows the cookie name and lifetime as the only knobs, so there's no hidden state.
Testing. Set the cookie in a feature test and assert the markup, and separately assert the absence of an executable tag:
$this->withCookie('cookie_guard', 'analytics')->get('/')
->assertSee('data-cookie-guard="analytics"', false)
->assertDontSee('<script src="https://www.googletagmanager.com', false);
Note this proves the markup, not the runtime — the actual gating is browser behaviour, so cover the JS with a Dusk/Browser test or a Jest test on activateGroups against a jsdom fixture.
composer require it into a Laravel app expecting it to work; vendor-copy the logic and attribute it (MIT).script-src, the cloned script node loses the nonce (the bundle never handled this). Copy the nonce attribute across when rebuilding, otherwise consent silently fails with a console CSP error and no network request.document.write, SPA route changes) bypasses the guard entirely. Either route all third-party loading through activateGroups() or add a MutationObserver on head.<script src> tags default to async, so two guarded scripts that depend on each other may run out of order. Set s.async = false (as above) and keep dependent code inside the same group.document.write inline snippets (older ad/analytics tags) cannot be re-injected after parsing and will corrupt or blank the page. Rewrite them to the modern async form before guarding.<meta>, structured data, and above-the-fold content outside the guarded block, and keep a text fallback/link inside the embed placeholder.Secure, SameSite=Lax, and a lifetime that matches your consent-renewal policy (commonly re-ask every 6–12 months). Path must be / or returning visitors on other routes lose consent and re-prompt.type="text/plain"? does document.cookie actually contain the group? does the group string match exactly (case, whitespace, URL-encoding)? did the clone get a nonce? anything in the console about CSP or Refused to execute?localStorage or an off-the-shelf consent manager should be a one-file change.How can I help you explore Laravel packages today?