Getting Started

Install and set up Zest cookie consent on your website

Installation

The easiest way to add Zest to your website is via CDN:

<!-- jsDelivr (full bundle with all 12 languages) -->
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>

<!-- or unpkg -->
<script src="https://unpkg.com/@freshjuice/zest"></script>

Single Language Bundle

For smaller bundle size (~10KB vs ~16KB gzipped), use a single-language build:

<!-- English only -->
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest/dist/zest.en.min.js"></script>

<!-- German only -->
<script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest/dist/zest.de.min.js"></script>

<!-- Available: en, de, es, fr, it, pt, nl, pl, uk, ru, ja, zh -->

npm (full build)

npm install @freshjuice/zest
import '@freshjuice/zest';

The full build auto-initializes synchronously when the bundle is evaluated and exposes window.Zest. Interceptors (cookies, storage, scripts, network) install immediately so that any later defer or async tracker script is already gated. The UI mount (banner / widget) is deferred until <body> is available.

npm (headless — bring your own UI)

If you want to run the consent engine without Zest's built-in UI (e.g. to render your own banner in React/Vue), use the headless entry:

npm install @freshjuice/zest
import Zest from '@freshjuice/zest/headless';

Zest.init({
  mode: 'safe',
  callbacks: {
    onAccept: (consent) => console.log('Accepted:', consent)
  }
});

The headless build (~11KB gzipped) ships without the Shadow DOM UI, without translations, and does not auto-initialize or set window.Zest. You call Zest.init(config) explicitly and render your own consent surface. See the Examples page for a full walkthrough.

Framework Integrations (Astro, Eleventy)

Since v2.4.0.

Official plugins inject the Zest IIFE inline into <head> at build time, so interceptors install before any other script — with no extra HTTP request. Pass runtime config (including geo: true) straight through.

Package Framework
@freshjuice/zest-astro Astro 3 / 4 / 5 / 6
@freshjuice/zest-eleventy Eleventy (11ty) 2+

@freshjuice/zest is a peer dependency of both plugins — install it alongside.

Astro

npm install @freshjuice/zest @freshjuice/zest-astro
// 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',
        policyUrl: '/privacy'
      }
    })
  ]
});

Options: enabled (default true), devMode (inject during astro dev, default true), language ('all' for the multilingual bundle or a single language code for a smaller one), and config (runtime Zest config, serialized to window.ZestConfig).

Eleventy (11ty)

npm install @freshjuice/zest @freshjuice/zest-eleventy
// eleventy.config.js (ESM, Eleventy 3+)
import zest from '@freshjuice/zest-eleventy';

export default function (eleventyConfig) {
  eleventyConfig.addPlugin(zest, {
    language: 'en',
    config: {
      theme: 'auto',
      position: 'bottom-right',
      policyUrl: '/privacy'
    }
  });
}

CommonJS works too: const zest = require('@freshjuice/zest-eleventy'). The plugin injects <script id="zest-consent">…</script> just before </head> on every .html output. Prefer manual placement? Pass autoInject: false and drop the {% zest %} shortcode into your base layout's <head>.

Options: enabled (default true), autoInject (default true), shortcode (name for manual placement, default 'zest'), language, and config — same semantics as the Astro integration.

Note: plugin config is serialized to an inline window.ZestConfig, so function-valued options (geo.resolver, geo.decide, callbacks) don't survive serialization — set those in your own client-side JS instead.

Basic Setup

Add the script before any tracking scripts you want to block:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My Website</title>

  <!-- Zest configuration (optional) -->
  <script>
    window.ZestConfig = {
      policyUrl: '/privacy-policy'
    };
  </script>

  <!-- Load Zest BEFORE tracking scripts -->
  <script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>

  <!-- These will be auto-blocked until consent -->
  <script async src="https://www.googletagmanager.com/gtag/js?id=GA-XXXXX"></script>
</head>
<body>
  <!-- Your content -->
</body>
</html>

How It Works

  1. On page load, Zest checks if the user has already made a consent decision
  2. If no decision, the consent banner is displayed
  3. Known tracking scripts are automatically blocked (in safe mode or higher)
  4. When user consents, blocked scripts are executed and cookie/storage operations are replayed
  5. A floating widget appears so users can change their preferences later

Configuration via Data Attributes

You can also configure Zest using data attributes on the script tag:

<script
  src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"
  data-position="bottom-right"
  data-theme="dark"
  data-accent-color="#ff6b35"
  data-policy-url="/privacy"
></script>

Zest uses four consent categories:

Category Default Description
Essential Always ON Strictly-necessary storage that cannot be disabled — login sessions, security tokens, shopping cart, the consent decision itself, and preferences the user has explicitly set on this site (language switcher, theme toggle). Exempt from consent under ePrivacy Art. 5(3).
Functional OFF Optional comfort features that enhance the experience without being strictly necessary — live-chat widgets, embedded video player preferences, third-party comment systems, recently-viewed lists.
Analytics OFF Usage tracking (Google Analytics, Plausible, etc.).
Marketing OFF Advertising and remarketing (Facebook Pixel, Google Ads, etc.).

Why language/theme aren't "Functional". A common mistake is to bucket UI-language and theme toggles under the optional functional category. If the user actively chose them on this site (clicked the language switcher, flipped to dark mode), they qualify as strictly-necessary under ePrivacy Art. 5(3) — the user explicitly requested that behaviour. Don't ask consent for honouring it. Reserve the functional toggle for things that are genuinely optional comfort, like a live-chat embed loaded from a third-party origin.

Next Steps