From 8d38d000fd517268b52306dd4f0a0cdfdf03efc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 4 Jul 2024 17:04:35 +0200 Subject: [PATCH] Fix RND-4344 (#2368) --- .github/workflows/ci.yaml | 14 +- bun.lockb | Bin 534314 -> 534346 bytes package.json | 2 +- packages/react-openapi/src/OpenAPISchema.tsx | 12 +- src/app/(global)/~gitbook/image/route.ts | 7 +- .../(content)/[[...pathname]]/page.tsx | 2 + src/components/Ads/Ad.tsx | 204 ++++++++++++++++++ src/components/Ads/AdClassicRendering.tsx | 47 ++++ src/components/Ads/AdCoverRendering.tsx | 108 ++++++++++ src/components/Ads/index.ts | 1 + src/components/Ads/types.ts | 51 +++++ src/components/DocumentView/Embed.tsx | 2 +- .../DocumentView/OpenAPI/scalar.css | 26 ++- src/components/PageAside/PageAside.tsx | 26 ++- src/lib/colors.ts | 9 + src/lib/csp.ts | 2 +- 16 files changed, 491 insertions(+), 22 deletions(-) create mode 100644 src/components/Ads/Ad.tsx create mode 100644 src/components/Ads/AdClassicRendering.tsx create mode 100644 src/components/Ads/AdCoverRendering.tsx create mode 100644 src/components/Ads/index.ts create mode 100644 src/components/Ads/types.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ec6def228..96e010216 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,7 +20,7 @@ jobs: - name: Setup bun uses: oven-sh/setup-bun@v1 with: - bun-version: 1.0.33 + bun-version: 1.1.18 - name: Install dependencies run: bun install --frozen-lockfile env: @@ -82,7 +82,7 @@ jobs: - name: Setup bun uses: oven-sh/setup-bun@v1 with: - bun-version: 1.0.33 + bun-version: 1.1.18 - name: Install dependencies run: bun install --frozen-lockfile - name: Setup Playwright @@ -102,7 +102,7 @@ jobs: - name: Setup bun uses: oven-sh/setup-bun@v1 with: - bun-version: 1.0.33 + bun-version: 1.1.18 - name: Install dependencies run: bun install --frozen-lockfile env: @@ -121,7 +121,7 @@ jobs: - name: Setup bun uses: oven-sh/setup-bun@v1 with: - bun-version: 1.0.33 + bun-version: 1.1.18 - name: Install dependencies run: bun install --frozen-lockfile env: @@ -136,7 +136,7 @@ jobs: - name: Setup bun uses: oven-sh/setup-bun@v1 with: - bun-version: 1.0.33 + bun-version: 1.1.18 - name: Install dependencies run: bun install --frozen-lockfile env: @@ -151,7 +151,7 @@ jobs: - name: Setup bun uses: oven-sh/setup-bun@v1 with: - bun-version: 1.0.33 + bun-version: 1.1.18 - name: Install dependencies run: bun install --frozen-lockfile env: @@ -166,7 +166,7 @@ jobs: - name: Setup bun uses: oven-sh/setup-bun@v1 with: - bun-version: 1.0.33 + bun-version: 1.1.18 - name: Install dependencies run: bun install --frozen-lockfile env: diff --git a/bun.lockb b/bun.lockb index 0bd371d2a0724eead445e47703fd235ae0350bb8..2536501e4f4d7d43056821a2dbf54776c9203992 100755 GIT binary patch delta 2656 zcmXZb2~bsa6vy!kkISMqDH!Hh3aB`^V2UPL>LYgxLBf4sFjrhqM3kPpV&;bW9Bs6$ z%u!1u@mV@$E>oi7P>$40nyJmWjEUk>P22ax?>`*==Y8(E|99^F|KHy7n6mPi^@*`v zZeM(y-QYp8_tL!V8}ZrI+YZ!PR7OnH+moC z^b6-7V?&7J1TjBPRE0TCE#{4~#^m=g8%44v0ovBw|Vo+b9` zQUY|*)?)0FwIshJ+mBhdt%yq5r~X;C!~tDu!=*A^I;bTQvDvZ`)wRV6WQY7;+Y+TJ zZ^xx;);;5}tUdW|zPcfEK$b+lOIvZoUoXjx@_nw=J923sml`o2Rr!nL`&E7nvoG&N zl&J12)g@y-)g9M*FJWo26RPWs{iz*3Sz|uuWpFVV!+Z*KnO`9qVYbI-baNN-Cc3!{ zv$xunh?AXDT{of70$(VD`k_iOX0V^F^#aa}VOW z>TY88?t2my{#hM(spEC&4wtOY-b50)jk~fwRcFyCXgCHfK_WmU3%*fsiS&3s?> z8u@kWjqyPCI(c!8`J8HDD!5pM4`uzaHnK-DdkqKZ)DF#KSvvWC>x%J2Hh{db>~~oP z7HmIc#vho+@j1>Le5j82-<-cxn90XP%#PLHvVr7nW&g+qVOu#mcC7xD4JNOz<77v~ z-s%t{8na^+j9FhpiBg}o+M~i@V1Jpb!r@p?S*UCT)(f*^6($==o}#*N*(j`&EJ8LK zo2YH~)e?>Yw+U;@#$ubXp3HS*pjsxxK z77-`a;RF>f#-4DqHIRkb+DnKk5^G?R>Xwo}kWH2?!v<=-DYE6*8QD~qHte^!f+!PC zQ{hVNoNT&m6?O_+#GH*;(}lzw)y)y99YuX+tN4CaZgLQ2G$+p$9CJlO{FWtcTJAG4-6680S#*3|+P7Ll*ef(vDvu+^9? zT%emb%WPpgz_wsjXA;K4bpMrfH;$#9KxJn)B{HbQ|4gcTH2AadKzXVg>WPT4^Ud%E zE~mTQ6C!5j*L*%6NXU2ldV-Qx<`lSroPy9c;kE0o;21Kh3Pb%BiEdQj%jKbwArUdF YYq%uvV_|6Wd~S*Kgasl!;m;M1& delta 2622 zcmXZd3ry8z7{~Gd;qXV$w&rDKGZpYcYG5K>@`89l#X4|QG}OEz-ftkHpman<1ZNs&2%%(RJ`T7$of9=d&k53oX`Jx-}Aoz_hr#Qx}I%%y3M8fe(}{?*WW4#BfOzo1>wJiJk_Do32si9FU)bA=ZKM! z{Gx3RcN`CW8=|g}<20tU!v0n3x^S6w%(Sau+I+@?6F^6osk zVymYek|mPw;;WyfKP>A(zEejL3O4QGHV+)p>^*t3hexgHkE*;Ed9}(v#q7&_69L)h z8ruiUkR8+5m#}|yn#W~*u@Wqr{!7edDyJVGT7!0;b{Mve{zN+s{uZ-0IDm+i)v7KT zi<5mX8;Ct4J1I-STFQRVe1ot{5gGiZod#`pgNa`_{aE_5SPS|g#8ushYnpHNnYopc4UM8=R z-Icw9l{8quxhM32@hZG8OUL%HV7nj>WcC`WZB?{~GCz4+*V%_r}(h zMI6$pdN8XSPoxKgUKLINQ)S_@iP$jAu9Yi$jeL;mB4m@W-m*rr$=FPtexz&)woMj@ z5>5rTf6=WY2RQ0 zXC@z3>)bnlmd+wxl*MWAY%HAfv~ztPYe_$caAnD=n~SAzbk+uHzIo(>WP@e%u^~~M zzqM2iUO?WOv$HcCCR<2;M6Y$YY!Oz&Td-F-0^`S7Ot|bWhu){UT=E;*NV+T!`$Luy z5H0~X3H_M8)_md}*=W@*#V%_nV`K%`4OynfF2fec#>tjrpRj0Ke3r%r3i)tQI9^zU z-PRXRkgdS(V78-)m_1%e>`>ig)fHn^m~Ci^>~-=?7Gn#YirMDu#eWs`q~lVla25E7 z!M1>G%;qj3>PTz>GgY^m{GM!w=iW7V&4Nij_eiMD=Z1TuoKX!0(V@qFcSz?bzkLJky--?_o96W- zuCFY1LzTr5@sUmHO1XWsAIl { - return (typeof schema.example === 'string' || typeof schema.example === 'number' || typeof schema.example === 'boolean') - } + return ( + typeof schema.example === 'string' || + typeof schema.example === 'number' || + typeof schema.example === 'boolean' + ); + }; return ( ) : null} {shouldDisplayExample(schema) ? ( - Example: {JSON.stringify(schema.example)} + + Example: {JSON.stringify(schema.example)} + ) : null} } diff --git a/src/app/(global)/~gitbook/image/route.ts b/src/app/(global)/~gitbook/image/route.ts index 429274564..dae9da5c6 100644 --- a/src/app/(global)/~gitbook/image/route.ts +++ b/src/app/(global)/~gitbook/image/route.ts @@ -1,6 +1,11 @@ import { NextRequest } from 'next/server'; -import { verifyImageSignature, resizeImage, CloudflareImageOptions, checkIsSizableImageURL } from '@/lib/images'; +import { + verifyImageSignature, + resizeImage, + CloudflareImageOptions, + checkIsSizableImageURL, +} from '@/lib/images'; import { parseImageAPIURL } from '@/lib/urls'; export const runtime = 'edge'; diff --git a/src/app/(space)/(content)/[[...pathname]]/page.tsx b/src/app/(space)/(content)/[[...pathname]]/page.tsx index 321c701fd..8385ca556 100644 --- a/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -27,6 +27,7 @@ export default async function Page(props: { params: PagePathParams }) { content: contentPointer, contentTarget, space, + parent, customization, pages, page, @@ -84,6 +85,7 @@ export default async function Page(props: { params: PagePathParams }) { {page.layout.outline ? ( (null); + const [visible, setVisible] = React.useState(false); + const [failed, setFailed] = React.useState(false); + const [ad, setAd] = React.useState(undefined); + + // Observe the container visibility + React.useEffect(() => { + if (!containerRef.current) { + return; + } + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setVisible(true); + } + }, + { + root: null, + rootMargin: '0px', + threshold: 0.1, + }, + ); + + observer.observe(containerRef.current); + + return () => { + observer.disconnect(); + }; + }, []); + + // When the container is visible, + // track an impression on the ad and fetch it + React.useEffect(() => { + if (!visible) { + return; + } + + let cancelled = false; + + (async () => { + const url = new URL(`https://srv.buysellads.com/ads/${zoneId}.json`); + url.searchParams.set('segment', `placement:${placement}`); + url.searchParams.set('v', 'true'); + if (ignore) { + url.searchParams.set('ignore', 'true'); + } + + try { + const res = await fetch(url); + const json: AdsResponse = await res.json(); + + if (cancelled) { + return; + } + + const first = json.ads[0]; + if (first && 'active' in first) { + setAd(first); + } + } catch (error) { + console.error( + 'Failed to fetch ad, it might have been blocked by a ad-blocker', + error, + ); + setFailed(true); + } + })(); + + return () => { + cancelled = true; + }; + }, [visible, zoneId, ignore, placement]); + + const viaUrl = new URL('https://www.gitbook.com'); + viaUrl.searchParams.set('utm_source', 'content'); + viaUrl.searchParams.set('utm_medium', 'ads'); + viaUrl.searchParams.set('utm_campaign', spaceId); + + if (ad) { + console.log('ad', ad); + } + + return ( +
+ {ad ? ( + <> + {mode === 'classic' || !('callToAction' in ad) ? ( + + ) : ( + + )} + {ad.pixel ? : null} +

+ + Ads via GitBook + +

+ + ) : failed ? ( + + ) : null} +
+ ); +} + +/** + * Render attribution or verification pixels. + * https://docs.buysellads.com/ad-serving-api#pixels + */ +function AdPixels({ rawPixel }: { rawPixel: string }) { + const pixels = rawPixel.split('||'); + const time = String(Math.round(Date.now() / 1e4) | 0); + + return ( +
+ {pixels.map((pixel, index) => { + return ( + Ads tracking pixel + ); + })} +
+ ); +} + +/** + * Placeholder when visitor has an ad-blocker. + */ +function AdBlockerPlaceholder() { + return ( +
+
+ +

Ad disabled

+
+

+ {`It looks like you're using an adblocker. Whitelist this site to help support this + project.`} +

+
+ ); +} diff --git a/src/components/Ads/AdClassicRendering.tsx b/src/components/Ads/AdClassicRendering.tsx new file mode 100644 index 000000000..eb9585b08 --- /dev/null +++ b/src/components/Ads/AdClassicRendering.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; + +import { tcls } from '@/lib/tailwind'; + +import { AdItem } from './types'; + +/** + * Classic rendering for an ad. + */ +export function AdClassicRendering({ ad }: { ad: AdItem }) { + return ( + + {'smallImage' in ad ? ( +
+ Ads logo +
+ ) : ( +
+ Ads logo +
+ )} +
+
{ad.description}
+
+
+ ); +} diff --git a/src/components/Ads/AdCoverRendering.tsx b/src/components/Ads/AdCoverRendering.tsx new file mode 100644 index 000000000..7a2aab5f4 --- /dev/null +++ b/src/components/Ads/AdCoverRendering.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; + +import { hexToRgba } from '@/lib/colors'; +import { tcls } from '@/lib/tailwind'; + +import { AdCover } from './types'; + +/** + * Cover rendering for an ad. + */ +export function AdCoverRendering({ ad }: { ad: AdCover }) { + return ( + +
+ +
+ Large image +
+
+ {ad.company} +
+
+
{ad.companyTagline}
+
+ {ad.description} +
+
+
+ + {ad.callToAction} + +
+
+ + ); +} diff --git a/src/components/Ads/index.ts b/src/components/Ads/index.ts new file mode 100644 index 000000000..b9dc3e63a --- /dev/null +++ b/src/components/Ads/index.ts @@ -0,0 +1 @@ +export * from './Ad'; diff --git a/src/components/Ads/types.ts b/src/components/Ads/types.ts new file mode 100644 index 000000000..e4cbb3be7 --- /dev/null +++ b/src/components/Ads/types.ts @@ -0,0 +1,51 @@ +export interface AdGeneric { + active: string; + ad_via_link: string; + bannerid: string; + creativeid: string; + evenodd: string; + external_id: string; + height: string; + i: string; + identifier: string; + longimp: string; + longlink: string; + num_slots: string; + statimp: string; + statlink: string; + timestamp: string; + width: string; + zoneid: string; + zonekey: string; + rendering: 'carbon'; + pixel?: string; +} + +export interface AdClassic extends AdGeneric { + description: string; + smallImage: string; +} + +export interface AdCover extends AdGeneric { + backgroundColor: string; + backgroundHoverColor?: string; + textColor?: string; + textColorHover?: string; + callToAction: string; + company: string; + companyTagline: string; + description: string; + largeImage: string; + image?: string; + logo: string; + ctaBackgroundColor?: string; + ctaBackgroundHoverColor?: string; + ctaTextColor?: string; + ctaTextColorHover?: string; +} + +export type AdItem = AdClassic | AdCover; + +export interface AdsResponse { + ads: Array; +} diff --git a/src/components/DocumentView/Embed.tsx b/src/components/DocumentView/Embed.tsx index e0f3597cd..a949a9e0c 100644 --- a/src/components/DocumentView/Embed.tsx +++ b/src/components/DocumentView/Embed.tsx @@ -14,7 +14,7 @@ import { IntegrationBlock } from './Integration'; export async function Embed(props: BlockProps) { const { block, context, ...otherProps } = props; const nonce = headers().get('x-nonce') || undefined; - + ReactDOM.preload('https://cdn.iframe.ly/embed.js', { as: 'script', nonce }); const { data: embed } = await (context.content diff --git a/src/components/DocumentView/OpenAPI/scalar.css b/src/components/DocumentView/OpenAPI/scalar.css index e9e215216..e23957d82 100644 --- a/src/components/DocumentView/OpenAPI/scalar.css +++ b/src/components/DocumentView/OpenAPI/scalar.css @@ -5,8 +5,16 @@ rgb(var(--primary-base-300, 180 180 180)), rgb(var(--dark-base, 23 23 23)) 96% ); - --scalar-color-2: color-mix(in srgb, var(--scalar-color-1), transparent calc(100% - 100% * 0.72)); - --scalar-color-3: color-mix(in srgb, var(--scalar-color-1), transparent calc(100% - 100% * 0.4)); + --scalar-color-2: color-mix( + in srgb, + var(--scalar-color-1), + transparent calc(100% - 100% * 0.72) + ); + --scalar-color-3: color-mix( + in srgb, + var(--scalar-color-1), + transparent calc(100% - 100% * 0.4) + ); --scalar-color-accent: #007d9c; --scalar-background-1: rgb(var(--light-base, 255 255 255)); @@ -50,8 +58,16 @@ rgb(var(--primary-base-700, 70 70 70)), rgb(var(--light-base, 255 255 255)) 100% ); - --scalar-color-2: color-mix(in srgb, var(--scalar-color-1), transparent calc(100% - 100% * 0.64)); - --scalar-color-3: color-mix(in srgb, var(--scalar-color-1), transparent calc(100% - 100% * 0.4)); + --scalar-color-2: color-mix( + in srgb, + var(--scalar-color-1), + transparent calc(100% - 100% * 0.64) + ); + --scalar-color-3: color-mix( + in srgb, + var(--scalar-color-1), + transparent calc(100% - 100% * 0.4) + ); --scalar-color-accent: #50b7e0; --scalar-background-1: rgb(var(--dark-base, 22 22 22)); @@ -330,4 +346,4 @@ .scalar .custom-scroll { padding-right: 12px; } -} \ No newline at end of file +} diff --git a/src/components/PageAside/PageAside.tsx b/src/components/PageAside/PageAside.tsx index 28e3b0306..cae2de1af 100644 --- a/src/components/PageAside/PageAside.tsx +++ b/src/components/PageAside/PageAside.tsx @@ -6,6 +6,7 @@ import { CustomizationSettings, JSONDocument, RevisionPageDocument, + Site, SiteCustomizationSettings, Space, } from '@gitbook/api'; @@ -17,9 +18,10 @@ import { getDocumentSections } from '@/lib/document'; import { absoluteHref } from '@/lib/links'; import { ContentRefContext, resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; -import { getPDFUrl, getPDFUrlSearchParams } from '@/lib/urls'; +import { getPDFUrlSearchParams } from '@/lib/urls'; import { ScrollSectionsList } from './ScrollSectionsList'; +import { Ad } from '../Ads'; import { PageFeedbackForm } from '../PageFeedback'; /** @@ -27,6 +29,7 @@ import { PageFeedbackForm } from '../PageFeedback'; */ export async function PageAside(props: { space: Space; + site: Site | undefined; customization: CustomizationSettings | SiteCustomizationSettings; page: RevisionPageDocument; document: JSONDocument | null; @@ -35,8 +38,16 @@ export async function PageAside(props: { withFullPageCover: boolean; withPageFeedback: boolean; }) { - const { space, page, document, customization, withHeaderOffset, withPageFeedback, context } = - props; + const { + space, + site, + page, + document, + customization, + withHeaderOffset, + withPageFeedback, + context, + } = props; const language = getSpaceLanguage(customization); return ( @@ -172,6 +183,15 @@ export async function PageAside(props: { ) : null}
+ {site?.ads ? ( + + ) : null} ); } diff --git a/src/lib/colors.ts b/src/lib/colors.ts index b798c2b1d..25a1ca0d8 100644 --- a/src/lib/colors.ts +++ b/src/lib/colors.ts @@ -16,6 +16,15 @@ export function hexToRgb(hex: string): string { return `${r} ${g} ${b}`; } +/** + * Convert a hex color to a RGBA color. + */ +export function hexToRgba(hex: string, alpha: number): string { + const [r, g, b] = hexToRgbArray(hex); + // Return the RGBA values separated by spaces + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + /** * Generate Tailwind-compatible shades from a single color * @param {string} hex The hex code to generate shades from diff --git a/src/lib/csp.ts b/src/lib/csp.ts index 4cbd46c57..4f7d2ceff 100644 --- a/src/lib/csp.ts +++ b/src/lib/csp.ts @@ -38,7 +38,7 @@ export function getContentSecurityPolicy(scripts: SpaceIntegrationScript[], nonc script-src 'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-inline' 'unsafe-eval' ${assetsDomain} https://integrations.gitbook.com https://cdn.iframe.ly; style-src 'self' ${assetsDomain} fonts.googleapis.com 'unsafe-inline'; img-src * 'self' blob: data: files.gitbook.com ${assetsDomain}; - connect-src * 'self' integrations.gitbook.com app.gitbook.com api.gitbook.com ${assetsDomain}; + connect-src * 'self' integrations.gitbook.com app.gitbook.com api.gitbook.com srv.buysellads.com ${assetsDomain}; font-src 'self' fonts.gstatic.com ${assetsDomain}; frame-src *; object-src 'none';