Age Gate
A site-wide, reusable age verification gate: every Brown-Forman liquor brand needs one, so it’s designed as a shared package rather than a one-off for Herradura.
Reference: how herradura.com actually does it
We inspected the live site directly rather than guessing at requirements: filled the form, cleared cookies and retried, watched every network request fire, and fetched raw HTML with a plain browser user agent. Confirmed:
- Full date-of-birth entry (separate MM / DD / YYYY fields) plus a country selector covering essentially every country: legal drinking age varies by market, and the gate is genuinely global. Same mechanism we found on jackdaniels.com (see below), so this is a real Brown-Forman-wide pattern, not one brand’s choice.
- No dedicated “verify age” API call. Cleared cookies, refilled the form, watched every request fire on submit: nothing posts the DOB anywhere. Validated entirely client-side against the selected country’s legal age.
- Legal/privacy links point to a shared corporate domain:
legal.brown-forman.com/terms-of-use,/privacy-policy; centralized across brands. We link to it, we don’t write our own. - Crawler-safe by construction. The underlying content is present in the raw HTML response regardless of gate state: the gate is a client-side visual overlay, not server-side content withholding. Confirmed on both brand sites.
Don’t reach for middleware.ts-based redirects or route blocking to enforce the
gate. That requires detecting bot user-agents to exempt them from the block, which is
spoofable and edges toward cloaking, something Google explicitly discourages. The
overlay approach below sidesteps the problem instead of managing it.
Where Herradura’s implementation actually differs from Jack Daniel’s
Two real Brown-Forman brand sites, same underlying requirement, genuinely different execution, worth designing our package to support both rather than assuming one is “the” pattern:
| herradura.com | jackdaniels.com | |
|---|---|---|
| Layout | Split-screen: lifestyle photography left, form right | Full-screen centered, logo-only background |
| Fields | Labeled (“Birthdate*”, “Location*”) | Placeholder-only, no visible labels |
| Persistence | Opt-in: a “Remember me” checkbox controls whether the cookie survives the session | Automatic: no visible opt-in, persists regardless |
| Consent | Explicit checkbox tied to terms/privacy | Disclaimer text only, no checkbox |
| Headline copy | “Please verify your age” | “Jack’s birthdate has always been a mystery. Hopefully yours isn’t.” (brand voice) |
The “Remember me” difference matters functionally, not just visually: if it’s genuinely opt-in and unchecked by default, a visitor who declines it gets re-gated on every new session even after passing once. Confirm this is the intended behavior (not a bug in the WordPress plugin) before treating it as a requirement: a P1-native rebuild should decide deliberately whether persistence is automatic or opt-in, not inherit an assumption from either site.
Approach
A root-layout client component, published as @brown-forman/p1-age-gate, built to
support both patterns above via config rather than picking one:
// app/layout.tsx: the one file that wraps every route automatically
import { AgeGate } from "@brown-forman/p1-age-gate";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AgeGate
brand={{ name: "Herradura", logo: "/images/herradura-seal.svg" }}
layout="split" // "split" (Herradura) | "centered" (Jack Daniel's)
persistence="opt-in" // "opt-in" (Herradura) | "automatic" (Jack Daniel's)
legalLinks={{
terms: "https://legal.brown-forman.com/terms-of-use/english",
privacy: "https://legal.brown-forman.com/privacy-policy/english",
}}
/>
{children}
</body>
</html>
);
}- Renders full-screen when unverified,
nullonce a cookie confirms verification. - No page-level or middleware wiring needed; every route gets it automatically.
- Content underneath is always server-rendered and present in the HTML: the overlay is a presentation-layer block, not a data-layer one.
Config shape
interface AgeGateConfig {
brand: { name: string; logo?: string };
layout?: "split" | "centered"; // default: "split"
persistence?: "opt-in" | "automatic"; // default: "opt-in"
legalLinks: { terms: string; privacy: string };
/** ISO 3166-1 alpha-2 → minimum age. Falls back to `default` for unlisted countries. */
minimumAgeByCountry: Record<string, number> & { default: number };
/** Countries offered in the selector; defaults to a full list like the reference sites. */
countries?: string[];
cookieName?: string; // default: "age_verified"
cookieMaxAgeDays?: number; // default: 365
}minimumAgeByCountry exists because legal drinking age isn’t universally 21:
confirmed directly on herradura.com’s own Visit Us page,
which states “Guests must be 18 years of age or older” for the Mexico-based distillery
tour (Mexico’s legal drinking age is 18). A brand operating only in the US can pass
just { default: 21 }; a brand with real multi-market traffic (like Herradura,
given the tour requirement) needs the full map.
Open questions for this page now live on Outstanding Questions, tracked centrally across all pages rather than repeated per page.