Examples
Basic Setup
Minimal configuration to get started:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
</head>
<body>
<h1>My Website</h1>
</body>
</html>
With Configuration
Common configuration options:
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.ZestConfig = {
position: 'bottom-right',
theme: 'auto',
accentColor: '#0071e3',
policyUrl: '/privacy-policy',
mode: 'safe'
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
</head>
<body>
<h1>My Website</h1>
</body>
</html>
Google Analytics 4
Block GA4 until consent:
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.ZestConfig = {
mode: 'safe',
callbacks: {
onAccept: (consent) => {
if (consent.analytics) {
// GA4 initialization
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
}
}
}
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
<!-- This will be auto-blocked in safe mode -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
</head>
<body>
<h1>My Website</h1>
</body>
</html>
Google Consent Mode v2
Integrate with Google Consent Mode v2 so Google tags automatically respect user consent:
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.ZestConfig = {
mode: 'safe',
consentModeGoogle: true,
policyUrl: '/privacy-policy'
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
<!-- GA4 / GTM - blocked until consent, then Consent Mode signals are sent automatically -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
</head>
<body>
<h1>My Website</h1>
</body>
</html>
Zest will automatically:
- Push a
'default'denied state for all consent types on initialization - Push an
'update'signal whenever the user changes their preferences
The following Google Consent Mode signals are mapped:
| Google Signal | Zest Category |
|---|---|
ad_storage |
marketing |
ad_user_data |
marketing |
ad_personalization |
marketing |
analytics_storage |
analytics |
functionality_storage |
Always 'granted' (essential) |
personalization_storage |
functional |
Microsoft UET Consent Mode
Integrate with Microsoft UET Consent Mode for Microsoft Advertising:
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.ZestConfig = {
mode: 'safe',
consentModeMicrosoft: true
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
</head>
<body>
<h1>My Website</h1>
</body>
</html>
Microsoft UET maps ad_storage to the Zest marketing category.
Both Consent Modes Together
You can enable both Google and Microsoft consent modes simultaneously:
<script>
window.ZestConfig = {
mode: 'safe',
consentModeGoogle: true,
consentModeMicrosoft: true
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
Or via data attributes:
<script
src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"
data-consent-mode-google
data-consent-mode-microsoft
></script>
Google Tag Manager
Block GTM until consent:
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.ZestConfig = {
mode: 'safe'
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
<!-- GTM will be blocked until analytics consent -->
<script data-consent-category="analytics">
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXX');
</script>
</head>
<body>
<noscript>
<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
<h1>My Website</h1>
</body>
</html>
Facebook Pixel
Block Facebook Pixel until marketing consent:
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.ZestConfig = {
mode: 'safe'
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
<!-- Facebook Pixel - blocked by default in safe mode -->
<script data-consent-category="marketing">
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'PIXEL_ID');
fbq('track', 'PageView');
</script>
</head>
<body>
<h1>My Website</h1>
</body>
</html>
Multi-language Site
Auto-detect language from HTML:
<!DOCTYPE html>
<html lang="de">
<head>
<script>
window.ZestConfig = {
lang: 'auto' // Will use 'de' from html lang attribute
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
</head>
<body>
<h1>Meine Webseite</h1>
</body>
</html>
Or specify explicitly:
<script>
window.ZestConfig = {
lang: 'fr' // Force French
};
</script>
Cookie Preferences Button
Add a footer link to reopen settings:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
</head>
<body>
<main>
<h1>My Website</h1>
</main>
<footer>
<a href="#" onclick="Zest.showSettings(); return false;">
Cookie Settings
</a>
<a href="/privacy-policy">Privacy Policy</a>
</footer>
</body>
</html>
Custom Styling
Match your brand:
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.ZestConfig = {
theme: 'dark',
accentColor: '#10b981',
position: 'bottom-left',
customStyles: `
.zest-banner {
border-radius: 16px;
margin: 16px;
max-width: 400px;
}
.zest-btn--primary {
border-radius: 9999px;
}
`
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
</head>
<body>
<h1>My Website</h1>
</body>
</html>
Consent-Based Feature Toggle
Show/hide features based on consent:
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.ZestConfig = {
callbacks: {
onReady: updateUI,
onChange: updateUI
}
};
function updateUI(consent) {
const chatWidget = document.getElementById('chat-widget');
const analyticsInfo = document.getElementById('analytics-info');
if (consent.functional) {
chatWidget.style.display = 'block';
} else {
chatWidget.style.display = 'none';
}
if (consent.analytics) {
analyticsInfo.textContent = 'Analytics enabled';
} else {
analyticsInfo.textContent = 'Analytics disabled';
}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
</head>
<body>
<h1>My Website</h1>
<p id="analytics-info">Loading...</p>
<div id="chat-widget" style="display: none;">Chat Widget Here</div>
</body>
</html>
Strict Privacy Mode
Block all trackers and respect DNT:
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.ZestConfig = {
mode: 'strict',
respectDNT: true,
dntBehavior: 'reject',
showWidget: true
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
</head>
<body>
<h1>Privacy-First Website</h1>
</body>
</html>
Geo Targeting
Since v2.4.0.
Show the banner only where the law requires it — opt-in banner for GDPR regions, a “Do Not Sell” link for US state-privacy, nothing elsewhere:
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.ZestConfig = {
geo: true, // hosted gateway — that's it
policyUrl: '/privacy-policy'
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
</head>
<body>
<h1>My Website</h1>
</body>
</html>
Already know the country at the edge? Skip the gateway round-trip with a custom resolver:
<script>
window.ZestConfig = {
geo: {
resolver: async () => {
// e.g. your server rendered the CDN geo header into the page
const country = document.documentElement.dataset.country;
const eea = ['AT','BE','BG','HR','CY','CZ','DK','EE','FI','FR','DE','GR','HU','IE','IT','LV','LT','LU','MT','NL','PL','PT','RO','SK','SI','ES','SE','IS','LI','NO'];
const isGDPR = eea.includes(country) || country === 'GB';
return {
isEU: eea.includes(country),
isEEA: eea.includes(country),
isGDPR,
isCCPA: country === 'US',
isUSPrivacy: country === 'US',
regulations: isGDPR ? ['gdpr'] : country === 'US' ? ['ccpa'] : []
};
},
fallback: 'consent' // fail closed if the resolver throws
}
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
See the Geo Targeting page for the full action vocabulary and self-hosting options.
Astro Integration
Since v2.4.0.
The official @freshjuice/zest-astro integration inlines Zest into <head> at build time:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import zest from '@freshjuice/zest-astro';
export default defineConfig({
integrations: [
zest({
language: 'en',
config: {
theme: 'auto',
position: 'bottom-right',
accentColor: '#0071e3',
policyUrl: '/privacy',
consentModeGoogle: true,
geo: true
}
})
]
});
Eleventy (11ty) Integration
Since v2.4.0.
The official @freshjuice/zest-eleventy plugin auto-injects Zest into every rendered page:
// eleventy.config.js
import zest from '@freshjuice/zest-eleventy';
export default function (eleventyConfig) {
eleventyConfig.addPlugin(zest, {
language: 'en',
config: {
theme: 'auto',
position: 'bottom-right',
policyUrl: '/privacy'
}
});
}
Prefer placing the script yourself? Disable the auto-transform and use the shortcode:
eleventyConfig.addPlugin(zest, {
autoInject: false,
config: { /* … */ }
});
{# in your base layout #}
<head>
<meta charset="utf-8">
{% zest %}
{# ...other head tags... #}
</head>
Manual Initialization
Control when Zest initializes:
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.ZestConfig = {
autoInit: false
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
</head>
<body>
<h1>My Website</h1>
<script>
// Initialize later, e.g., after user interaction
document.getElementById('start-btn').addEventListener('click', () => {
Zest.init();
});
</script>
<button id="start-btn">Enter Site</button>
</body>
</html>
Headless (bring your own UI)
Since v2.0.0, Zest ships a headless entry that runs the consent engine without any built-in UI, translations, or auto-init. Perfect when you want to render the banner in your own framework with your own CSS.
import Zest from '@freshjuice/zest/headless';
// Initialize explicitly — no auto-init, no window.Zest
Zest.init({
mode: 'safe',
respectDNT: true,
policyUrl: '/privacy-policy'
});
// React to consent changes with the subscriber API
Zest.on('change', (consent) => {
renderMyBanner(consent);
});
// Later, when your custom form submits:
function handleSave(formData) {
Zest.updateConsent({
analytics: formData.has('analytics'),
marketing: formData.has('marketing'),
functional: formData.has('functional')
});
}
The headless build is ESM-only (~16 KB gzipped) and exposes the same API as the full build except for show / hide / showSettings / hideSettings — you render your own surface and call Zest.updateConsent() / Zest.acceptAll() / Zest.rejectAll() from your own handlers.
React example
import { useEffect, useState } from 'react';
import Zest from '@freshjuice/zest/headless';
function ConsentBanner() {
const [consent, setConsent] = useState(null);
const [decided, setDecided] = useState(false);
useEffect(() => {
Zest.init({ mode: 'safe' });
setConsent(Zest.getConsent());
setDecided(Zest.hasConsentDecision());
const off = Zest.on('change', (next) => {
setConsent(next);
setDecided(true);
});
return off;
}, []);
if (decided) return null;
return (
<div className="my-banner">
<p>We use cookies. <a href="/privacy-policy">Privacy policy</a>.</p>
<button onClick={() => Zest.acceptAll()}>Accept all</button>
<button onClick={() => Zest.rejectAll()}>Reject all</button>
<button onClick={() => Zest.updateConsent({ analytics: true })}>
Analytics only
</button>
</div>
);
}
SPA Integration (React/Vue/etc)
Initialize once at your app shell: the headless entry is the natural fit (the full build also works if you want Zest’s own banner). Interceptors patch globals (document.cookie, fetch, script insertion), so they survive every route change; nothing re-runs per view and you don’t need to touch your router.
// App shell: runs once, not per route
import Zest from '@freshjuice/zest/headless';
Zest.init({ mode: 'safe', respectDNT: true });
The consent cookie is read on init() and on every change event, so client-side navigation needs no extra wiring:
import { useEffect, useState } from 'react';
function useConsent() {
const [consent, setConsent] = useState(Zest.getConsent());
useEffect(() => {
const off = Zest.on('change', setConsent);
return off; // Zest.on returns an unsubscribe function
}, []);
return consent;
}
function MyBanner() {
const consent = useConsent();
// Route changes don't re-show the banner: a decision exists.
if (Zest.hasConsentDecision()) return null;
return (
<div className="my-banner">
<button onClick={() => Zest.acceptAll()}>Accept all</button>
<button onClick={() => Zest.rejectAll()}>Reject all</button>
</div>
);
}
Two things people ask about:
- StrictMode double-mount:
init()is idempotent. The second call returns the current state and installs nothing. - “Cookie Preferences” link: call
Zest.showSettings()(full build) or open your own settings panel (headless); no special SPA handling needed.
For the full headless wiring pattern see Headless.