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]
---
-[](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]
---
-[](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]
---
-[](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]
---
-[](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]