Building a GDPR-Compliant Cookie Consent System with Sanity CMS
Last updated: 30.12.2025
Building a GDPR-Compliant Cookie Consent System with Sanity CMS
Audio Narration

The author decided to build a custom cookie consent solution for their blog to avoid the complexities and privacy issues of third-party libraries. Their solution is fully GDPR-compliant, privacy-first, and integrates with Sanity CMS. The custom implementation includes three main components: a Sanity CMS schema for managing consent content, a React Context for state management, and conditional loading of analytics based on user consent. The system emphasizes accessibility, server-side data fetching, and smart reload logic to ensure a seamless user experience. The cookie banner is designed to be editor-friendly, allowing non-technical users to update content without code changes. The result is a lightweight, under 5KB, GDPR-compliant system that respects user privacy by default, loads no third-party tracking libraries, and provides full transparency with a detailed policy page. Users can withdraw consent at any time, and the system is fully accessible, making it an ideal solution for personal blogs or small sites.
When I started thinking about adding analytics to my blog, I quickly realized that most cookie consent solutions felt like overkill. Third-party libraries added unnecessary weight, came with their own tracking scripts, and often lacked the flexibility I wanted. So I decided to build my own—fully GDPR-compliant, privacy-first, and powered by Sanity CMS.
Why Build Your Own?
The problem with most cookie consent libraries is that they're designed for complex enterprise scenarios. For a personal blog that only uses privacy-respecting analytics (Vercel Analytics in my case), I needed something simpler:
- No third-party scripts - Why add another tracker to manage trackers?
- Full control - I wanted to understand exactly what was happening
- Content management - Legal text should be editable without code changes
- Privacy by default - No cookies until explicit consent
The Architecture
My implementation has three main components:
1. Sanity CMS as the Single Source of Truth
I created a cookieConsent singleton schema in Sanity that stores:
- Banner text (what users see)
- Accept/Reject button labels
- Full cookie policy content (as Portable Text)
- Last updated timestamp
This means my content team (okay, just me) can update the consent text, policy details, or button labels directly in the Sanity Studio—no code deployment needed.
2. React Context for State Management
A simple CookieConsentProvider handles:
- Reading consent state from
localStorage - Providing accept/reject/withdraw methods
- Preventing hydration mismatches with an
isMountedflag
The key insight here was to initialize state as null and read from localStorage in useEffect to avoid the flash-of-content issue during SSR.
3. Conditional Analytics Loading
The ConditionalAnalytics component only renders Vercel Analytics and SpeedInsights when consent === 'accepted'. If consent is rejected or not given, nothing loads. Simple as that.
Key Implementation Details
Accessibility First
The cookie banner includes proper ARIA attributes:
role="dialog"andaria-modal="true"for screen readersaria-labelattributes on all buttons- Semantic HTML throughout
Server-Side Data Fetching
Cookie consent sanity data is fetched server-side in the Next.js layout and passed as props to the banner component. This keeps the banner itself lightweight and ensures no client-side Sanity queries.
Smart Reload Logic
When transitioning from "accepted" to "rejected," I reload the page to clear any loaded analytics scripts. But rejecting when you haven't accepted yet? No reload needed—nothing to clear.
No Flash on Load
The banner checks isMounted before rendering, ensuring it only appears client-side after localStorage is read. This eliminates the annoying flash where the banner appears briefly even when consent was already given.
Learn More Link
The banner includes a "Learn more" link to the full cookie policy page, rendered from Sanity's Portable Text. Users can make an informed decision without feeling pressured.
Footer Management
A "Manage Cookie Preferences" button in the footer lets users withdraw consent at any time—a GDPR requirement that's often overlooked.
Why This Approach Works
Full Control: I understand every line of code. No black boxes.
Privacy-Focused: No analytics load until the user explicitly accepts. No sneaky preconnections or preloads.
Editor-Friendly: Non-technical users can update the policy text, banner wording, or button labels through Sanity Studio.
Performance: No third-party library to download. The entire implementation is under 5KB.
The Result
A fully GDPR-compliant cookie consent system that:
- ✅ Respects user privacy by default
- ✅ Loads no third-party tracking libraries
- ✅ Provides full transparency with a detailed policy page
- ✅ Allows users to withdraw consent anytime
- ✅ Is fully accessible
- ✅ Can be updated by content editors without code changes
Building your own cookie consent isn't complicated—it just requires thinking through the user experience and legal requirements. For a personal blog or small site, a custom solution often makes more sense than pulling in a heavy library.

Securing My Blog's Secrets with Varlock and Doppler
The author describes the security risks of traditional .env files in Next.js projects: secrets stored in plain text on local machines, duplicated across environments, and easily exposed through accidental commits or misconfigured deployments. Varlock is introduced as a structured, schema-based replacement for dotenv, using a committed .env.schema file that defines variable types, requirements, and sensitivity without storing actual secrets. It integrates with Next.js as a drop-in replacement for @next/env, providing TypeScript types, startup validation, sensitivity-aware client bundling, and log redaction. Doppler is chosen as the secrets provider for its generous free tier, clean dashboard, environment configs, and especially its native Vercel sync, which keeps secrets updated without tokens or runtime API calls. Locally, Varlock’s exec() calls the Doppler CLI to fetch secrets in memory; in production, Vercel’s environment variables are used directly, with the same schema validating both paths. Benefits include zero secrets on disk, early configuration error detection, automatic sensitivity handling, and an AI-friendly schema. The author would skip Varlock’s Doppler plugin next time and rely solely on the CLI-based exec() approach despite a small startup performance cost.
Read post
Using the WAT Framework: Writing Sanity MCP Workflows That Make Claude Consistent and Reliable
The text explains why open-ended AI instructions like “write a blog post about TypeScript” lead to inconsistent results. Because models are probabilistic, they vary structure, miss fields, and overlook edge cases, which is problematic for repetitive, structured tasks such as publishing to a CMS. To solve this, it introduces the WAT framework: Workflows, Agents, Tools. Workflows are plain-language markdown SOPs that encode domain knowledge and specify steps. Agents (Claude) handle reasoning and decisions. Tools are deterministic scripts or APIs, like the Sanity MCP, that execute actions. This separation narrows the decision space and keeps behavior consistent across sessions. A concrete example is the draft_blog_post workflow, which fetches authors and categories from Sanity, requires outline approval, and strictly defines document shape and constraints, including a 5000-byte body limit. Workflows evolve through a self-improvement loop: each failure adds new rules and edge cases. To get started, you document repeatable tasks, inputs, tools, steps, and edge cases, store them in .claude/wat/workflows/, and reuse them for faster, cheaper, and more reliable AI-assisted work.
Read post