This is an automated email from the ASF dual-hosted git repository.

xiangfu pushed a commit to branch new-site-dev
in repository https://gitbox.apache.org/repos/asf/pinot-site.git


The following commit(s) were added to refs/heads/new-site-dev by this push:
     new 48e871ed Gate YouTube embeds and fix build
48e871ed is described below

commit 48e871ed1de67422a9da221c3978f661f2338f23
Author: Xiang Fu <[email protected]>
AuthorDate: Fri Jan 9 02:46:31 2026 -0800

    Gate YouTube embeds and fix build
---
 app/layout.tsx                                     |  12 +-
 components/TextMediaSplitSection.tsx               |  10 +-
 components/VideoEmbed.tsx                          | 125 +++++++++++++++++++--
 ...28-Apache-Pinot-Pausing-Real-Time-Ingestion.mdx |   2 +-
 ...pache-Pinot-0-12-Configurable-Time-Boundary.mdx |   2 +-
 ...03-30-Apache-Pinot-0-12-Consumer-Record-Lag.mdx |   2 +-
 ...3-05-11-Geospatial-Indexing-in-Apache-Pinot.mdx |   2 +-
 package.json                                       |   2 +-
 scripts/patch-contentlayer.mjs                     |  50 +++++++++
 9 files changed, 178 insertions(+), 29 deletions(-)

diff --git a/app/layout.tsx b/app/layout.tsx
index fc75eb6d..becfd065 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,7 +1,7 @@
 import 'css/tailwind.css';
 import 'pliny/search/algolia.css';
 
-import localFont from 'next/font/local';
+import { Work_Sans } from 'next/font/google';
 import { Analytics, AnalyticsConfig } from 'pliny/analytics';
 import { SearchProvider, SearchConfig } from 'pliny/search';
 import Header from '@/components/Header';
@@ -10,14 +10,8 @@ import siteMetadata from '@/data/siteMetadata';
 import { ThemeProviders } from './theme-providers';
 import { Metadata } from 'next';
 
