Skip to content

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.

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 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.

CategoryNameDescriptionConsent Required
C0001Strictly NecessaryEssential for the site to function (privacy preferences, login, forms). Cannot be disabled by users. Does not store personally identifiable information.No
C0002PerformanceCounts visits and traffic sources to measure site performance. All data is aggregated and anonymous.Yes
C0003FunctionalEnables enhanced functionality and personalization. May be set by us or third-party providers.Yes
C0004Targeting/AdvertisingUsed by advertising partners to build interest profiles and show relevant ads. Based on uniquely identifying the browser/device.Yes
C0005Social MediaSet 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.

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 to lonelyplanet.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).

OneTrust serves different experiences based on the visitor’s location using geolocation rules:

RegionBehaviorDefault State
Global (Default)Opt-outNo banner shown; users are opted into all cookies by default
EEA, UK, Norway, Iceland, LiechtensteinOpt-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.

  1. The Cohesion SDK script loads in layout.astro, configured with consent.onetrust.optIn: true — this initializes OneTrust
  2. OneTrust fires first on all pages, checking which cookie groups have been accepted or denied
  3. When a user makes a consent choice, OneTrust populates window.OnetrustActiveGroups with a comma-separated string of accepted categories (e.g. ",C0001,C0003,C0005,")
  4. OneTrust calls window.OptanonWrapper() whenever consent state changes
  5. 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

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)

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 consentCategory are rendered as <script async> immediately
  • Scripts with a consentCategory are 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 src is 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.

Both tools share the same strategy for determining when to load scripts:

  1. Hook window.OptanonWrapper — OneTrust calls this when consent state is resolved. Scripts load if their category is in window.OnetrustActiveGroups.
  2. 5-second fallback — If OptanonWrapper is never called (Cohesion may overwrite the handler):
    • If window.OnetrustActiveGroups is populated, OneTrust loaded — respect the consent state via tryLoadAll()
    • If window.OnetrustActiveGroups is undefined, OneTrust is absent (ad-blocker, local dev without Cohesion) — load all scripts as graceful degradation

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.

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 state

Embed 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:

components/embeds/instagram.tsx
export const scriptInstagram = {
src: "https://www.instagram.com/embed.js",
consentCategory: "C0005",
};
components/embeds/twitter.tsx
export const scriptTwitter = {
src: "https://platform.twitter.com/widgets.js",
consentCategory: "C0005",
};
components/embeds/tiktok.tsx
export const scriptTiktok = {
src: "https://www.tiktok.com/embed.js",
consentCategory: "C0005",
};

Scripts without a consentCategory are loaded directly without gating.

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, partners
  • src/pages/articles/[slug]/_components/blocks/blocks.astro — article pages
  • src/components/blocks/externalEmbedCollection/externalEmbedCollection.astro — self-fetching embed carousels

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.

  1. Create the embed component in src/components/embeds/ and export a script descriptor:

    export const scriptMyVendor = {
    src: "https://vendor.com/embed.js",
    consentCategory: "C0005",
    };
  2. Export it from src/components/embeds/index.ts

  3. Register the script in each block renderer’s embedScripts map (include CMS vendor string aliases if they differ from the canonical key):

    import { scriptMyVendor } from "#components/embeds";
    const embedScripts = {
    // ...existing
    myvendor: scriptMyVendor,
    "alternate vendor name": scriptMyVendor,
    };
  4. Add detection logic in the block renderer’s forEach loop 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.

  • 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 OptanonWrapper or the fallback timeout to ensure stored consent preferences are respected