Third-Party Embeds & OneTrust Consent
This guide explains how we load third-party embed scripts (Instagram, Twitter/X, TikTok, etc.) while respecting user privacy consent managed by OneTrust, and documents the broader GDPR/CCPA compliance requirements for our site.
Why Consent Gating Exists
Section titled “Why Consent Gating Exists”In the EEA (European Economic Area) and UK, user consent is required before sites or third-party technology read or write information on a user’s device, unless the operation is strictly necessary for the requested service. “Strictly necessary” is narrowly defined and does not include marketing, advertising, analytics, or optional functionality.
Under GDPR, valid consent must be:
- Freely given — not linked to providing a service that doesn’t require consent
- Specific and informed — the user must understand what they are consenting to
- An affirmative act — consent cannot be inferred from continued browsing or pre-checked boxes
Consent must also be as easy to withdraw as it is to give.
Third-party embed vendors (Instagram, Twitter/X, TikTok) require loading external JavaScript that sets cookies and enables cross-site tracking, placing them squarely under these regulations. We use OneTrust as our Consent Management Platform (CMP) to gate these scripts behind user consent.
OneTrust Cookie Categories
Section titled “OneTrust Cookie Categories”OneTrust groups all cookies and scripts into consent categories. These category IDs must stay in sync across OneTrust, GTM, and Cohesion for consent to function properly.
| Category | Name | Description | Consent Required |
|---|---|---|---|
C0001 | Strictly Necessary | Essential for the site to function (privacy preferences, login, forms). Cannot be disabled by users. Does not store personally identifiable information. | No |
C0002 | Performance | Counts visits and traffic sources to measure site performance. All data is aggregated and anonymous. | Yes |
C0003 | Functional | Enables enhanced functionality and personalization. May be set by us or third-party providers. | Yes |
C0004 | Targeting/Advertising | Used by advertising partners to build interest profiles and show relevant ads. Based on uniquely identifying the browser/device. | Yes |
C0005 | Social Media | Set by social media services to enable content sharing. Can track browsers across sites and build interest profiles. Embed scripts fall in this group. | Yes |
Embed scripts from Instagram, Twitter/X, and TikTok fall under C0005 (Social Media) because they are social media services that enable content sharing and can track browsers across sites.
Cookie Scanning and Categorization
Section titled “Cookie Scanning and Categorization”The RV Privacy team runs periodic scans of our site that detect new and uncategorized cookies. They will reach out when uncategorized cookies appear, but we should proactively check scan results.
- Scan results:
https://app.onetrust.com/cookies/scan-results/.../reports - Categorization:
https://app.onetrust.com/cookies/categorizations(filter tolonelyplanet.com)
When categorizing, add descriptors to help track where and how cookies are added to our site.
To access the OneTrust admin panel, reach out to the RV Privacy Team (
#ask-privacy).
How OneTrust Works on Our Site
Section titled “How OneTrust Works on Our Site”Banner and Geolocation Rules
Section titled “Banner and Geolocation Rules”OneTrust serves different experiences based on the visitor’s location using geolocation rules:
| Region | Behavior | Default State |
|---|---|---|
| Global (Default) | Opt-out | No banner shown; users are opted into all cookies by default |
| EEA, UK, Norway, Iceland, Liechtenstein | Opt-in (GDPR) | Banner overlay shown; users must make an active consent selection |
Both regions provide a Cookie Settings link in the site footer that opens the OneTrust preference center, allowing users to toggle cookie categories at any time.
Script Loading Order
Section titled “Script Loading Order”- The Cohesion SDK script loads in
layout.astro, configured withconsent.onetrust.optIn: true— this initializes OneTrust - OneTrust fires first on all pages, checking which cookie groups have been accepted or denied
- When a user makes a consent choice, OneTrust populates
window.OnetrustActiveGroupswith a comma-separated string of accepted categories (e.g.",C0001,C0003,C0005,") - OneTrust calls
window.OptanonWrapper()whenever consent state changes - Cohesion reads the consent state and enables/disables its own products accordingly — if the consent mapping between OneTrust and Cohesion is not in sync, data will not flow properly to the data pipeline
Footer Requirements
Section titled “Footer Requirements”Every page must include these links in the footer:
- Cookie Settings — opens the OneTrust preference center (requires the OneTrust banner snippet in
<head>) - Privacy Policy — links to
https://www.lonelyplanet.com/legal/privacy-policy(maintained by the RV Privacy team) - Do Not Sell or Share My Personal Information — links to the OneTrust CCPA opt-out form
- ”, a Red Ventures Company” — common branding text next to the copyright mark (text only, required on all RV sites)
Consent Utilities — src/utils/consent/
Section titled “Consent Utilities — src/utils/consent/”Two tools are provided for consent-gated script loading. Both implement the same core logic but are designed for different rendering contexts.
consentGatedScripts.astro — for Astro templates
Section titled “consentGatedScripts.astro — for Astro templates”Use this when you are in an .astro file and need to load third-party scripts.
---import ConsentGatedScripts from "#utils/consent/consentGatedScripts.astro";---
<ConsentGatedScripts scripts={[ { src: "https://www.instagram.com/embed.js", consentCategory: "C0005" }, { src: "https://example.com/no-consent-needed.js" },]} />How it works:
- Scripts without a
consentCategoryare rendered as<script async>immediately - Scripts with a
consentCategoryare gated behind OneTrust consent via an inline script (is:inline+define:vars) - Zero additional HTTP requests — the gating logic is embedded directly in the HTML
When to use: Block renderers, page layouts, and any .astro component that needs to load third-party scripts.
loadConsentGatedScripts() — for client-side TypeScript
Section titled “loadConsentGatedScripts() — for client-side TypeScript”Use this when you are in a SolidJS component, vanilla TypeScript, or any client-side context where you need to programmatically load scripts.
import { loadConsentGatedScripts } from "#utils/consent/loadConsentGatedScripts";
loadConsentGatedScripts([ { src: "https://vendor.com/sdk.js", category: "C0004" },]);How it works:
- Identical consent-checking logic to the Astro component
- Imported as a standard ES module — bundled and tree-shaken by Vite
- Deduplicates across calls (each
srcis loaded at most once)
When to use: SolidJS components with client:* directives, event handlers, or any runtime JavaScript that needs to load a consent-gated script after user interaction.
Consent-checking behavior (both tools)
Section titled “Consent-checking behavior (both tools)”Both tools share the same strategy for determining when to load scripts:
- Hook
window.OptanonWrapper— OneTrust calls this when consent state is resolved. Scripts load if their category is inwindow.OnetrustActiveGroups. - 5-second fallback — If
OptanonWrapperis never called (Cohesion may overwrite the handler):- If
window.OnetrustActiveGroupsis populated, OneTrust loaded — respect the consent state viatryLoadAll() - If
window.OnetrustActiveGroupsis undefined, OneTrust is absent (ad-blocker, local dev without Cohesion) — load all scripts as graceful degradation
- If
Scripts are never loaded eagerly before consent is resolved. This avoids a race condition where OnetrustActiveGroups may be pre-populated with default opt-out values before OneTrust processes the user’s stored preference.
Consent-Gated Embed Architecture
Section titled “Consent-Gated Embed Architecture”The consent-gated script loading for embeds involves three layers:
Embed Components (instagram.tsx, twitter.tsx, tiktok.tsx) └─ Export script metadata: { src, consentCategory }
Block Renderers (blocks.astro, externalEmbedCollection.astro) └─ Collect required scripts from rendered blocks └─ Deduplicate across multiple embeds
consentGatedScripts.astro └─ Render scripts respecting consent stateEmbed Components Define Their Script Requirements
Section titled “Embed Components Define Their Script Requirements”Each embed component exports a script descriptor alongside its component. This co-locates the consent requirement with the component that needs it:
export const scriptInstagram = { src: "https://www.instagram.com/embed.js", consentCategory: "C0005",};export const scriptTwitter = { src: "https://platform.twitter.com/widgets.js", consentCategory: "C0005",};export const scriptTiktok = { src: "https://www.tiktok.com/embed.js", consentCategory: "C0005",};Scripts without a consentCategory are loaded directly without gating.
Block Renderers Collect Required Scripts
Section titled “Block Renderers Collect Required Scripts”The block renderers scan their rendered blocks to build a deduplicated set of required scripts. The embedScripts map includes aliases for CMS vendor strings that differ from the canonical key (e.g. "x (twitter)" → scriptTwitter):
---const embedScripts = { instagram: scriptInstagram, twitter: scriptTwitter, "x (twitter)": scriptTwitter, tiktok: scriptTiktok,};
const requiredScripts = new Map<string, { src: string; consentCategory?: string }>();
blocks.forEach((block) => { if (block.name === "FeaturedMedia" && block.data?.media?.vendor) { addScriptForVendor(block.data.media.vendor); } // ... similar checks for ExternalEmbedCollection, ListFeatured});---
<ConsentGatedScripts scripts={Array.from(requiredScripts.values())} />This ensures each vendor script is loaded once regardless of how many embeds appear on the page. The following renderers include ConsentGatedScripts:
src/components/blocks/blocks.astro— landing pages, campaigns, partnerssrc/pages/articles/[slug]/_components/blocks/blocks.astro— article pagessrc/components/blocks/externalEmbedCollection/externalEmbedCollection.astro— self-fetching embed carousels
Graceful Degradation
Section titled “Graceful Degradation”The embed components render semantic HTML placeholders (blockquotes with links) that remain accessible even before the vendor script loads. If the user never grants consent, or if the vendor script fails, the content is still reachable via the fallback link.
Adding a New Consent-Gated Embed
Section titled “Adding a New Consent-Gated Embed”-
Create the embed component in
src/components/embeds/and export a script descriptor:export const scriptMyVendor = {src: "https://vendor.com/embed.js",consentCategory: "C0005",}; -
Export it from
src/components/embeds/index.ts -
Register the script in each block renderer’s
embedScriptsmap (include CMS vendor string aliases if they differ from the canonical key):import { scriptMyVendor } from "#components/embeds";const embedScripts = {// ...existingmyvendor: scriptMyVendor,"alternate vendor name": scriptMyVendor,}; -
Add detection logic in the block renderer’s
forEachloop so the script is collected when the embed is present
To add a script that does not require consent, omit the consentCategory field — it will be loaded directly.
Ongoing Maintenance
Section titled “Ongoing Maintenance”- New cookies must be assessed — any use of new cookies requires updating OneTrust categorization and disclosures
- Re-consent is required when cookies are used for new purposes not covered by existing consent
- Keep category IDs in sync across OneTrust, GTM, and Cohesion — mismatches break the consent flow
- Monitor cookie scans proactively rather than waiting for the Privacy team to flag issues
- OneTrust banner changes can be published from the admin panel and go live within ~15 minutes
- Do not load scripts eagerly — always wait for
OptanonWrapperor the fallback timeout to ensure stored consent preferences are respected
Related
Section titled “Related”- Component Architecture — rendering strategy decisions
- Data Fetching Patterns — how block data flows to components