-const work_sans = localFont({
-    src: [
-        {
-            path: '../public/static/fonts/WorkSans-Variable.woff2',
-            weight: '100 900',
-            style: 'normal'
-        }
-    ],
+const work_sans = Work_Sans({
+    subsets: ['latin'],
     display: 'swap',
     variable: '--custom-font-work-sans'
 });
diff --git a/components/TextMediaSplitSection.tsx 
b/components/TextMediaSplitSection.tsx
index dc6edd3e..beb5b096 100644
--- a/components/TextMediaSplitSection.tsx
+++ b/components/TextMediaSplitSection.tsx
@@ -4,6 +4,7 @@ import React from 'react';
 import { Button } from './ui/button';
 import { ArrowRight } from 'lucide-react';
 import Link from 'next/link';
+import VideoEmbed from './VideoEmbed';
 
 interface TextMediaSplitSectionProps {
     heading: string;
@@ -59,13 +60,12 @@ const TextMediaSplitSection: 
React.FC<TextMediaSplitSectionProps> = ({
                 </article>
                 <aside className="flex-1">
                     {videoUrl ? (
-                        <iframe
-                            className="h-[197px] w-full md:h-full"
+                        <VideoEmbed
                             src={videoUrl}
                             title={videoTitle}
-                            allow="accelerometer; autoplay; clipboard-write; 
encrypted-media; gyroscope; picture-in-picture"
-                            allowFullScreen
-                        ></iframe>
+                            className="h-[197px] w-full md:h-full"
+                            aspectRatioClassName=""
+                        />
                     ) : imageUrl ? (
                         <img src={imageUrl} alt={imageAlt} />
                     ) : null}
diff --git a/components/VideoEmbed.tsx b/components/VideoEmbed.tsx
index e0d46d75..f3d4b8a5 100644
--- a/components/VideoEmbed.tsx
+++ b/components/VideoEmbed.tsx
@@ -1,21 +1,126 @@
 'use client';
 
+import { useMemo, useState } from 'react';
+import { cn } from '@/app/lib/utils';
+import CustomLink from './Link';
+
 type VideoEmbedProps = {
     src: string;
     title?: string;
+    posterSrc?: string;
+    className?: string;
+    iframeClassName?: string;
+    aspectRatioClassName?: string;
+    buttonLabel?: string;
+    privacyNote?: string;
+};
+
+const defaultPrivacyNote = 'Loading this video will connect to YouTube and may 
set cookies.';
+
+const getYouTubeVideoId = (src: string) => {
+    try {
+        const url = new URL(src);
+
+        if (url.hostname === 'youtu.be') {
+            return url.pathname.replace(/^\/+/, '').replace(/\/+$/, '') || 
null;
+        }
+
+        const queryId = url.searchParams.get('v');
+        if (queryId) {
+            return queryId;
+        }
+
+        const embedMatch = url.pathname.match(/\/embed\/([^/?]+)/);
+        if (embedMatch) {
+            return embedMatch[1];
+        }
+    } catch {
+        return null;
+    }
+
+    return null;
 };
 
-const VideoEmbed = ({ src, title }: VideoEmbedProps) => {
+const getYouTubeWatchUrl = (src: string) => {
+    const videoId = getYouTubeVideoId(src);
+    return videoId ? `https://www.youtube.com/watch?v=${videoId}` : null;
+};
+
+const getNoCookieEmbedUrl = (src: string) => {
+    try {
+        const url = new URL(src);
+        const youtubeHosts = new Set([
+            'youtube.com',
+            'www.youtube.com',
+            'm.youtube.com',
+            'youtube-nocookie.com',
+            'www.youtube-nocookie.com'
+        ]);
+
+        if (youtubeHosts.has(url.hostname) && 
url.pathname.startsWith('/embed/')) {
+            url.hostname = 'www.youtube-nocookie.com';
+            return url.toString();
+        }
+    } catch {
+        return src;
+    }
+
+    return src;
+};
+
+const VideoEmbed = ({
+    src,
+    title,
+    posterSrc = '/static/images/video_thumbnail.png',
+    className,
+    iframeClassName,
+    aspectRatioClassName = 'aspect-h-9 aspect-w-16',
+    buttonLabel = 'Load video',
+    privacyNote = defaultPrivacyNote
+}: VideoEmbedProps) => {
+    const [isLoaded, setIsLoaded] = useState(false);
+    const embedSrc = useMemo(() => getNoCookieEmbedUrl(src), [src]);
+    const watchUrl = useMemo(() => getYouTubeWatchUrl(src), [src]);
+
     return (
-        <div className="aspect-h-9 aspect-w-16">
-            <iframe
-                className="h-full w-full"
-                src={src}
-                title={title || 'Embedded Video'}
-                allowFullScreen
-                frameBorder="0"
-                allow="accelerometer; autoplay; clipboard-write; 
encrypted-media; gyroscope; picture-in-picture; web-share"
-            ></iframe>
+        <div className={cn('relative w-full', aspectRatioClassName, 
className)}>
+            {isLoaded ? (
+                <iframe
+                    className={cn('h-full w-full', iframeClassName)}
+                    src={embedSrc}
+                    title={title || 'Embedded Video'}
+                    allowFullScreen
+                    frameBorder="0"
+                    allow="accelerometer; autoplay; clipboard-write; 
encrypted-media; gyroscope; picture-in-picture; web-share"
+                ></iframe>
+            ) : (
+                <div
+                    className="flex h-full w-full flex-col items-center 
justify-center gap-3 bg-black/70 bg-cover bg-center px-4 py-6 text-center 
text-white"
+                    style={{
+                        backgroundImage: `linear-gradient(0deg, rgba(0, 0, 0, 
0.65), rgba(0, 0, 0, 0.65)), url(${posterSrc})`
+                    }}
+                >
+                    <p className="text-sm font-semibold sm:text-base">
+                        This video is hosted on YouTube.
+                    </p>
+                    <button
+                        type="button"
+                        onClick={() => setIsLoaded(true)}
+                        className="rounded-md bg-white px-4 py-2 text-sm 
font-semibold text-black shadow-sm transition hover:bg-gray-200 
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
+                    >
+                        {buttonLabel}
+                    </button>
+                    {privacyNote && <p className="text-xs 
text-white/90">{privacyNote}</p>}
+                    {watchUrl && (
+                        <CustomLink
+                            href={watchUrl}
+                            className="text-xs underline underline-offset-4 
hover:text-white"
+                        >
+                            Open on YouTube
+                        </CustomLink>
+                    )}
+                </div>
+            )}
         </div>
     );
 };
diff --git a/data/blog/2022-11-28-Apache-Pinot-Pausing-Real-Time-Ingestion.mdx 
b/data/blog/2022-11-28-Apache-Pinot-Pausing-Real-Time-Ingestion.mdx
index ab709525..40e4bce0 100644
--- a/data/blog/2022-11-28-Apache-Pinot-Pausing-Real-Time-Ingestion.mdx
+++ b/data/blog/2022-11-28-Apache-Pinot-Pausing-Real-Time-Ingestion.mdx
@@ -8,7 +8,7 @@ summary: Learn about a feature that lets you pause and resume 
real-time data ing
 tags: [Pinot, Data, Analytics, User-Facing Analytics, pause, resume, real-time 
ingestion]
 ---
 
-[![Watch the 
video](https://i3.ytimg.com/vi/u9CwDpMZRog/maxresdefault.jpg)](https://youtu.be/u9CwDpMZRog)
+<VideoEmbed src="https://www.youtube.com/embed/u9CwDpMZRog"; title="YouTube 
video player" />
 
 The Apache Pinot community recently released version 
[0.11.0](https://medium.com/apache-pinot-developer-blog/apache-pinot-0-11-released-d564684df5d4),
 which has lots of goodies for you to play with.
 
diff --git 
a/data/blog/2023-02-21-Apache-Pinot-0-12-Configurable-Time-Boundary.mdx 
b/data/blog/2023-02-21-Apache-Pinot-0-12-Configurable-Time-Boundary.mdx
index d903940a..887ece48 100644
--- a/data/blog/2023-02-21-Apache-Pinot-0-12-Configurable-Time-Boundary.mdx
+++ b/data/blog/2023-02-21-Apache-Pinot-0-12-Configurable-Time-Boundary.mdx
@@ -8,7 +8,7 @@ summary: This post will explore the ability to configure the 
time boundary when
 tags: [Pinot, Data, Analytics, User-Facing Analytics, hybrid tables, time 
boundary]
 ---
 
-[![Watch the 
video](https://i3.ytimg.com/vi/lB3RaKJ0Hbs/maxresdefault.jpg)](https://youtu.be/lB3RaKJ0Hbs)
+<VideoEmbed src="https://www.youtube.com/embed/lB3RaKJ0Hbs"; title="YouTube 
video player" />
 
 The Apache Pinot community recently released version 
[0.12.0](https://docs.pinot.apache.org/basics/releases/0.12.0), which has lots 
of goodies for you to play with. This is the first in a series of blog posts 
showing off some of the new features in this release.
 
diff --git a/data/blog/2023-03-30-Apache-Pinot-0-12-Consumer-Record-Lag.mdx 
b/data/blog/2023-03-30-Apache-Pinot-0-12-Consumer-Record-Lag.mdx
index 0b346cd4..e0145211 100644
--- a/data/blog/2023-03-30-Apache-Pinot-0-12-Consumer-Record-Lag.mdx
+++ b/data/blog/2023-03-30-Apache-Pinot-0-12-Consumer-Record-Lag.mdx
@@ -8,7 +8,7 @@ summary: This post will explore a new API endpoint that lets 
you check how much
 tags: [Pinot, Data, Analytics, User-Facing Analytics, consumer record lag, 
kafka]
 ---
 
-[![Watch the 
video](https://i3.ytimg.com/vi/JJEh_kBfJts/maxresdefault.jpg)](https://youtu.be/JJEh_kBfJts)
+<VideoEmbed src="https://www.youtube.com/embed/JJEh_kBfJts"; title="YouTube 
video player" />
 
 The Apache Pinot community recently released version 
[0.12.0](https://docs.pinot.apache.org/basics/releases/0.12.0), which has lots 
of goodies for you to play with. I’ve been exploring and writing about those 
features in a series of blog posts.
 
diff --git a/data/blog/2023-05-11-Geospatial-Indexing-in-Apache-Pinot.mdx 
b/data/blog/2023-05-11-Geospatial-Indexing-in-Apache-Pinot.mdx
index 8afbcadb..77149451 100644
--- a/data/blog/2023-05-11-Geospatial-Indexing-in-Apache-Pinot.mdx
+++ b/data/blog/2023-05-11-Geospatial-Indexing-in-Apache-Pinot.mdx
@@ -8,7 +8,7 @@ summary: This post will explore a new API endpoint that lets 
you check how much
 tags: [Pinot, Data, Analytics, User-Facing Analytics, geospatial indexing]
 ---
 
-[![Watch the 
video](https://i3.ytimg.com/vi/J-4iHPolZz0/maxresdefault.jpg)](https://youtu.be/J-4iHPolZz0)
+<VideoEmbed src="https://www.youtube.com/embed/J-4iHPolZz0"; title="YouTube 
video player" />
 
 It’s been over 18 months since [geospatial indexes were added to Apache 
Pinot™](https://medium.com/apache-pinot-developer-blog/introduction-to-geospatial-queries-in-apache-pinot-b63e2362e2a9),
 giving you the ability to retrieve data based on geographic location—a common 
requirement in many analytics use cases. Using geospatial queries in 
combination with time series queries in Pinot, you can perform complex 
spatiotemporal analysis, such as analyzing changes in weather patterns over 
time  [...]
 
diff --git a/package.json b/package.json
index 603119e0..29a40867 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
         "lint": "next lint --fix --dir pages --dir app --dir components --dir 
lib --dir layouts --dir scripts",
         "check": "npx prettier --check .",
         "format": "npx prettier --write .",
-        "postinstall": "husky install",
+        "postinstall": "husky install && node 
./scripts/patch-contentlayer.mjs",
         "test": "node --test tests/matomo.test.cjs"
     },
     "dependencies": {
diff --git a/scripts/patch-contentlayer.mjs b/scripts/patch-contentlayer.mjs
new file mode 100644
index 00000000..9d9728bb
--- /dev/null
+++ b/scripts/patch-contentlayer.mjs
@@ -0,0 +1,50 @@
+import fs from 'node:fs';
+import path from 'node:path';
+
+const nodeMajor = Number(process.versions.node.split('.')[0]);
+
+if (Number.isNaN(nodeMajor) || nodeMajor < 22) {
+    process.exit(0);
+}
+
+const targetPath = path.join(
+    process.cwd(),
+    'node_modules',
+    '@contentlayer',
+    'core',
+    'dist',
+    'generation',
+    'generate-dotpkg.js'
+);
+
+if (!fs.existsSync(targetPath)) {
+    console.warn(`contentlayer patch skipped: ${targetPath} not found.`);
+    process.exit(0);
+}
+
+const source = fs.readFileSync(targetPath, 'utf8');
+
+if (source.includes("with { type: 'json' }")) {
+    process.exit(0);
+}
+
+const needle = `const needsJsonAssertStatement = nodeVersionMajor > 16 || 
(nodeVersionMajor === 16 && nodeVersionMinor >= 14);
+    const assertStatement = needsJsonAssertStatement ? \` assert { type: 
'json' }\` : '';`;
+
+const replacement = `const needsJsonWithStatement = nodeVersionMajor >= 22;
+    const needsJsonAssertStatement =
+        !needsJsonWithStatement &&
+        (nodeVersionMajor > 16 || (nodeVersionMajor === 16 && nodeVersionMinor 
>= 14));
+    const assertStatement = needsJsonWithStatement
+        ? \` with { type: 'json' }\`
+        : needsJsonAssertStatement
+        ? \` assert { type: 'json' }\`
+        : '';`;
+
+if (!source.includes(needle)) {
+    throw new Error('contentlayer patch failed: expected snippet not found.');
+}
+
+const patched = source.replace(needle, replacement);
+fs.writeFileSync(targetPath, patched, 'utf8');
+console.log('contentlayer patch applied for Node >= 22.');


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to