Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import aboutDevhubPlugin from "./plugins/about-devhub";
import chunkReloadPlugin from "./plugins/chunk-reload";
import contentEntriesPlugin from "./plugins/content-entries";
import cookbooksPlugin from "./plugins/cookbooks";
import gtmNoscriptPlugin from "./plugins/gtm-noscript";
import llmsTxtPlugin from "./plugins/llms-txt";
import markdownStaticRoutesPlugin from "./plugins/markdown-static-routes";
import perspectivesPlugin from "./plugins/perspectives";
Expand All @@ -16,6 +17,8 @@ import robotsTxtPlugin from "./plugins/robots-txt";
import solutionRssFeedPlugin from "./plugins/solutions-rss-feed";
import { showDrafts } from "./src/lib/feature-flags-server";
import { getHackathonBannerConfig } from "./src/lib/hackathon-banner-server";
import { gtmHeadTag } from "./src/lib/gtm";
import { oneTrustHeadTags, resolveOneTrustEnv } from "./src/lib/onetrust";
import {
resolveSiteBaseUrl,
resolveSiteOrigin,
Expand All @@ -28,6 +31,7 @@ const siteOrigin = resolveSiteOrigin();
const siteBaseUrl = resolveSiteBaseUrl();
const publicSiteUrl = siteUrlFromConfig(siteOrigin, siteBaseUrl);
const hackathonBanner = getHackathonBannerConfig();
const oneTrustEnv = resolveOneTrustEnv();

const config: Config = {
title: "Databricks Developer",
Expand Down Expand Up @@ -76,6 +80,9 @@ const config: Config = {
},

headTags: [
// OneTrust must stay first so its AutoBlocker can gate GTM's cookies. The
// GTM <noscript> counterpart is injected by plugins/gtm-noscript.ts.
...(oneTrustEnv ? [...oneTrustHeadTags(oneTrustEnv), gtmHeadTag()] : []),
{
tagName: "meta",
attributes: {
Expand Down Expand Up @@ -195,6 +202,7 @@ const config: Config = {
cookbooksPlugin,
perspectivesPlugin,
chunkReloadPlugin,
gtmNoscriptPlugin,
],

themeConfig: {
Expand Down
20 changes: 20 additions & 0 deletions plugins/gtm-noscript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Plugin } from "@docusaurus/types";
import { gtmNoscriptTag } from "../src/lib/gtm";
import { resolveOneTrustEnv } from "../src/lib/onetrust";

/**
* Injects the GTM <noscript> iframe immediately after the opening <body> tag
* (config `headTags` can't place body tags, only a plugin's preBodyTags can).
* The GTM <head> script lives in docusaurus.config.ts, below the OneTrust
* tags. Gated like OneTrust: GTM must never load without consent management.
*/
export default function gtmNoscriptPlugin(): Plugin {
return {
name: "docusaurus-gtm-noscript",
injectHtmlTags() {
return {
preBodyTags: resolveOneTrustEnv() ? [gtmNoscriptTag()] : [],
};
},
};
}
4 changes: 4 additions & 0 deletions src/components/perspectives/perspectives-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useColorMode } from "@docusaurus/theme-common";
import Layout from "@theme/Layout";
import { Moon, Sun } from "lucide-react";
import { useEffect, useState, type ReactNode } from "react";
import { YourPrivacyChoicesLink } from "@/components/theme/your-privacy-choices-link";
import { COPYRIGHT_LINE, LEGAL_LINKS } from "@/lib/legal-links";

type PerspectivesShellProps = {
Expand Down Expand Up @@ -80,6 +81,9 @@ export function PerspectivesShell({
</a>
</li>
))}
<li className="flex items-center gap-x-2 before:opacity-50 before:content-['|']">
<YourPrivacyChoicesLink className="perspectives-legal-link rounded-sm no-underline transition-colors" />
</li>
</ul>
</div>
<div className="space-y-2 border-t border-black/8 pt-4 text-xs leading-relaxed dark:border-white/10">
Expand Down
35 changes: 10 additions & 25 deletions src/components/theme/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
import type { ReactNode } from "react";

import { Icons } from "@/components/icons";
import { COPYRIGHT_LINE } from "@/lib/legal-links";
import { YourPrivacyChoicesLink } from "@/components/theme/your-privacy-choices-link";
import { COPYRIGHT_LINE, LEGAL_LINKS } from "@/lib/legal-links";
import { cn } from "@/lib/utils";

type FooterItem = {
Expand Down Expand Up @@ -68,25 +69,6 @@ const SOCIAL_LINKS = [
},
] as const;

const LEGAL_LINKS: FooterItem[] = [
{
label: "Privacy Notice",
href: "https://www.databricks.com/legal/privacynotice",
},
{
label: "Terms of Use",
href: "https://www.databricks.com/legal/terms-of-use",
},
{
label: "Modern Slavery Statement",
href: "https://www.databricks.com/legal/modern-slavery-policy-statement",
},
{
label: "California Privacy",
href: "https://www.databricks.com/legal/supplemental-privacy-notice-california-residents",
},
];

type SocialIcon = keyof typeof Icons;

function resolveSocialLabel(label: string): string {
Expand Down Expand Up @@ -155,6 +137,11 @@ function FooterLink({ item }: { item: FooterItem }): ReactNode {
);
}

const legalLinkClassName = cn(
"inline-flex items-center w-fit text-[0.8125rem] leading-none tracking-tight no-underline transition-colors hover:no-underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-db-cyan",
"text-grey-40 hover:text-grey-70",
);

function FooterLegalLinks({ className }: { className?: string }): ReactNode {
return (
<nav
Expand All @@ -164,16 +151,14 @@ function FooterLegalLinks({ className }: { className?: string }): ReactNode {
{LEGAL_LINKS.map((item) => (
<Link
key={item.label}
to={item.to ?? (item.href as string)}
to={item.href}
target="_blank"
className={cn(
"inline-flex items-center w-fit text-[0.8125rem] leading-none tracking-tight no-underline transition-colors hover:no-underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-db-cyan",
"text-grey-40 hover:text-grey-70",
)}
className={legalLinkClassName}
>
{item.label}
</Link>
))}
<YourPrivacyChoicesLink className={legalLinkClassName} />
</nav>
);
}
Expand Down
40 changes: 40 additions & 0 deletions src/components/theme/your-privacy-choices-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import useBaseUrl from "@docusaurus/useBaseUrl";

import { cn } from "@/lib/utils";

declare global {
interface Window {
OneTrust?: { ToggleInfoDisplay: () => void };
}
}

/**
* The legally required "Your Privacy Choices" footer link with the GPC icon,
* matching www.databricks.com. Opens the OneTrust preference center when the
* consent scripts are loaded (production/previews, see src/lib/onetrust.ts);
* a no-op in local dev where they are off.
*/
export function YourPrivacyChoicesLink({ className }: { className?: string }) {
const iconSrc = useBaseUrl("/img/gpc-icon.png");

return (
<a
href="#yourprivacychoices"
className={cn("inline-flex items-center gap-1.5", className)}
onClick={(event) => {
event.preventDefault();
window.OneTrust?.ToggleInfoDisplay();
}}
>
Your Privacy Choices
<img
src={iconSrc}
alt=""
className="h-3.5 w-auto"
width={75}
height={36}
loading="lazy"
/>
</a>
);
}
41 changes: 41 additions & 0 deletions src/lib/gtm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Google Tag Manager tags for the global Databricks container (GTM-TWTKQQ),
* copied verbatim from the www.databricks.com install. Two parts:
* - an inline <head> script, which must load BELOW the OneTrust tags so the
* AutoBlocker can gate the cookies GTM drops (see src/lib/onetrust.ts)
* - a <noscript> iframe immediately after the opening <body> tag
*
* GTM must never load without OneTrust, so callers gate both tags on
* resolveOneTrustEnv() returning a variant.
*
* Framework-neutral on purpose: the descriptors match Docusaurus
* `headTags`/`preBodyTags` and can be rendered as JSX from a Next.js root
* layout.
*/

const GTM_CONTAINER_ID = "GTM-TWTKQQ";

export function gtmHeadTag(): {
tagName: "script";
// Docusaurus config `headTags` validation requires `attributes`, even empty.
attributes: Record<string, string>;
innerHTML: string;
} {
return {
tagName: "script",
attributes: {},
innerHTML:
"(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':" +
"new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0]," +
"j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=" +
"'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);" +
`})(window,document,'script','dataLayer','${GTM_CONTAINER_ID}');`,
};
}

export function gtmNoscriptTag(): { tagName: "noscript"; innerHTML: string } {
return {
tagName: "noscript",
innerHTML: `<iframe src="https://www.googletagmanager.com/ns.html?id=${GTM_CONTAINER_ID}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
};
}
85 changes: 85 additions & 0 deletions src/lib/onetrust.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* OneTrust cookie consent tags, injected into the <head> of every page. They
* must appear above any other third-party script (e.g. GTM) so the AutoBlocker
* can gate non-consented cookies.
*
* Two variants of the standard Databricks OneTrust install exist:
* - "test": works on any domain — Vercel previews and local testing.
* - "production": only works on *.databricks.com domains.
*
* Variant resolution:
* 1. ONETRUST_ENV — explicit override, e.g. `ONETRUST_ENV=test npm run dev`
* to see the banner locally.
* 2. VERCEL_ENV=production — production variant.
* 3. VERCEL_ENV=preview|development — test variant.
* 4. otherwise (local dev/build) — null: no tags, no banner.
*
* Framework-neutral on purpose: the descriptors match Docusaurus `headTags`
* and can be rendered as <script>/<link> JSX from a Next.js root layout.
*/

const ONETRUST_DOMAIN_SCRIPT_ID = "92466579-1717-44d3-809d-a05fb02843ed";

type Env = Record<string, string | undefined>;

export type OneTrustEnv = "production" | "test";

export function resolveOneTrustEnv(env: Env = process.env): OneTrustEnv | null {
if (env.ONETRUST_ENV === "production" || env.ONETRUST_ENV === "test") {
return env.ONETRUST_ENV;
}
if (env.VERCEL_ENV === "production") return "production";
if (env.VERCEL_ENV === "preview" || env.VERCEL_ENV === "development") {
return "test";
}
return null;
}

export function oneTrustHeadTags(variant: OneTrustEnv): {
tagName: "script" | "link";
attributes: Record<string, string>;
}[] {
const isTest = variant === "test";
return [
// Self-hosted AutoBlocker: prevents non-consented cookies/scripts from loading.
{
tagName: "script",
attributes: {
type: "text/javascript",
src: `https://www.databricks.com/sites/default/files/${isTest ? "onetrust-test" : "onetrust"}/DB_OtAutoBlock.js?v=1`,
},
},
// OneTrust SDK stub: loads the consent banner itself.
{
tagName: "script",
attributes: {
src: "https://cdn.cookielaw.org/scripttemplates/otSDKStub.js",
"data-document-language": "true",
type: "text/javascript",
charset: "UTF-8",
"data-domain-script": isTest
? `${ONETRUST_DOMAIN_SCRIPT_ID}-test`
: ONETRUST_DOMAIN_SCRIPT_ID,
},
},
// Databricks custom integration: removes cookies the user opted out of.
{
tagName: "script",
attributes: {
type: "text/javascript",
src: "https://www.databricks.com/wp-content/plugins/databricks/js/onetrust.js?ver=1.0.0",
id: "db-onetrust-script",
},
},
// Databricks styling for the banner.
{
tagName: "link",
attributes: {
rel: "stylesheet",
id: "db-onetrust-style",
href: "https://www.databricks.com/wp-content/uploads/db_onetrust.css",
media: "all",
},
},
];
}
4 changes: 4 additions & 0 deletions src/theme/Footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
import { ExternalLink } from "lucide-react";
import type { ReactNode } from "react";
import { Separator } from "@/components/ui/separator";
import { YourPrivacyChoicesLink } from "@/components/theme/your-privacy-choices-link";
import { COPYRIGHT_LINE, LEGAL_LINKS } from "@/lib/legal-links";

type FooterItem = {
Expand Down Expand Up @@ -141,6 +142,9 @@ export default function Footer(): ReactNode {
</a>
</li>
))}
<li className="flex items-center gap-x-2 before:text-white/35 before:content-['|']">
<YourPrivacyChoicesLink className="rounded-sm text-white/70 no-underline transition-colors duration-150 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-db-cyan" />
</li>
</ul>
</div>
</footer>
Expand Down
Binary file added static/img/gpc-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/e2e/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ test.describe("footer navigation", () => {
"https://www.databricks.com/legal/terms-of-use",
"https://www.databricks.com/legal/modern-slavery-policy-statement",
"https://www.databricks.com/legal/supplemental-privacy-notice-california-residents",
"#yourprivacychoices",
"https://www.databricks.com/product/databricks-apps",
"https://www.databricks.com/product/lakebase",
"https://www.databricks.com/product/artificial-intelligence/agent-bricks",
Expand Down