From 35d781120574aacdd95c1e60fbc8c0da368a72e7 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Sun, 21 Jun 2026 13:37:04 +0300 Subject: [PATCH 01/38] feat(daily): always-expanded feed card glass actions Render the floating glass action bar on feed cards fully expanded by default instead of hover-to-reveal, and drop the appearance animation. - FeedCardGlassActions: remove the morph/fade transitions, the hover-collapse grid tracks, and the Segment wrapper. Every action now gets an equal-width centered slot so icons stay evenly spaced regardless of upvote/comment counts. - ArticleGrid: clamp glass-card titles to 2 lines so long titles don't crowd the card. - AdGrid: move the CTA / advertise / remove-ad row above the cover image to match the new card design. - CardCoverShare: pad the "Should anyone else see this post?" overlay (pb-12) so its buttons clear the bottom action bar. Co-Authored-By: Claude Opus 4.8 --- .../shared/src/components/cards/ad/AdGrid.tsx | 2 +- .../components/cards/article/ArticleGrid.tsx | 6 +- .../cards/common/CardCoverShare.tsx | 5 +- .../cards/common/FeedCardGlassActions.tsx | 182 +++++++----------- 4 files changed, 78 insertions(+), 117 deletions(-) diff --git a/packages/shared/src/components/cards/ad/AdGrid.tsx b/packages/shared/src/components/cards/ad/AdGrid.tsx index dae02d7aa0d..953bed6e6bb 100644 --- a/packages/shared/src/components/cards/ad/AdGrid.tsx +++ b/packages/shared/src/components/cards/ad/AdGrid.tsx @@ -56,7 +56,6 @@ export const AdGrid = forwardRef(function AdGrid( ) : null} -
{!!ad.callToAction && ( @@ -89,6 +88,7 @@ export const AdGrid = forwardRef(function AdGrid(
+ ); diff --git a/packages/shared/src/components/cards/article/ArticleGrid.tsx b/packages/shared/src/components/cards/article/ArticleGrid.tsx index 272bd5ed3b3..9eaa6109449 100644 --- a/packages/shared/src/components/cards/article/ArticleGrid.tsx +++ b/packages/shared/src/components/cards/article/ArticleGrid.tsx @@ -137,7 +137,11 @@ export const ArticleGrid = forwardRef(function ArticleGrid( onReadArticleClick={onReadArticleClick} showFeedback={showFeedback} /> - + {title} diff --git a/packages/shared/src/components/cards/common/CardCoverShare.tsx b/packages/shared/src/components/cards/common/CardCoverShare.tsx index 9fe6b75c6c0..d02a048d1d2 100644 --- a/packages/shared/src/components/cards/common/CardCoverShare.tsx +++ b/packages/shared/src/components/cards/common/CardCoverShare.tsx @@ -26,7 +26,10 @@ export function CardCoverShare({ }; return ( - + + )} + + ); }; diff --git a/packages/shared/src/features/googleCloudTakeover/content.ts b/packages/shared/src/features/googleCloudTakeover/content.ts index f75388e2b52..ca9dc37402f 100644 --- a/packages/shared/src/features/googleCloudTakeover/content.ts +++ b/packages/shared/src/features/googleCloudTakeover/content.ts @@ -11,6 +11,10 @@ const googleCloudBlogUrl = 'https://cloud.google.com/blog/topics/inside-google-cloud/whats-new-google-cloud'; const googleCloudBlogImage = 'https://storage.googleapis.com/gweb-cloudblog-publish/images/whats_new_2026_CfhxFWX.max-2500x2500.jpg'; +// A different Google Cloud blog cover for the ad slot, so it doesn't repeat +// the sponsored blog card's image. +const googleCloudAdImage = + 'https://storage.googleapis.com/gweb-cloudblog-publish/images/1148-GC-IO-Header-GC-43-0519.max-2500x2500.jpg'; // Rendered through the real ArticleGrid/ArticleList so the sponsored post // looks identical to an organic feed card. The Google Cloud logo is supplied @@ -45,7 +49,7 @@ export const googleCloudAd: Ad = { 'Build what’s next. Ship faster with serverless, AI, and data tools trusted by developers worldwide.', link: 'https://cloud.google.com/free', source: 'Google Cloud', - image: googleCloudBlogImage, + image: googleCloudAdImage, companyLogo: googleCloudLogoDataUri, callToAction: 'Start building free', }; From ea8f080fa88d4bfa88fd3eefc8a7db79f5073520 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 13:05:19 +0300 Subject: [PATCH 11/38] fix: glass action bar buttons overlapping/overflowing on narrow cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bar forced six equal-width flex-1 slots, each 1/6 of the pill. On tight (e.g. 5-column) cards a count-bearing button (upvote/comment) is wider than its slot, so buttons overflowed their slots — overlapping neighbours and spilling past the rounded edge. Size each action to its content and spread the row with justify-between (the standard action-bar pattern), which fits all six without overlap from 272px up. Co-Authored-By: Claude Opus 4.8 --- .../components/cards/common/FeedCardGlassActions.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx b/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx index 9a63092fe53..0e6bec0d136 100644 --- a/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx +++ b/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx @@ -29,16 +29,19 @@ const outerClasses = 'pointer-events-none absolute inset-x-2 bottom-2 z-1'; // hover/pressed colors are left to each `btn-tertiary-*` class so icons keep // their brand tint on hover, matching the standard ActionButtons. const pillClasses = classNames( - 'pointer-events-auto flex h-10 w-full items-center overflow-hidden px-1', + 'pointer-events-auto flex h-10 w-full items-center justify-between gap-0.5 overflow-hidden px-1', 'rounded-12 border border-border-subtlest-tertiary', 'bg-blur-bg text-text-primary backdrop-blur-xl backdrop-saturate-150', '[&_.btn-quaternary]:[--button-default-color:var(--theme-text-primary)]', '[&_.btn]:[--button-default-color:var(--theme-text-primary)]', ); -// Every action gets an equal-width centered slot so the icons stay evenly spaced -// across the pill regardless of upvote/comment counts widening a button. -const slotClasses = 'flex min-w-0 flex-1 items-center justify-center'; +// Each action sizes to its content (so the count inside the upvote/comment +// button isn't clipped) and the row spreads them with `justify-between`. An +// earlier equal-width `flex-1` layout forced every slot to 1/N of the pill, +// which is narrower than a count-bearing button on tight (e.g. 5-column) +// cards — so the buttons overflowed their slots and overlapped. +const slotClasses = 'flex min-w-0 shrink items-center justify-center'; // Dark glow behind the pill so it stays readable over busy cover images. Fixed // pepper tint in both themes; inline gradient since it's a one-off scrim. From 086d82efd00bc7fcf0203c13954f3c300726005a Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 13:07:03 +0300 Subject: [PATCH 12/38] chore: remove em dashes from Google Cloud takeover copy Co-Authored-By: Claude Opus 4.8 --- .../src/features/googleCloudTakeover/GoogleCloudTakeover.tsx | 4 ++-- packages/shared/src/features/googleCloudTakeover/content.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudTakeover.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudTakeover.tsx index dec3240141a..51e763127b5 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudTakeover.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudTakeover.tsx @@ -23,7 +23,7 @@ const topPosts: MockPost[] = [ { source: 'CSS-Tricks', title: - 'Container queries are finally everywhere — here is the mental model that clicks', + 'Container queries are finally everywhere: the mental model that clicks', tag: '#webdev', meta: 'Jun 21 · 7 min read', avatar: 'bg-accent-bun-default', @@ -59,7 +59,7 @@ const bottomPosts: MockPost[] = [ { source: 'Rust Blog', title: - 'Async traits land in stable Rust — what changes for library authors', + 'Async traits land in stable Rust: what changes for library authors', tag: '#rust', meta: 'Jun 20 · 11 min read', avatar: 'bg-accent-ketchup-default', diff --git a/packages/shared/src/features/googleCloudTakeover/content.ts b/packages/shared/src/features/googleCloudTakeover/content.ts index ca9dc37402f..2a4a943b997 100644 --- a/packages/shared/src/features/googleCloudTakeover/content.ts +++ b/packages/shared/src/features/googleCloudTakeover/content.ts @@ -57,7 +57,7 @@ export const googleCloudAd: Ad = { // Shared messaging for the announcement bar + in-feed strip. export const googleCloudMessage = { title: 'Google Cloud supports developers', - body: 'Get $300 in free credits to build, test, and ship your next project on Google Cloud — on us.', + body: 'Get $300 in free credits to build, test, and ship your next project on Google Cloud, on us.', cta: 'Claim credits', ctaUrl: 'https://cloud.google.com/free', }; From 6136b59a3a5838367e8fceba7ece31c500b7e144 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 13:10:18 +0300 Subject: [PATCH 13/38] fix: order ad attribution as Promoted then Advertise here Co-Authored-By: Claude Opus 4.8 --- .../src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx index 899a09f8e35..ff90dee1ff4 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx @@ -74,12 +74,12 @@ export const GoogleCloudHeadAd = ({
+ - {!isPlus && ( Date: Mon, 22 Jun 2026 13:11:19 +0300 Subject: [PATCH 14/38] chore: shorten Google Cloud ad title to avoid truncation Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/features/googleCloudTakeover/content.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/shared/src/features/googleCloudTakeover/content.ts b/packages/shared/src/features/googleCloudTakeover/content.ts index 2a4a943b997..c41957eb110 100644 --- a/packages/shared/src/features/googleCloudTakeover/content.ts +++ b/packages/shared/src/features/googleCloudTakeover/content.ts @@ -45,8 +45,7 @@ export const googleCloudBlogPost: Post = { // `companyLogo` drives the favicon; `image` drives the cover. export const googleCloudAd: Ad = { company: 'Google Cloud', - description: - 'Build what’s next. Ship faster with serverless, AI, and data tools trusted by developers worldwide.', + description: 'Build what’s next on Google Cloud', link: 'https://cloud.google.com/free', source: 'Google Cloud', image: googleCloudAdImage, From f615f920529b094580bfde64ae1f35bc805249b8 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 13:25:08 +0300 Subject: [PATCH 15/38] feat: center announcement-bar CTA and ad hover CTA - Announcement bar: center the logo/text/CTA group and pin the close button to the far-right corner, with a short bar body so "Claim credits" sits centrally and clear of the X (full message kept on the in-feed strip). - Ad card: center the "Start building free" hover CTA over the cover image with equal padding, instead of pinning it to the corner. Co-Authored-By: Claude Opus 4.8 --- .../GoogleCloudAnnouncementBar.tsx | 57 ++++++++++--------- .../googleCloudTakeover/GoogleCloudHeadAd.tsx | 40 ++++++------- .../GoogleCloudTakeover.tsx | 3 +- .../features/googleCloudTakeover/content.ts | 5 +- 4 files changed, 57 insertions(+), 48 deletions(-) diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudAnnouncementBar.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudAnnouncementBar.tsx index 8525cab8668..b2988d7bf2a 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudAnnouncementBar.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudAnnouncementBar.tsx @@ -29,44 +29,49 @@ export const GoogleCloudAnnouncementBar = ({ return null; } - const { title, body, cta, ctaUrl } = googleCloudMessage; + const { title, barBody, cta, ctaUrl } = googleCloudMessage; return (
-
- -
-
- - {title} - - +
+ +
+
+ + {title} + + + {barBody} + +
+ - {body} -
+ {cta} +
- - {cta} - setDismissed(true)} />
diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx index ff90dee1ff4..d2af1b666aa 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx @@ -89,25 +89,27 @@ export const GoogleCloudHeadAd = ({ )}
- - {!!googleCloudAd.callToAction && ( - - )} +
+ + {!!googleCloudAd.callToAction && ( + + )} +
diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudTakeover.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudTakeover.tsx index 51e763127b5..f9b3f573714 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudTakeover.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudTakeover.tsx @@ -58,8 +58,7 @@ const topPosts: MockPost[] = [ const bottomPosts: MockPost[] = [ { source: 'Rust Blog', - title: - 'Async traits land in stable Rust: what changes for library authors', + title: 'Async traits land in stable Rust: what changes for library authors', tag: '#rust', meta: 'Jun 20 · 11 min read', avatar: 'bg-accent-ketchup-default', diff --git a/packages/shared/src/features/googleCloudTakeover/content.ts b/packages/shared/src/features/googleCloudTakeover/content.ts index c41957eb110..98a18bca0b9 100644 --- a/packages/shared/src/features/googleCloudTakeover/content.ts +++ b/packages/shared/src/features/googleCloudTakeover/content.ts @@ -53,9 +53,12 @@ export const googleCloudAd: Ad = { callToAction: 'Start building free', }; -// Shared messaging for the announcement bar + in-feed strip. +// Shared messaging for the announcement bar + in-feed strip. The bar uses a +// short body so the centered logo/text/CTA group stays compact and the CTA +// sits centrally, clear of the close button. export const googleCloudMessage = { title: 'Google Cloud supports developers', + barBody: 'Get $300 in free credits, on us.', body: 'Get $300 in free credits to build, test, and ship your next project on Google Cloud, on us.', cta: 'Claim credits', ctaUrl: 'https://cloud.google.com/free', From f895d9044839945c93ad23b50d746d34abc1c24b Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 13:35:29 +0300 Subject: [PATCH 16/38] feat: open Google Cloud blog post as a regular post modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking the sponsored Google Cloud post now opens the standard article post modal (like any other post), populated from the post object — PostContent renders from the passed post and usePostById falls back to initialData, so no backend post is required. The modal's "Read post" link, the card's read action, and cmd/ctrl-click all redirect to the Google Cloud blog. Added a summary so the modal shows richer content. Co-Authored-By: Claude Opus 4.8 --- .../GoogleCloudBlogCard.tsx | 61 +++++++++++++------ .../features/googleCloudTakeover/content.ts | 2 + 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx index 20c2ed3edb5..38faec1108f 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx @@ -1,7 +1,9 @@ import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useState } from 'react'; import { ArticleGrid } from '../../components/cards/article/ArticleGrid'; import { ArticleList } from '../../components/cards/article/ArticleList'; +import ArticlePostModal from '../../components/modals/ArticlePostModal'; +import { PostPosition } from '../../hooks/usePostModalNavigation'; import { googleCloudBlogPost } from './content'; type GoogleCloudBlogCardProps = { @@ -11,30 +13,51 @@ type GoogleCloudBlogCardProps = { const noop = () => undefined; -// The sponsored Google Cloud blog post, rendered through the real -// ArticleGrid/ArticleList so it is visually identical to an organic feed -// card (source row, title, tags, metadata, cover, engagement bar) with no -// "Sponsored" treatment. Interactions are no-ops for the demo. +const openBlog = () => { + if (typeof window !== 'undefined') { + window.open(googleCloudBlogPost.permalink, '_blank', 'noopener,noreferrer'); + } +}; + +// The sponsored Google Cloud blog post. Renders the real ArticleGrid/ArticleList +// so it's identical to an organic card, and behaves like a normal post: +// clicking the card opens the standard article post modal (populated from the +// post object via usePostById's initialData fallback), while "Read post" and +// the modal's read action redirect to the Google Cloud blog. export const GoogleCloudBlogCard = ({ isList = false, className, }: GoogleCloudBlogCardProps): ReactElement => { + const [isModalOpen, setIsModalOpen] = useState(false); const Card = isList ? ArticleList : ArticleGrid; return ( - + <> + setIsModalOpen(true)} + onPostAuxClick={openBlog} + onUpvoteClick={noop} + onDownvoteClick={noop} + onCommentClick={() => setIsModalOpen(true)} + onBookmarkClick={noop} + onShare={noop} + onCopyLinkClick={noop} + onReadArticleClick={openBlog} + onMenuClick={noop} + domProps={{ className }} + /> + {isModalOpen && ( + setIsModalOpen(false)} + /> + )} + ); }; diff --git a/packages/shared/src/features/googleCloudTakeover/content.ts b/packages/shared/src/features/googleCloudTakeover/content.ts index 98a18bca0b9..c9880c5b24d 100644 --- a/packages/shared/src/features/googleCloudTakeover/content.ts +++ b/packages/shared/src/features/googleCloudTakeover/content.ts @@ -22,6 +22,8 @@ const googleCloudAdImage = export const googleCloudBlogPost: Post = { id: 'gcp-blog-demo', title: "What's new with Google Cloud", + summary: + 'A roundup of the latest launches, updates, and resources from Google Cloud: agentic AI, Gemini Enterprise, Spot VM optimization, and more.', permalink: googleCloudBlogUrl, commentsPermalink: googleCloudBlogUrl, createdAt: '2026-06-20T09:00:00.000Z', From 3b5aae32eb00bf8f16078a15c7c2624b2bd4e9c4 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 13:40:57 +0300 Subject: [PATCH 17/38] fix: place ad CTA at top-right on the source line with equal insets Move the 'Start building free' hover button from the image center to the top-right corner, vertically aligned with the Google Cloud logo/source row (matches the read-post affordance), with equal top and right insets. Co-Authored-By: Claude Opus 4.8 --- .../googleCloudTakeover/GoogleCloudHeadAd.tsx | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx index d2af1b666aa..e36580a2931 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx @@ -89,27 +89,25 @@ export const GoogleCloudHeadAd = ({ )} -
- - {!!googleCloudAd.callToAction && ( - - )} -
+ + {!!googleCloudAd.callToAction && ( + + )}
From 4087cad9ccfcc45f7b91607cc4715f19ad59229a Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 14:13:41 +0300 Subject: [PATCH 18/38] feat: uniform feed row heights on the takeover home feed A tall card in a row (e.g. the "Happening Now" highlights widget) stretched all its neighbours, expanding the article cards' title/tags spacer and making that row taller than the others. Pin the grid's implicit rows to the glass card height (21.5rem) and clip overflow, scoped to the takeover home feed, so every row is the same height. Tall widgets clip within their cell. Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/Feed.tsx | 1 + .../shared/src/components/feeds/FeedContainer.tsx | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index fa8699d5747..cf1e58a106a 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -705,6 +705,7 @@ export default function Feed({ feedContainerRef, showBriefCard, disableListFrame, + uniformCardRows: showGoogleCloudTakeover, }; return ( diff --git a/packages/shared/src/components/feeds/FeedContainer.tsx b/packages/shared/src/components/feeds/FeedContainer.tsx index 89990cc5796..853f741a729 100644 --- a/packages/shared/src/components/feeds/FeedContainer.tsx +++ b/packages/shared/src/components/feeds/FeedContainer.tsx @@ -47,6 +47,9 @@ export interface FeedContainerProps { feedContainerRef?: React.Ref; showBriefCard?: boolean; disableListFrame?: boolean; + // DEMO: force uniform grid row heights so a tall card in a row doesn't + // stretch its neighbours (used by the Google Cloud takeover home feed). + uniformCardRows?: boolean; } const listGapClass = 'gap-2'; @@ -133,6 +136,7 @@ export const FeedContainer = ({ feedContainerRef, showBriefCard, disableListFrame = false, + uniformCardRows = false, }: FeedContainerProps): ReactElement => { const currentSettings = useContext(FeedContext); const { subject } = useToastNotification(); @@ -366,6 +370,14 @@ export const FeedContainer = ({ !isAnyExplore && !isV2Laptop && 'mt-8', + // Uniform row heights: pin every implicit row to the glass + // card height and clip overflow so a tall card (e.g. the + // highlights widget) can't stretch its row and leave the + // neighbours with an expanded title/tags gap. + uniformCardRows && + !isList && + !isHorizontal && + 'auto-rows-[21.5rem] [&>*]:overflow-hidden', isHorizontal && 'no-scrollbar snap-x snap-mandatory grid-flow-col overflow-x-scroll scroll-smooth py-2 pt-5', gapClass({ From bb440076babf4fcd3822aaeec5296732e086e142 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 14:16:34 +0300 Subject: [PATCH 19/38] Revert "feat: uniform feed row heights on the takeover home feed" This reverts commit 4087cad9ccfcc45f7b91607cc4715f19ad59229a. --- packages/shared/src/components/Feed.tsx | 1 - .../shared/src/components/feeds/FeedContainer.tsx | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index cf1e58a106a..fa8699d5747 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -705,7 +705,6 @@ export default function Feed({ feedContainerRef, showBriefCard, disableListFrame, - uniformCardRows: showGoogleCloudTakeover, }; return ( diff --git a/packages/shared/src/components/feeds/FeedContainer.tsx b/packages/shared/src/components/feeds/FeedContainer.tsx index 853f741a729..89990cc5796 100644 --- a/packages/shared/src/components/feeds/FeedContainer.tsx +++ b/packages/shared/src/components/feeds/FeedContainer.tsx @@ -47,9 +47,6 @@ export interface FeedContainerProps { feedContainerRef?: React.Ref; showBriefCard?: boolean; disableListFrame?: boolean; - // DEMO: force uniform grid row heights so a tall card in a row doesn't - // stretch its neighbours (used by the Google Cloud takeover home feed). - uniformCardRows?: boolean; } const listGapClass = 'gap-2'; @@ -136,7 +133,6 @@ export const FeedContainer = ({ feedContainerRef, showBriefCard, disableListFrame = false, - uniformCardRows = false, }: FeedContainerProps): ReactElement => { const currentSettings = useContext(FeedContext); const { subject } = useToastNotification(); @@ -370,14 +366,6 @@ export const FeedContainer = ({ !isAnyExplore && !isV2Laptop && 'mt-8', - // Uniform row heights: pin every implicit row to the glass - // card height and clip overflow so a tall card (e.g. the - // highlights widget) can't stretch its row and leave the - // neighbours with an expanded title/tags gap. - uniformCardRows && - !isList && - !isHorizontal && - 'auto-rows-[21.5rem] [&>*]:overflow-hidden', isHorizontal && 'no-scrollbar snap-x snap-mandatory grid-flow-col overflow-x-scroll scroll-smooth py-2 pt-5', gapClass({ From d415e6fe7a734581586f08150647ce0ccaffa14d Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 14:27:31 +0300 Subject: [PATCH 20/38] fix: remove empty feed cells caused by the takeover injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The injected blog card + ad shift the grid, which desynced the feed's wide/hero-card placements and the full-row briefing banner — so those items couldn't fit the remaining columns and wrapped, leaving an empty cell in the preceding row. When the takeover is active, force single-column cards and skip the briefing banner so every cell packs cleanly; the only full-row item is the GCP strip, positioned on a row boundary. Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/Feed.tsx | 32 +++++++++++++++---------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index fa8699d5747..1085702b901 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -345,10 +345,11 @@ export default function Feed({ if (cells >= targetCells && cells % virtualizedNumCards === 0) { return i; } - cells += itemPlacements[i]?.colSpan ?? 1; + // Takeover forces single-column cards, so every item is one cell. + cells += 1; } return -1; - }, [showGoogleCloudTakeover, virtualizedNumCards, items, itemPlacements]); + }, [showGoogleCloudTakeover, virtualizedNumCards, items]); // Experiment: let the browser skip layout/paint for off-screen cards on long // vertical feeds. Horizontal carousels are short and scroll on the other axis, @@ -734,7 +735,12 @@ export default function Feed({ )} {items.map((item, index) => { const placement = itemPlacements[index]; - const { colSpan } = placement; + // DEMO: the takeover injects extra cells (blog card, ad), which + // shifts the grid and desyncs the feed's wide-card placements, + // leaving empty cells. Force single-column cards so every cell + // packs cleanly; the only full-row item is the GCP strip, which + // is positioned on a row boundary. + const colSpan = showGoogleCloudTakeover ? 1 : placement.colSpan; const isWidened = colSpan > 1; const wideColSpan = isWidened && (colSpan === 2 || colSpan === 3 || colSpan === 4) @@ -807,15 +813,17 @@ export default function Feed({ : undefined, }} > - {showPromoBanner && index === indexWhenShowingPromoBanner && ( - - )} + {showPromoBanner && + !showGoogleCloudTakeover && + index === indexWhenShowingPromoBanner && ( + + )} {showGoogleCloudTakeover && index === 1 && ( )} From 7b6dca3b6d934a0f6feb8d7d4568c1db5c34ccae Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 14:56:43 +0300 Subject: [PATCH 21/38] feat: takeover engagement polish (no organic ads, fake discussion, ad cleanup) - Suppress the feed's organic ads during the takeover so the only ad is the Google Cloud slot (non-Plus users no longer see extra ads). - Seed a simulated 48-comment discussion into the blog post's comment cache (pinned with staleTime: Infinity so a refetch can't clear it) so the modal shows engagement; original placeholder comment copy. - Blog card: upvote / downvote / bookmark now toggle locally so the icons and counts react for the demo. - Ad card: remove the "Advertise here" and "Remove" actions; the only attribution is "Promoted", styled to match the date / read-time metadata of organic post cards (text-text-tertiary, typo-footnote, same placement). Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/Feed.tsx | 5 +- .../GoogleCloudBlogCard.tsx | 52 ++++++-- .../googleCloudTakeover/GoogleCloudHeadAd.tsx | 31 +---- .../googleCloudTakeover/fakeDiscussion.ts | 114 ++++++++++++++++++ 4 files changed, 165 insertions(+), 37 deletions(-) create mode 100644 packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index 1085702b901..5759131edca 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -310,7 +310,10 @@ export default function Feed({ firstSlotOffset: Number(showFirstSlotCard), disableTopHero: isV2, settings: { - disableAds, + // DEMO: suppress the feed's organic ads during the takeover so the + // only ad is the injected Google Cloud slot (avoids extra ads that + // non-Plus users would otherwise see). + disableAds: disableAds || showGoogleCloudTakeover, staticAd, adPostLength: isSquadFeed ? 2 : undefined, showAcquisitionForm, diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx index 38faec1108f..1b2ddb0c35e 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx @@ -1,10 +1,14 @@ import type { ReactElement } from 'react'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; import { ArticleGrid } from '../../components/cards/article/ArticleGrid'; import { ArticleList } from '../../components/cards/article/ArticleList'; import ArticlePostModal from '../../components/modals/ArticlePostModal'; import { PostPosition } from '../../hooks/usePostModalNavigation'; +import type { Post } from '../../graphql/posts'; +import { UserVote } from '../../graphql/posts'; import { googleCloudBlogPost } from './content'; +import { seedGoogleCloudDiscussion } from './fakeDiscussion'; type GoogleCloudBlogCardProps = { isList?: boolean; @@ -20,27 +24,53 @@ const openBlog = () => { }; // The sponsored Google Cloud blog post. Renders the real ArticleGrid/ArticleList -// so it's identical to an organic card, and behaves like a normal post: -// clicking the card opens the standard article post modal (populated from the -// post object via usePostById's initialData fallback), while "Read post" and -// the modal's read action redirect to the Google Cloud blog. +// so it's identical to an organic card and behaves like a normal post: +// - clicking opens the standard article post modal (with a simulated 48-comment +// discussion seeded for the demo), and "Read post" redirects to the blog; +// - upvote / downvote / bookmark toggle locally so engagement is demo-able. export const GoogleCloudBlogCard = ({ isList = false, className, }: GoogleCloudBlogCardProps): ReactElement => { + const queryClient = useQueryClient(); const [isModalOpen, setIsModalOpen] = useState(false); + const [post, setPost] = useState(googleCloudBlogPost); const Card = isList ? ArticleList : ArticleGrid; + // Seed the simulated discussion so the post modal shows engagement. + useEffect(() => { + seedGoogleCloudDiscussion(queryClient, googleCloudBlogPost.id); + }, [queryClient]); + + const toggleVote = (vote: UserVote) => + setPost((current) => { + const isActive = current.userState?.vote === vote; + const wasUpvote = current.userState?.vote === UserVote.Up; + const willUpvote = !isActive && vote === UserVote.Up; + const upvoteDelta = (willUpvote ? 1 : 0) - (wasUpvote ? 1 : 0); + return { + ...current, + numUpvotes: Math.max(0, (current.numUpvotes ?? 0) + upvoteDelta), + userState: { + ...current.userState, + vote: isActive ? UserVote.None : vote, + }, + }; + }); + + const toggleBookmark = () => + setPost((current) => ({ ...current, bookmarked: !current.bookmarked })); + return ( <> setIsModalOpen(true)} onPostAuxClick={openBlog} - onUpvoteClick={noop} - onDownvoteClick={noop} + onUpvoteClick={() => toggleVote(UserVote.Up)} + onDownvoteClick={() => toggleVote(UserVote.Down)} onCommentClick={() => setIsModalOpen(true)} - onBookmarkClick={noop} + onBookmarkClick={toggleBookmark} onShare={noop} onCopyLinkClick={noop} onReadArticleClick={openBlog} @@ -50,8 +80,8 @@ export const GoogleCloudBlogCard = ({ {isModalOpen && ( undefined; const adFeedContext = { items: [], queryKey: ['gcp-takeover-ad'] }; // The Google Cloud ad slot. Built from the real ad sub-components so it reads -// like a production ad card, with two takeover tweaks: +// like a production ad card, with takeover tweaks: // - the CTA ("Start building free") is hidden and revealed on hover in the // top-right corner (mirrors the post card's "Read post" hover affordance); -// - the advertiser label is left-aligned. +// - the only attribution is "Promoted", styled to match the date / read-time +// metadata of organic post cards (no "Advertise here" / "Remove"). // On list/mobile layout there's no hover, so fall back to the standard AdList. export const GoogleCloudHeadAd = ({ isList = false, className, }: GoogleCloudHeadAdProps): ReactElement => { - const { isPlus } = usePlusSubscription(); - if (isList) { return ( @@ -71,22 +65,9 @@ export const GoogleCloudHeadAd = ({ {googleCloudAd.description}
- - -
- - - {!isPlus && ( - - )} + {/* Match the exact look of a post card's date / read-time line. */} +
+ Promoted
+ `data:image/svg+xml,${encodeURIComponent( + `${initials}`, + )}`; + +const people = [ + { name: 'Priya Sharma', username: 'priyabuilds', color: '#4285F4' }, + { name: 'Marcus Lee', username: 'marcusdev', color: '#EA4335' }, + { name: 'Sofia Alvarez', username: 'sofiacodes', color: '#34A853' }, + { name: 'Tom Becker', username: 'tbecker', color: '#FBBC04' }, + { name: 'Aisha Khan', username: 'aishak', color: '#A142F4' }, + { name: 'Daniel Park', username: 'dpark', color: '#00ACC1' }, + { name: 'Lena Novak', username: 'lenan', color: '#F4511E' }, + { name: 'Owen Wright', username: 'owenw', color: '#3949AB' }, + { name: 'Rina Tanaka', username: 'rinat', color: '#00897B' }, + { name: 'Caleb Stone', username: 'cstone', color: '#8E24AA' }, + { name: 'Mira Patel', username: 'mirap', color: '#43A047' }, + { name: 'Jonas Vogel', username: 'jvogel', color: '#FB8C00' }, +]; + +const remarks = [ + 'The Spot VM optimization is going to cut our batch costs significantly. Already testing it this week.', + 'Finally proper in-country processing for Gemini. This unblocks a sovereign workload we shelved last year.', + 'The serverless Spark improvements look great. Cold starts were the only thing holding us back.', + 'Gemini Enterprise integration with our data warehouse is exactly what the team has been asking for.', + 'Good roundup. The agentic tooling updates alone are worth a read for anyone shipping AI features.', + 'Migrated a service to managed clusters last month — the autoscaling changes here would have saved us a lot of tuning.', + 'Curious how the new API management features compare to what we built in-house. Might be time to switch.', + 'The multi-tenant guidance is solid. We learned most of this the hard way.', + 'Bookmarking this. The links to the deep-dive posts are the real value here.', + 'Been waiting for the VS Code workbench notebooks to go GA. Local + managed compute is the dream workflow.', + 'Anyone tried the new data residency controls in production yet? Wondering about latency impact.', + 'The cost optimizer recommendations have been surprisingly accurate for our fleet. Nice to see it expanding.', + 'Great to see first-class support for this. The docs are clearer than they used to be too.', + 'We run a similar stack and the reliability numbers they quote line up with what we see.', + 'This is the kind of update that quietly makes day-to-day infra work less painful.', + 'Shared this with the platform team. A few of these land directly on our roadmap.', +]; + +const days = [ + '2026-06-20T11:40:00.000Z', + '2026-06-20T09:15:00.000Z', + '2026-06-19T18:05:00.000Z', + '2026-06-19T08:30:00.000Z', + '2026-06-18T20:50:00.000Z', +]; + +const buildComments = (count: number): Comment[] => + Array.from({ length: count }).map((_, i) => { + const person = people[i % people.length]; + const initials = person.name + .split(' ') + .map((part) => part[0]) + .join(''); + const text = remarks[i % remarks.length]; + const author: Author = { + id: `gcp-author-${i}`, + name: person.name, + username: person.username, + permalink: `https://app.daily.dev/${person.username}`, + image: avatar(person.color, initials), + } as Author; + + return { + id: `gcp-comment-${i}`, + content: text, + contentHtml: `

${text}

`, + contentEmbeds: [], + createdAt: days[i % days.length], + lastUpdatedAt: days[i % days.length], + permalink: 'https://cloud.google.com/blog', + numUpvotes: Math.max(0, 47 - i), + numAwards: 0, + author, + children: { edges: [], pageInfo: { hasNextPage: false } }, + } as Comment; + }); + +export const buildGoogleCloudDiscussion = (count = 48): PostCommentsData => ({ + postComments: { + edges: buildComments(count).map((node) => ({ node })), + pageInfo: { hasNextPage: false, endCursor: null }, + }, +}); + +// Seed every comments-query-key variant for the post and pin it so the live +// (empty) refetch can't replace the simulated discussion. +export const seedGoogleCloudDiscussion = ( + queryClient: QueryClient, + postId: string, + count = 48, +): void => { + const data = buildGoogleCloudDiscussion(count); + const keys = [ + generateCommentsQueryKey({ postId }), + ...getAllCommentsQuery(postId), + ]; + keys.forEach((key) => { + queryClient.setQueryDefaults(key, { + staleTime: Number.POSITIVE_INFINITY, + gcTime: Number.POSITIVE_INFINITY, + }); + queryClient.setQueryData(key, data); + }); +}; From f388d09b8a553ffb42f8d125fe489b71643544f4 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 15:16:33 +0300 Subject: [PATCH 22/38] fix: blog "Read post" opens the real Google Cloud blog URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reader is the user's default reading experience, so for an Article post "Read post" opened the in-app reader (which can't load the demo post). Type the sponsored post as Share (no sharedPost) — it renders identically in our directly-mounted ArticleGrid/ArticlePostModal but isn't a reader-gated type, so "Read post" links straight to post.permalink (the Google Cloud blog) and opens in a new tab. Upvote/downvote/bookmark already toggle locally. Co-Authored-By: Claude Opus 4.8 --- .../features/googleCloudTakeover/GoogleCloudBlogCard.tsx | 1 + packages/shared/src/features/googleCloudTakeover/content.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx index 1b2ddb0c35e..0a0381d4abf 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx @@ -75,6 +75,7 @@ export const GoogleCloudBlogCard = ({ onCopyLinkClick={noop} onReadArticleClick={openBlog} onMenuClick={noop} + openNewTab domProps={{ className }} /> {isModalOpen && ( diff --git a/packages/shared/src/features/googleCloudTakeover/content.ts b/packages/shared/src/features/googleCloudTakeover/content.ts index c9880c5b24d..bed28477d8a 100644 --- a/packages/shared/src/features/googleCloudTakeover/content.ts +++ b/packages/shared/src/features/googleCloudTakeover/content.ts @@ -40,7 +40,11 @@ export const googleCloudBlogPost: Post = { numUpvotes: 312, numComments: 48, numAwards: 0, - type: PostType.Article, + // Share (with no sharedPost) renders identically to an article in our + // directly-mounted ArticleGrid/ArticlePostModal, but it is NOT a + // reader-gated type — so "Read post" links straight to the Google Cloud + // blog (post.permalink) instead of opening the in-app reader. + type: PostType.Share, }; // Rendered through the real AdGrid/AdList so it matches the live ad slot. From 61bb9ac504af982c03b18ced860734bb9a7fe871 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 15:31:22 +0300 Subject: [PATCH 23/38] fix: keep Google Cloud takeover bar visible while post modal is open Gate the demo announcement bar on router.pathname instead of asPath, so the post modal (which only changes asPath to /posts/...) no longer hides the bar and causes a layout shift. Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/MainLayout.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/components/MainLayout.tsx b/packages/shared/src/components/MainLayout.tsx index 2497993fec7..7a0ae7e1645 100644 --- a/packages/shared/src/components/MainLayout.tsx +++ b/packages/shared/src/components/MainLayout.tsx @@ -126,11 +126,12 @@ function MainLayoutComponent({ }); const { plusEntryAnnouncementBar } = usePlusEntry(); // DEMO: Google Cloud takeover bar, rendered at the app layout level so it - // sits above/outside the feed's floating-card box on the home feed. + // sits above/outside the feed's floating-card box on the home feed. Gate on + // `router.pathname` (the underlying page route) rather than `asPath`. When a + // post modal opens it only changes `asPath`/query to `/posts/...` while the + // home route stays `/`, so the bar stays put and doesn't cause a layout shift. const showGoogleCloudBar = - googleCloudTakeoverEnabled && - !isTesting && - (activePage ?? router?.asPath ?? router?.pathname) === webappUrl; + googleCloudTakeoverEnabled && !isTesting && router?.pathname === webappUrl; const isLaptop = useViewSize(ViewSize.Laptop); const isLaptopXL = useViewSize(ViewSize.LaptopXL); const { screenCenteredOnMobileLayout } = useFeedLayout(); From e5790a6aff443d84d69cf735740540b1c44a8f09 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 15:45:20 +0300 Subject: [PATCH 24/38] feat: add Google Cloud engagement-ad card + richer demo comments - Second feed card is now a hardcoded popular post wrapped in a scoped EngagementAdsProvider with a Google Cloud creative, so upvoting plays the branded upvote animation and the post modal highlights the sponsored tag. - Wire brandAnimation into FeedCardGlassActions so the glass action bar shows the advertiser upvote animation too. - Make the simulated discussion look real: photo avatars, granular upvote counts, and author reputation + verified-company badges. Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/Feed.tsx | 30 ++--- .../cards/common/FeedCardGlassActions.tsx | 22 +++- .../src/contexts/EngagementAdsContext.tsx | 2 +- .../GoogleCloudEngagementCard.tsx | 98 +++++++++++++++++ .../GoogleCloudEngagementProvider.tsx | 42 +++++++ .../features/googleCloudTakeover/config.ts | 5 +- .../googleCloudTakeover/engagementContent.ts | 69 ++++++++++++ .../googleCloudTakeover/fakeDiscussion.ts | 103 ++++++++++++++---- 8 files changed, 331 insertions(+), 40 deletions(-) create mode 100644 packages/shared/src/features/googleCloudTakeover/GoogleCloudEngagementCard.tsx create mode 100644 packages/shared/src/features/googleCloudTakeover/GoogleCloudEngagementProvider.tsx create mode 100644 packages/shared/src/features/googleCloudTakeover/engagementContent.ts diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index 5759131edca..85020e3af9f 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -71,6 +71,7 @@ import { useLayoutVariant } from '../hooks/layout/useLayoutVariant'; import { useReaderModalEligibility } from './post/reader/hooks/useReaderModalEligibility'; import { useQuestDashboard } from '../hooks/useQuestDashboard'; import { GoogleCloudBlogCard } from '../features/googleCloudTakeover/GoogleCloudBlogCard'; +import { GoogleCloudEngagementCard } from '../features/googleCloudTakeover/GoogleCloudEngagementCard'; import { GoogleCloudHeadAd } from '../features/googleCloudTakeover/GoogleCloudHeadAd'; import { GoogleCloudStrip } from '../features/googleCloudTakeover/GoogleCloudStrip'; import { @@ -331,25 +332,25 @@ export default function Feed({ const useList = isListMode && numCards > 1; const virtualizedNumCards = useList ? 1 : numCards; // Find the item index before which the strip should render so it starts on a - // whole grid row (no empty cells above it). Walk the real rendered cells — - // the prepended blog card, the ad inserted at index 1, and each item's - // colSpan (wide/hero cards, cadence ads) — and stop at the first column-0 - // boundary at/after the target row. Robust to the feed's dynamic insertions. + // whole grid row (no empty cells above it). The takeover forces every feed + // item to a single cell, so the cells rendered before item `i` are just the + // fixed injected cards (the prepended blog + engagement cards, plus the head + // ad inserted before item 0) plus `i`. Stop at the first column-0 boundary + // at/after the target row. const googleCloudStripBeforeIndex = useMemo(() => { if (!showGoogleCloudTakeover || virtualizedNumCards < 1) { return -1; } const targetCells = googleCloudStripRow * virtualizedNumCards; - let cells = googleCloudPrependedCards - 1; // blog card, prepended at the top + const injectedBeforeItems = googleCloudPrependedCards + 1; // + head ad for (let i = 0; i < items.length; i += 1) { - if (i === 1) { - cells += 1; // ad slot, inserted before item index 1 - } - if (cells >= targetCells && cells % virtualizedNumCards === 0) { + const cellsBefore = injectedBeforeItems + i; + if ( + cellsBefore >= targetCells && + cellsBefore % virtualizedNumCards === 0 + ) { return i; } - // Takeover forces single-column cards, so every item is one cell. - cells += 1; } return -1; }, [showGoogleCloudTakeover, virtualizedNumCards, items]); @@ -734,7 +735,10 @@ export default function Feed({ /> )} {showGoogleCloudTakeover && ( - + <> + + + )} {items.map((item, index) => { const placement = itemPlacements[index]; @@ -827,7 +831,7 @@ export default function Feed({ }} /> )} - {showGoogleCloudTakeover && index === 1 && ( + {showGoogleCloudTakeover && index === 0 && ( )} {showGoogleCloudTakeover && diff --git a/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx b/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx index 48011695b2c..32f438c08bc 100644 --- a/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx +++ b/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx @@ -1,5 +1,5 @@ import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useMemo } from 'react'; import classNames from 'classnames'; import type { ActionButtonsProps } from './ActionButtons'; import { UpvoteButtonIcon } from './UpvoteButtonIcon'; @@ -17,6 +17,7 @@ import { IconSize } from '../../Icon'; import { Tooltip } from '../../tooltip/Tooltip'; import { useFeedPreviewMode } from '../../../hooks/useFeedPreviewMode'; import { useCardActions } from '../../../hooks/cards/useCardActions'; +import { useBrandSponsorship } from '../../../hooks/useBrandSponsorship'; // Full-bleed cover: drop side padding/bottom margin and round only the bottom // corners so the image meets the card edges. Height/crop are untouched. @@ -62,6 +63,7 @@ export function FeedCardGlassActions({ coverScrim = false, }: ActionButtonsProps & { coverScrim?: boolean }): ReactElement | null { const isFeedPreview = useFeedPreviewMode(); + const { getUpvoteAnimation } = useBrandSponsorship(); const { isUpvoteActive, isDownvoteActive, @@ -77,6 +79,23 @@ export function FeedCardGlassActions({ onCopyLinkClick, }); + // Branded upvote animation when the post has a sponsored tag (engagement ad). + const brandAnimation = useMemo(() => { + const animationResult = getUpvoteAnimation(post.tags || []); + if ( + !animationResult.shouldAnimate || + !animationResult.colors || + !animationResult.config + ) { + return null; + } + return { + colors: animationResult.colors, + config: animationResult.config, + brandLogo: animationResult.brandLogo, + }; + }, [getUpvoteAnimation, post.tags]); + if (isFeedPreview) { return null; } @@ -113,6 +132,7 @@ export function FeedCardGlassActions({ } > diff --git a/packages/shared/src/contexts/EngagementAdsContext.tsx b/packages/shared/src/contexts/EngagementAdsContext.tsx index 1be5807c5e3..23917ae4c02 100644 --- a/packages/shared/src/contexts/EngagementAdsContext.tsx +++ b/packages/shared/src/contexts/EngagementAdsContext.tsx @@ -31,7 +31,7 @@ const defaultValue: EngagementAdsContextValue = { getCreativeForTool: () => null, }; -const EngagementAdsContext = +export const EngagementAdsContext = createContext(defaultValue); export const useEngagementAdsContext = (): EngagementAdsContextValue => diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudEngagementCard.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudEngagementCard.tsx new file mode 100644 index 00000000000..86c8463cb38 --- /dev/null +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudEngagementCard.tsx @@ -0,0 +1,98 @@ +import type { ReactElement } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { ArticleGrid } from '../../components/cards/article/ArticleGrid'; +import { ArticleList } from '../../components/cards/article/ArticleList'; +import ArticlePostModal from '../../components/modals/ArticlePostModal'; +import { PostPosition } from '../../hooks/usePostModalNavigation'; +import type { Post } from '../../graphql/posts'; +import { UserVote } from '../../graphql/posts'; +import { GoogleCloudEngagementProvider } from './GoogleCloudEngagementProvider'; +import { googleCloudEngagementPost } from './engagementContent'; +import { seedGoogleCloudDiscussion } from './fakeDiscussion'; + +type GoogleCloudEngagementCardProps = { + isList?: boolean; + className?: string; +}; + +const noop = () => undefined; + +const openPost = () => { + if (typeof window !== 'undefined') { + window.open( + googleCloudEngagementPost.permalink, + '_blank', + 'noopener,noreferrer', + ); + } +}; + +// The second feed card: a hardcoded popular post that Google Cloud "promotes +// engagement" on. Wrapped in a scoped EngagementAdsProvider so the real brand +// system fires here only — upvoting plays the Google Cloud icon animation, and +// opening the post highlights the sponsored tag. Otherwise it behaves like a +// normal organic card (opens the post modal, has a simulated discussion). +export const GoogleCloudEngagementCard = ({ + isList = false, + className, +}: GoogleCloudEngagementCardProps): ReactElement => { + const queryClient = useQueryClient(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [post, setPost] = useState(googleCloudEngagementPost); + const Card = isList ? ArticleList : ArticleGrid; + + useEffect(() => { + seedGoogleCloudDiscussion(queryClient, googleCloudEngagementPost.id); + }, [queryClient]); + + const toggleVote = (vote: UserVote) => + setPost((current) => { + const isActive = current.userState?.vote === vote; + const wasUpvote = current.userState?.vote === UserVote.Up; + const willUpvote = !isActive && vote === UserVote.Up; + const upvoteDelta = (willUpvote ? 1 : 0) - (wasUpvote ? 1 : 0); + return { + ...current, + numUpvotes: Math.max(0, (current.numUpvotes ?? 0) + upvoteDelta), + userState: { + ...current.userState, + vote: isActive ? UserVote.None : vote, + }, + }; + }); + + const toggleBookmark = () => + setPost((current) => ({ ...current, bookmarked: !current.bookmarked })); + + return ( + + setIsModalOpen(true)} + onPostAuxClick={openPost} + onUpvoteClick={() => toggleVote(UserVote.Up)} + onDownvoteClick={() => toggleVote(UserVote.Down)} + onCommentClick={() => setIsModalOpen(true)} + onBookmarkClick={toggleBookmark} + onShare={noop} + onCopyLinkClick={noop} + onReadArticleClick={openPost} + onMenuClick={noop} + openNewTab + domProps={{ className }} + /> + {isModalOpen && ( + setIsModalOpen(false)} + /> + )} + + ); +}; diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudEngagementProvider.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudEngagementProvider.tsx new file mode 100644 index 00000000000..65f5f88814a --- /dev/null +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudEngagementProvider.tsx @@ -0,0 +1,42 @@ +import type { ReactElement, ReactNode } from 'react'; +import React, { useMemo } from 'react'; +import { EngagementAdsContext } from '../../contexts/EngagementAdsContext'; +import { + findCreativeForTags, + findCreativeForTool, + parseCreatives, + resolveCreative, +} from '../../lib/engagementAds'; +import { useIsLightTheme } from '../../hooks/utils/useThemedAsset'; +import { googleCloudEngagementCreativeRaw } from './engagementContent'; + +// Scoped engagement-ads provider for the demo. Supplies just the Google Cloud +// creative to its subtree, so the branded upvote animation and sponsored tag +// fire ONLY on the card it wraps. Unlike the app-level provider, it doesn't +// drop creatives for Plus users — the sales demo must render on any account. +export const GoogleCloudEngagementProvider = ({ + children, +}: { + children: ReactNode; +}): ReactElement => { + const isLight = useIsLightTheme(); + + const value = useMemo(() => { + const creatives = parseCreatives([googleCloudEngagementCreativeRaw]).map( + (creative) => resolveCreative(creative, isLight), + ); + return { + creatives, + getCreativeForTags: (tags: string[]) => + findCreativeForTags(creatives, tags), + getCreativeForTool: (toolName?: string | null) => + findCreativeForTool(creatives, toolName), + }; + }, [isLight]); + + return ( + + {children} + + ); +}; diff --git a/packages/shared/src/features/googleCloudTakeover/config.ts b/packages/shared/src/features/googleCloudTakeover/config.ts index f04207d6641..65c1643b7c0 100644 --- a/packages/shared/src/features/googleCloudTakeover/config.ts +++ b/packages/shared/src/features/googleCloudTakeover/config.ts @@ -8,8 +8,9 @@ // off, and do not ship it enabled to a production audience. export const googleCloudTakeoverEnabled = true; -// Number of cards prepended at the top of the feed (the sponsored blog card + -// the head ad). Used to keep the strip aligned to a row boundary. +// Number of cards prepended at the top of the feed before the real items: the +// sponsored blog card + the engagement (second) card. The head ad is injected +// separately before item 0. Used to keep the strip aligned to a row boundary. export const googleCloudPrependedCards = 2; // The full-row strip starts at this grid row (0-based). Picking a whole row diff --git a/packages/shared/src/features/googleCloudTakeover/engagementContent.ts b/packages/shared/src/features/googleCloudTakeover/engagementContent.ts new file mode 100644 index 00000000000..0b2288d203d --- /dev/null +++ b/packages/shared/src/features/googleCloudTakeover/engagementContent.ts @@ -0,0 +1,69 @@ +// Content for the second feed card in the Google Cloud takeover: a hardcoded +// popular explore-style post that Google Cloud "promotes engagement" on via the +// real Engagement Ads system (branded upvote animation + sponsored tag). + +import type { Post } from '../../graphql/posts'; +import { PostType } from '../../graphql/posts'; +import { googleCloudLogoDataUri } from './GoogleCloudLogo'; + +const engagementPostUrl = + 'https://huggingface.co/blog/building-production-ai-agents'; +const engagementPostImage = + 'https://images.unsplash.com/photo-1620712943543-bcc4688e7485?auto=format&fit=crop&w=1080&q=80'; + +// The tag Google Cloud sponsors. Must appear in the post's tags so the +// engagement system can match and brand it. +export const googleCloudSponsoredTag = 'ai'; + +// A realistic, popular organic post (Share type renders identically to an +// article in ArticleGrid/ArticlePostModal but isn't reader-gated, so "Read +// post" links straight to the source instead of opening the in-app reader). +export const googleCloudEngagementPost: Post = { + id: 'gcp-engagement-post', + title: 'Building production-ready AI agents: lessons from a year in prod', + summary: + 'What actually breaks when you take an AI agent from a demo to real traffic — tool calling, evals, cost control, and the guardrails we wish we had on day one.', + permalink: engagementPostUrl, + commentsPermalink: engagementPostUrl, + createdAt: '2026-06-19T07:30:00.000Z', + readTime: 9, + image: engagementPostImage, + source: { + id: 'huggingface', + handle: 'huggingface', + name: 'Hugging Face', + permalink: 'https://huggingface.co/blog', + image: 'https://github.com/huggingface.png', + } as unknown as Post['source'], + tags: [googleCloudSponsoredTag, 'machine-learning', 'llm', 'python'], + numUpvotes: 1843, + numComments: 48, + numAwards: 0, + type: PostType.Share, +}; + +// Raw Engagement Ads creative (snake_case, the boot API shape) for Google +// Cloud. Parsed/resolved by the scoped provider. Matched to the post above by +// the `tags` overlap, which drives both the branded tag and the upvote icon +// swap. +export const googleCloudEngagementCreativeRaw = { + gen_id: 'gcp-engagement-demo', + promoted_name: 'Google Cloud', + promoted_body: + 'Build, deploy, and scale AI agents on Google Cloud with Vertex AI and Gemini. Get $300 in free credits to start.', + promoted_cta: 'Start building free', + promoted_url: 'https://cloud.google.com/free', + promoted_logo_img: { + dark: googleCloudLogoDataUri, + light: googleCloudLogoDataUri, + }, + promoted_icon_img: { + dark: googleCloudLogoDataUri, + light: googleCloudLogoDataUri, + }, + promoted_gradient_start: { dark: '#4285F4', light: '#4285F4' }, + promoted_gradient_end: { dark: '#34A853', light: '#34A853' }, + tools: [], + keywords: [], + tags: [googleCloudSponsoredTag], +}; diff --git a/packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts b/packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts index 10489bd2f19..7251fe7ccbe 100644 --- a/packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts +++ b/packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts @@ -7,24 +7,63 @@ import { generateCommentsQueryKey, getAllCommentsQuery } from '../../lib/query'; // cache (and pinned so a refetch can't clear it) since the post isn't a real // backend post. -const avatar = (bg: string, initials: string): string => - `data:image/svg+xml,${encodeURIComponent( - `${initials}`, - )}`; +// Real avatar photos (deterministic per index) so the discussion doesn't look +// like a placeholder. Verified company logos use GitHub org avatars, which +// redirect to the real image and load reliably. +const avatar = (img: number): string => `https://i.pravatar.cc/150?img=${img}`; +const orgLogo = (org: string): string => `https://github.com/${org}.png`; -const people = [ - { name: 'Priya Sharma', username: 'priyabuilds', color: '#4285F4' }, - { name: 'Marcus Lee', username: 'marcusdev', color: '#EA4335' }, - { name: 'Sofia Alvarez', username: 'sofiacodes', color: '#34A853' }, - { name: 'Tom Becker', username: 'tbecker', color: '#FBBC04' }, - { name: 'Aisha Khan', username: 'aishak', color: '#A142F4' }, - { name: 'Daniel Park', username: 'dpark', color: '#00ACC1' }, - { name: 'Lena Novak', username: 'lenan', color: '#F4511E' }, - { name: 'Owen Wright', username: 'owenw', color: '#3949AB' }, - { name: 'Rina Tanaka', username: 'rinat', color: '#00897B' }, - { name: 'Caleb Stone', username: 'cstone', color: '#8E24AA' }, - { name: 'Mira Patel', username: 'mirap', color: '#43A047' }, - { name: 'Jonas Vogel', username: 'jvogel', color: '#FB8C00' }, +type Person = { + name: string; + username: string; + img: number; + reputation: number; + company?: { name: string; org: string }; +}; + +const people: Person[] = [ + { + name: 'Priya Sharma', + username: 'priyabuilds', + img: 5, + reputation: 18430, + company: { name: 'Vercel', org: 'vercel' }, + }, + { name: 'Marcus Lee', username: 'marcusdev', img: 12, reputation: 9120 }, + { + name: 'Sofia Alvarez', + username: 'sofiacodes', + img: 47, + reputation: 31280, + company: { name: 'Stripe', org: 'stripe' }, + }, + { name: 'Tom Becker', username: 'tbecker', img: 33, reputation: 2740 }, + { + name: 'Aisha Khan', + username: 'aishak', + img: 23, + reputation: 12660, + company: { name: 'Microsoft', org: 'microsoft' }, + }, + { name: 'Daniel Park', username: 'dpark', img: 8, reputation: 5380 }, + { + name: 'Lena Novak', + username: 'lenan', + img: 16, + reputation: 44910, + company: { name: 'GitHub', org: 'github' }, + }, + { name: 'Owen Wright', username: 'owenw', img: 51, reputation: 870 }, + { + name: 'Rina Tanaka', + username: 'rinat', + img: 44, + reputation: 21540, + company: { name: 'Datadog', org: 'DataDog' }, + }, + { name: 'Caleb Stone', username: 'cstone', img: 60, reputation: 3960 }, + { name: 'Mira Patel', username: 'mirap', img: 26, reputation: 15070 }, + { name: 'Jonas Vogel', username: 'jvogel', img: 14, reputation: 6620 }, ]; const remarks = [ @@ -54,20 +93,38 @@ const days = [ '2026-06-18T20:50:00.000Z', ]; +// Hand-tuned, granular upvote counts so they don't all cluster around one +// value — a real discussion has a long tail of low-engagement replies and a +// few standouts. +const upvotes = [ + 214, 7, 86, 1, 132, 19, 3, 58, 0, 41, 9, 167, 24, 2, 73, 12, 38, 5, 95, 0, 16, + 49, 4, 121, 8, 31, 1, 62, 14, 6, 88, 0, 27, 53, 3, 109, 11, 2, 44, 19, 7, 76, + 0, 35, 5, 23, 1, 64, +]; + const buildComments = (count: number): Comment[] => Array.from({ length: count }).map((_, i) => { const person = people[i % people.length]; - const initials = person.name - .split(' ') - .map((part) => part[0]) - .join(''); const text = remarks[i % remarks.length]; const author: Author = { id: `gcp-author-${i}`, name: person.name, username: person.username, permalink: `https://app.daily.dev/${person.username}`, - image: avatar(person.color, initials), + image: avatar(person.img), + reputation: person.reputation, + createdAt: '2021-03-01T00:00:00.000Z', + companies: person.company + ? [ + { + id: person.company.org, + name: person.company.name, + image: orgLogo(person.company.org), + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + }, + ] + : undefined, } as Author; return { @@ -78,7 +135,7 @@ const buildComments = (count: number): Comment[] => createdAt: days[i % days.length], lastUpdatedAt: days[i % days.length], permalink: 'https://cloud.google.com/blog', - numUpvotes: Math.max(0, 47 - i), + numUpvotes: upvotes[i % upvotes.length], numAwards: 0, author, children: { edges: [], pageInfo: { hasNextPage: false } }, From 3c529d125fb8518854de6663d9c4481bc0e62439 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 15:46:21 +0300 Subject: [PATCH 25/38] fix: align Google Cloud ad 'Promoted' label flush-left with title Co-Authored-By: Claude Opus 4.8 --- .../src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx index 0b7762869aa..b43f6d70c8d 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx @@ -65,8 +65,10 @@ export const GoogleCloudHeadAd = ({ {googleCloudAd.description} - {/* Match the exact look of a post card's date / read-time line. */} -
+ {/* Match the exact look of a post card's date / read-time line. + No horizontal margin — CardTextContainer already applies mx-4, so + this lines up flush-left with the title above it. */} +
Promoted
From d43e688a8383883725ecc13cf5b185bb9029058d Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 15:59:58 +0300 Subject: [PATCH 26/38] fix: drop branded upvote animation from feed-card glass bar Keeps the compact feed-card actions static and smooth for the demo (no spin/icon-swap flicker on upvote). The Google Cloud engagement ad still shows the branded upvote animation and sponsored tag inside the post modal, where the action bar has room. Co-Authored-By: Claude Opus 4.8 --- .../cards/common/FeedCardGlassActions.tsx | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx b/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx index 32f438c08bc..48011695b2c 100644 --- a/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx +++ b/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx @@ -1,5 +1,5 @@ import type { ReactElement } from 'react'; -import React, { useMemo } from 'react'; +import React from 'react'; import classNames from 'classnames'; import type { ActionButtonsProps } from './ActionButtons'; import { UpvoteButtonIcon } from './UpvoteButtonIcon'; @@ -17,7 +17,6 @@ import { IconSize } from '../../Icon'; import { Tooltip } from '../../tooltip/Tooltip'; import { useFeedPreviewMode } from '../../../hooks/useFeedPreviewMode'; import { useCardActions } from '../../../hooks/cards/useCardActions'; -import { useBrandSponsorship } from '../../../hooks/useBrandSponsorship'; // Full-bleed cover: drop side padding/bottom margin and round only the bottom // corners so the image meets the card edges. Height/crop are untouched. @@ -63,7 +62,6 @@ export function FeedCardGlassActions({ coverScrim = false, }: ActionButtonsProps & { coverScrim?: boolean }): ReactElement | null { const isFeedPreview = useFeedPreviewMode(); - const { getUpvoteAnimation } = useBrandSponsorship(); const { isUpvoteActive, isDownvoteActive, @@ -79,23 +77,6 @@ export function FeedCardGlassActions({ onCopyLinkClick, }); - // Branded upvote animation when the post has a sponsored tag (engagement ad). - const brandAnimation = useMemo(() => { - const animationResult = getUpvoteAnimation(post.tags || []); - if ( - !animationResult.shouldAnimate || - !animationResult.colors || - !animationResult.config - ) { - return null; - } - return { - colors: animationResult.colors, - config: animationResult.config, - brandLogo: animationResult.brandLogo, - }; - }, [getUpvoteAnimation, post.tags]); - if (isFeedPreview) { return null; } @@ -132,7 +113,6 @@ export function FeedCardGlassActions({ } > From 6102c2ea4f5c0caa9cb1564a69c7d1e994d54e2c Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 16:14:44 +0300 Subject: [PATCH 27/38] =?UTF-8?q?feat:=20demo=20polish=20=E2=80=94=20in-ap?= =?UTF-8?q?p=20reader=20on=20blog=20card,=20ad=20tags,=20richer=20discussi?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - First (Google Cloud blog) card's Read post opens the URL in the daily.dev in-app reader (post is Article type; card opens ReaderPreview directly so it works on any account). Body-click still opens the discussion. - Advertiser (3rd) card now shows tag chips, like organic/ad cards. - Rewrote the simulated discussion to feel like a real community: varied voices/lengths/vibes, code snippets, an embedded chart, threaded replies, skeptics and war stories. Removed em dashes. - Timestamps are relative to load time so the long-lived demo never looks stale (it is not meant to be merged). Co-Authored-By: Claude Opus 4.8 --- .../GoogleCloudBlogCard.tsx | 11 +- .../googleCloudTakeover/GoogleCloudHeadAd.tsx | 9 + .../features/googleCloudTakeover/content.ts | 20 +- .../googleCloudTakeover/engagementContent.ts | 8 +- .../googleCloudTakeover/fakeDiscussion.ts | 351 ++++++++++++++---- .../googleCloudTakeover/relativeTime.ts | 10 + 6 files changed, 327 insertions(+), 82 deletions(-) create mode 100644 packages/shared/src/features/googleCloudTakeover/relativeTime.ts diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx index 0a0381d4abf..8393c0df33d 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx @@ -7,6 +7,8 @@ import ArticlePostModal from '../../components/modals/ArticlePostModal'; import { PostPosition } from '../../hooks/usePostModalNavigation'; import type { Post } from '../../graphql/posts'; import { UserVote } from '../../graphql/posts'; +import { useLazyModal } from '../../hooks/useLazyModal'; +import { LazyModal } from '../../components/modals/common/types'; import { googleCloudBlogPost } from './content'; import { seedGoogleCloudDiscussion } from './fakeDiscussion'; @@ -33,10 +35,17 @@ export const GoogleCloudBlogCard = ({ className, }: GoogleCloudBlogCardProps): ReactElement => { const queryClient = useQueryClient(); + const { openModal } = useLazyModal(); const [isModalOpen, setIsModalOpen] = useState(false); const [post, setPost] = useState(googleCloudBlogPost); const Card = isList ? ArticleList : ArticleGrid; + // Open the real URL inside the daily.dev in-app reader/browser (only this + // first card does this). Opening the ReaderPreview modal directly bypasses + // the per-user reader gate so it works on any demo account. + const openReader = () => + openModal({ type: LazyModal.ReaderPreview, props: { post } }); + // Seed the simulated discussion so the post modal shows engagement. useEffect(() => { seedGoogleCloudDiscussion(queryClient, googleCloudBlogPost.id); @@ -73,7 +82,7 @@ export const GoogleCloudBlogCard = ({ onBookmarkClick={toggleBookmark} onShare={noop} onCopyLinkClick={noop} - onReadArticleClick={openBlog} + onReadArticleClick={openReader} onMenuClick={noop} openNewTab domProps={{ className }} diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx index b43f6d70c8d..c33929b9992 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx @@ -13,6 +13,7 @@ import { AdImage } from '../../components/cards/ad/common/AdImage'; import { AdPixel } from '../../components/cards/ad/common/AdPixel'; import { AdFavicon } from '../../components/cards/ad/common/AdFavicon'; import { AdList } from '../../components/cards/ad/AdList'; +import PostTags from '../../components/cards/common/PostTags'; import { Button, ButtonSize, @@ -65,6 +66,14 @@ export const GoogleCloudHeadAd = ({ {googleCloudAd.description} + {/* Advertiser cards carry tag chips like organic cards (mirrors the + real AdGrid's PostTags row). */} + {!!googleCloudAd.matchingTags?.length && ( + + )} {/* Match the exact look of a post card's date / read-time line. No horizontal margin — CardTextContainer already applies mx-4, so this lines up flush-left with the title above it. */} diff --git a/packages/shared/src/features/googleCloudTakeover/content.ts b/packages/shared/src/features/googleCloudTakeover/content.ts index bed28477d8a..4231a8ce4aa 100644 --- a/packages/shared/src/features/googleCloudTakeover/content.ts +++ b/packages/shared/src/features/googleCloudTakeover/content.ts @@ -6,6 +6,8 @@ import type { Ad, Post } from '../../graphql/posts'; import { PostType } from '../../graphql/posts'; import { googleCloudLogoDataUri } from './GoogleCloudLogo'; +import { hoursAgo } from './relativeTime'; +import { googleCloudDiscussionCount } from './fakeDiscussion'; const googleCloudBlogUrl = 'https://cloud.google.com/blog/topics/inside-google-cloud/whats-new-google-cloud'; @@ -26,9 +28,11 @@ export const googleCloudBlogPost: Post = { 'A roundup of the latest launches, updates, and resources from Google Cloud: agentic AI, Gemini Enterprise, Spot VM optimization, and more.', permalink: googleCloudBlogUrl, commentsPermalink: googleCloudBlogUrl, - createdAt: '2026-06-20T09:00:00.000Z', + createdAt: hoursAgo(5), readTime: 6, image: googleCloudBlogImage, + // `domain` drives the reader header favicon when opened in the in-app reader. + domain: 'cloud.google.com', source: { id: 'google-cloud-blog', handle: 'google-cloud-blog', @@ -38,13 +42,12 @@ export const googleCloudBlogPost: Post = { } as unknown as Post['source'], tags: ['cloud', 'ai', 'devops'], numUpvotes: 312, - numComments: 48, + numComments: googleCloudDiscussionCount, numAwards: 0, - // Share (with no sharedPost) renders identically to an article in our - // directly-mounted ArticleGrid/ArticlePostModal, but it is NOT a - // reader-gated type — so "Read post" links straight to the Google Cloud - // blog (post.permalink) instead of opening the in-app reader. - type: PostType.Share, + // Article so "Read post" can open the real URL inside the daily.dev in-app + // reader/browser (READER_GATE_ELIGIBLE_TYPES). The card mounts the reader + // explicitly on read; see GoogleCloudBlogCard. + type: PostType.Article, }; // Rendered through the real AdGrid/AdList so it matches the live ad slot. @@ -57,6 +60,9 @@ export const googleCloudAd: Ad = { image: googleCloudAdImage, companyLogo: googleCloudLogoDataUri, callToAction: 'Start building free', + // Advertiser cards carry tags like organic cards; these drive the chips on + // the ad card (and the AdList/list path via `matchingTags`). + matchingTags: ['cloud', 'ai', 'devops', 'kubernetes', 'serverless', 'gemini'], }; // Shared messaging for the announcement bar + in-feed strip. The bar uses a diff --git a/packages/shared/src/features/googleCloudTakeover/engagementContent.ts b/packages/shared/src/features/googleCloudTakeover/engagementContent.ts index 0b2288d203d..43801751e15 100644 --- a/packages/shared/src/features/googleCloudTakeover/engagementContent.ts +++ b/packages/shared/src/features/googleCloudTakeover/engagementContent.ts @@ -5,6 +5,8 @@ import type { Post } from '../../graphql/posts'; import { PostType } from '../../graphql/posts'; import { googleCloudLogoDataUri } from './GoogleCloudLogo'; +import { hoursAgo } from './relativeTime'; +import { googleCloudDiscussionCount } from './fakeDiscussion'; const engagementPostUrl = 'https://huggingface.co/blog/building-production-ai-agents'; @@ -22,10 +24,10 @@ export const googleCloudEngagementPost: Post = { id: 'gcp-engagement-post', title: 'Building production-ready AI agents: lessons from a year in prod', summary: - 'What actually breaks when you take an AI agent from a demo to real traffic — tool calling, evals, cost control, and the guardrails we wish we had on day one.', + 'What actually breaks when you take an AI agent from a demo to real traffic: tool calling, evals, cost control, and the guardrails we wish we had on day one.', permalink: engagementPostUrl, commentsPermalink: engagementPostUrl, - createdAt: '2026-06-19T07:30:00.000Z', + createdAt: hoursAgo(28), readTime: 9, image: engagementPostImage, source: { @@ -37,7 +39,7 @@ export const googleCloudEngagementPost: Post = { } as unknown as Post['source'], tags: [googleCloudSponsoredTag, 'machine-learning', 'llm', 'python'], numUpvotes: 1843, - numComments: 48, + numComments: googleCloudDiscussionCount, numAwards: 0, type: PostType.Share, }; diff --git a/packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts b/packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts index 7251fe7ccbe..ad19553f4dd 100644 --- a/packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts +++ b/packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts @@ -1,11 +1,13 @@ import type { QueryClient } from '@tanstack/react-query'; import type { Author, Comment, PostCommentsData } from '../../graphql/comments'; import { generateCommentsQueryKey, getAllCommentsQuery } from '../../lib/query'; +import { hoursAgo } from './relativeTime'; -// A simulated discussion for the sponsored Google Cloud blog post, so the -// sales demo can show engagement. Seeded straight into the comments query -// cache (and pinned so a refetch can't clear it) since the post isn't a real -// backend post. +// A simulated discussion for the sponsored Google Cloud blog post, written to +// feel like a real community thread: different voices and lengths, code +// snippets, an embedded chart, questions with replies, jokes, skeptics, war +// stories. Seeded straight into the comments query cache (and pinned so a +// refetch can't clear it) since the post isn't a real backend post. // Real avatar photos (deterministic per index) so the discussion doesn't look // like a placeholder. Verified company logos use GitHub org avatars, which @@ -64,87 +66,295 @@ const people: Person[] = [ { name: 'Caleb Stone', username: 'cstone', img: 60, reputation: 3960 }, { name: 'Mira Patel', username: 'mirap', img: 26, reputation: 15070 }, { name: 'Jonas Vogel', username: 'jvogel', img: 14, reputation: 6620 }, + { + name: 'Elena Rossi', + username: 'elenar', + img: 20, + reputation: 27800, + company: { name: 'Shopify', org: 'shopify' }, + }, + { name: 'Hassan Ali', username: 'hassana', img: 56, reputation: 4310 }, + { name: 'Yuki Sato', username: 'yukis', img: 65, reputation: 11200 }, + { name: "Sam O'Neill", username: 'samon', img: 40, reputation: 1580 }, ]; -const remarks = [ - 'The Spot VM optimization is going to cut our batch costs significantly. Already testing it this week.', - 'Finally proper in-country processing for Gemini. This unblocks a sovereign workload we shelved last year.', - 'The serverless Spark improvements look great. Cold starts were the only thing holding us back.', - 'Gemini Enterprise integration with our data warehouse is exactly what the team has been asking for.', - 'Good roundup. The agentic tooling updates alone are worth a read for anyone shipping AI features.', - 'Migrated a service to managed clusters last month — the autoscaling changes here would have saved us a lot of tuning.', - 'Curious how the new API management features compare to what we built in-house. Might be time to switch.', - 'The multi-tenant guidance is solid. We learned most of this the hard way.', - 'Bookmarking this. The links to the deep-dive posts are the real value here.', - 'Been waiting for the VS Code workbench notebooks to go GA. Local + managed compute is the dream workflow.', - 'Anyone tried the new data residency controls in production yet? Wondering about latency impact.', - 'The cost optimizer recommendations have been surprisingly accurate for our fleet. Nice to see it expanding.', - 'Great to see first-class support for this. The docs are clearer than they used to be too.', - 'We run a similar stack and the reliability numbers they quote line up with what we see.', - 'This is the kind of update that quietly makes day-to-day infra work less painful.', - 'Shared this with the platform team. A few of these land directly on our roadmap.', -]; +const chartImage = + 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=900&q=80'; -const days = [ - '2026-06-20T11:40:00.000Z', - '2026-06-20T09:15:00.000Z', - '2026-06-19T18:05:00.000Z', - '2026-06-19T08:30:00.000Z', - '2026-06-18T20:50:00.000Z', -]; +type Reply = { p: number; html: string; up: number; h: number }; +type Spec = { + p: number; + html: string; + up: number; + h: number; + replies?: Reply[]; +}; -// Hand-tuned, granular upvote counts so they don't all cluster around one -// value — a real discussion has a long tail of low-engagement replies and a -// few standouts. -const upvotes = [ - 214, 7, 86, 1, 132, 19, 3, 58, 0, 41, 9, 167, 24, 2, 73, 12, 38, 5, 95, 0, 16, - 49, 4, 121, 8, 31, 1, 62, 14, 6, 88, 0, 27, 53, 3, 109, 11, 2, 44, 19, 7, 76, - 0, 35, 5, 23, 1, 64, +// Curated discussion. `p` indexes `people`, `up` is upvotes, `h` is hours ago. +const specs: Spec[] = [ + { + p: 0, + up: 214, + h: 3, + html: `

The Spot VM optimizer is the headline for us. We run a nightly feature-engineering job on a 40-node pool and just flipped it to spot with a fallback to on-demand. First week came in at roughly 71% cheaper with zero failed runs thanks to the new graceful drain window.

If you have anything batch-shaped and idempotent, this is basically free money.

`, + replies: [ + { + p: 7, + up: 12, + h: 2, + html: `

How are you handling the 30s preemption notice? Checkpointing mid-job or just letting the orchestrator retry the shard?

`, + }, + { + p: 0, + up: 28, + h: 1, + html: `

Retry at the shard level. Each shard writes to a temp prefix and we only promote on success, so a preemption just re-runs that shard. Way simpler than trying to checkpoint model state.

`, + }, + ], + }, + { + p: 2, + up: 167, + h: 5, + html: `

People sleep on how much of this is just sane defaults now. Deploying a containerized agent used to be a half-day of IAM yak-shaving. Today it's basically:

gcloud run deploy support-agent \\
+  --image=us-docker.pkg.dev/$PROJECT/agents/support:latest \\
+  --region=us-central1 \\
+  --cpu=2 --memory=2Gi \\
+  --concurrency=8 --min-instances=1

Cold starts on min-instances=1 are under 400ms for us. That's the whole story.

`, + }, + { + p: 4, + up: 9, + h: 8, + html: `

Genuine question from someone newer to GCP: is Cloud Run the right call for a stateful websocket service, or should I be looking at GKE for that? The docs hint at both and I can't tell what the "blessed" path is.

`, + replies: [ + { + p: 8, + up: 41, + h: 7, + html: `

Cloud Run supports websockets fine now (60 min request timeout), but if you need sticky sessions or your own ingress rules, GKE Autopilot is less of a fight. Rule of thumb we use: stateless and bursty goes to Run, anything that wants to own its networking goes to GKE.

`, + }, + { + p: 4, + up: 3, + h: 6, + html: `

That's the clearest answer I've gotten, thank you. Going with Run for now.

`, + }, + ], + }, + { + p: 6, + up: 132, + h: 11, + html: `

Migrated a 12-service stack off self-managed k8s to Autopilot last quarter. The honest scorecard:

  • Node management toil: basically gone.
  • Bill: down about 22% after we right-sized requests (the optimizer recs were accurate).
  • Surprises: a couple of DaemonSets needed rework because you don't own the nodes anymore.

Net very positive, but budget a sprint for the DaemonSet stuff if you're coming from standard GKE.

`, + }, + { + p: 8, + up: 1, + h: 14, + html: `

+1, the docs are genuinely good now. Felt like that needed saying.

`, + }, + { + p: 3, + up: 88, + h: 9, + html: `

I'll be the skeptic. Every cloud keynote promises "agentic AI" and most of it is a thin wrapper over function calling. What's actually different here versus building the same loop yourself with the SDK?

`, + replies: [ + { + p: 10, + up: 54, + h: 8, + html: `

Fair, but the managed part is the eval + tracing story, not the loop. Getting per-step traces, replayable sessions, and a grounding layer wired into your own data without standing up infra is the actual time save. The loop was never the hard part.

`, + }, + ], + }, + { + p: 12, + up: 76, + h: 16, + html: `

The in-country processing for Gemini quietly unblocks a sovereign workload we shelved last year. For regulated EU data this is the difference between "no" and "yes" from our compliance team. Underrated line item in this post.

`, + }, + { + p: 9, + up: 121, + h: 6, + html: `

We A/B'd the new autoscaling profile on a latency-sensitive service. p95 before vs after the switch:

p95 latency dashboard, before and after the autoscaling change

Roughly a 38% drop at the tail without adding baseline cost. The scale-up reaction time is the real improvement.

`, + }, + { + p: 5, + up: 23, + h: 19, + html: `

Anyone running the serverless Spark jobs in production yet? Cold starts were the only thing stopping us from moving our ETL off a always-on Dataproc cluster, and the post claims that's fixed.

`, + replies: [ + { + p: 11, + up: 19, + h: 18, + html: `

Yes, ~3 weeks in. Cold start went from minutes to about 20s for our jobs. Not instant, but for hourly ETL it's a non-issue and we killed the standing cluster entirely.

`, + }, + ], + }, + { + p: 1, + up: 7, + h: 22, + html: `

Slightly off topic but the VS Code workbench notebooks going GA is the update I'm most selfishly happy about. Local editor, managed compute, no more babysitting a Jupyter VM.

`, + }, + { + p: 13, + up: 44, + h: 27, + html: `

Heads up for the cost-optimizer crowd: validate the recommendations against your own traffic shape before you apply them in bulk. We took the CPU downsizing suggestion on a spiky endpoint and got throttled during a campaign. Quick sanity check we run now:

# rough headroom check
+peak_rps * avg_cpu_ms / 1000 > provisioned_vcpu * 0.6

If that's false you probably have room, if it's true leave it alone.

`, + }, + { + p: 14, + up: 2, + h: 31, + html: `

Bookmarking purely for the deep-dive links at the bottom. That's where the actual engineering content lives every time.

`, + }, + { + p: 7, + up: 58, + h: 24, + html: `

The contrarian take nobody wants: this is great until you've got 200 services wired into managed everything and the lock-in is total. I love the DX, I just keep an abstraction layer over the queue and storage so a future migration isn't a company-ending project. Cheap insurance.

`, + replies: [ + { + p: 2, + up: 31, + h: 23, + html: `

This is the right amount of paranoia. We do the same for pub/sub and object storage and ignore it for everything else. Abstracting all of it is its own tax.

`, + }, + ], + }, + { + p: 11, + up: 95, + h: 13, + html: `

Reliability numbers in the post line up with what we see in our own dashboards, which is rare for a vendor blog. We've been at four nines on the managed gateway for two quarters with no manual intervention. Credit where it's due.

`, + }, + { + p: 10, + up: 16, + h: 36, + html: `

Mild gripe: the API management updates are nice but the pricing page still needs a PhD to parse. Would love a "here's what this costs at 10M requests/month" calculator that isn't three tabs deep.

`, + }, + { + p: 15, + up: 0, + h: 40, + html: `

Saving this to read properly after standup. The agentic tooling section looks like exactly what our team has been hacking together by hand.

`, + }, + { + p: 4, + up: 49, + h: 30, + html: `

For anyone wiring Gemini into a data warehouse: the native BigQuery integration means you can keep governance in one place instead of shuttling data to a separate vector store. That alone removed a whole service from our diagram.

`, + }, + { + p: 6, + up: 38, + h: 44, + html: `

Small thing that makes daily infra work less painful: the new default for graceful shutdown actually respects SIGTERM grace periods now. We used to lose in-flight requests on every deploy. Quiet quality-of-life win.

`, + }, + { + p: 12, + up: 11, + h: 52, + html: `

Shared this with our platform team channel. A few of these land directly on our Q3 roadmap, especially the data residency controls.

`, + }, + { + p: 9, + up: 64, + h: 20, + html: `

If you're comparing to the other big two: the thing that keeps us here is that the AI tooling and the data tooling are the same product surface. Less glue code than stitching a model provider to a separate warehouse. Your mileage will vary by stack.

`, + }, + { + p: 5, + up: 5, + h: 58, + html: `

Tried the new data residency controls in staging, no measurable latency hit for us in europe-west. Curious if anyone in apac is seeing different.

`, + }, + { + p: 3, + up: 73, + h: 17, + html: `

Okay, walking back some of my skepticism after actually trying the eval harness this weekend. Being able to replay a failed agent session step by step with the tool calls inline is genuinely good. That's the feature that earns the "agentic" label, not the marketing.

`, + }, + { + p: 1, + up: 27, + h: 48, + html: `

Underrated: the Spot + managed instance group combo means our CI runners are basically free now. We burst to 60 runners on spot and the queue time problem just disappeared.

`, + }, ]; -const buildComments = (count: number): Comment[] => - Array.from({ length: count }).map((_, i) => { - const person = people[i % people.length]; - const text = remarks[i % remarks.length]; - const author: Author = { - id: `gcp-author-${i}`, - name: person.name, - username: person.username, - permalink: `https://app.daily.dev/${person.username}`, - image: avatar(person.img), - reputation: person.reputation, - createdAt: '2021-03-01T00:00:00.000Z', - companies: person.company - ? [ - { - id: person.company.org, - name: person.company.name, - image: orgLogo(person.company.org), - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - }, - ] - : undefined, - } as Author; +const buildAuthor = (personIndex: number, key: string): Author => { + const person = people[personIndex]; + return { + id: `gcp-author-${key}`, + name: person.name, + username: person.username, + permalink: `https://app.daily.dev/${person.username}`, + image: avatar(person.img), + reputation: person.reputation, + createdAt: '2021-03-01T00:00:00.000Z', + companies: person.company + ? [ + { + id: person.company.org, + name: person.company.name, + image: orgLogo(person.company.org), + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + }, + ] + : undefined, + } as Author; +}; + +const buildComments = (): Comment[] => + specs.map((spec, i): Comment => { + const children = (spec.replies ?? []).map((reply, j) => ({ + node: { + id: `gcp-comment-${i}-r${j}`, + content: '', + contentHtml: reply.html, + contentEmbeds: [], + createdAt: hoursAgo(reply.h), + lastUpdatedAt: hoursAgo(reply.h), + permalink: 'https://cloud.google.com/blog', + numUpvotes: reply.up, + numAwards: 0, + author: buildAuthor(reply.p, `${i}-r${j}`), + children: { edges: [], pageInfo: { hasNextPage: false } }, + } as Comment, + })); return { id: `gcp-comment-${i}`, - content: text, - contentHtml: `

${text}

`, + content: '', + contentHtml: spec.html, contentEmbeds: [], - createdAt: days[i % days.length], - lastUpdatedAt: days[i % days.length], + createdAt: hoursAgo(spec.h), + lastUpdatedAt: hoursAgo(spec.h), permalink: 'https://cloud.google.com/blog', - numUpvotes: upvotes[i % upvotes.length], + numUpvotes: spec.up, numAwards: 0, - author, - children: { edges: [], pageInfo: { hasNextPage: false } }, + author: buildAuthor(spec.p, `${i}`), + children: { edges: children, pageInfo: { hasNextPage: false } }, } as Comment; }); -export const buildGoogleCloudDiscussion = (count = 48): PostCommentsData => ({ +// Total comments (top-level + replies) so the post header count matches. +export const googleCloudDiscussionCount = specs.reduce( + (sum, spec) => sum + 1 + (spec.replies?.length ?? 0), + 0, +); + +export const buildGoogleCloudDiscussion = (): PostCommentsData => ({ postComments: { - edges: buildComments(count).map((node) => ({ node })), + edges: buildComments().map((node) => ({ node })), pageInfo: { hasNextPage: false, endCursor: null }, }, }); @@ -154,9 +364,8 @@ export const buildGoogleCloudDiscussion = (count = 48): PostCommentsData => ({ export const seedGoogleCloudDiscussion = ( queryClient: QueryClient, postId: string, - count = 48, ): void => { - const data = buildGoogleCloudDiscussion(count); + const data = buildGoogleCloudDiscussion(); const keys = [ generateCommentsQueryKey({ postId }), ...getAllCommentsQuery(postId), diff --git a/packages/shared/src/features/googleCloudTakeover/relativeTime.ts b/packages/shared/src/features/googleCloudTakeover/relativeTime.ts new file mode 100644 index 00000000000..c24aa39d7fc --- /dev/null +++ b/packages/shared/src/features/googleCloudTakeover/relativeTime.ts @@ -0,0 +1,10 @@ +// Timestamps for the demo are computed relative to load time so the takeover +// keeps looking fresh indefinitely (it's a long-lived sales demo that never +// merges, so fixed dates would slowly age into "5 months ago"). + +const HOUR_MS = 60 * 60 * 1000; + +export const hoursAgo = (hours: number): string => + new Date(Date.now() - hours * HOUR_MS).toISOString(); + +export const daysAgo = (days: number): string => hoursAgo(days * 24); From e6d3fea77eec691313fb8ecfb1b2fbb006ee4009 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 16:16:35 +0300 Subject: [PATCH 28/38] chore: update Google Cloud ad card title Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/features/googleCloudTakeover/content.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/features/googleCloudTakeover/content.ts b/packages/shared/src/features/googleCloudTakeover/content.ts index 4231a8ce4aa..abc5d997314 100644 --- a/packages/shared/src/features/googleCloudTakeover/content.ts +++ b/packages/shared/src/features/googleCloudTakeover/content.ts @@ -54,7 +54,8 @@ export const googleCloudBlogPost: Post = { // `companyLogo` drives the favicon; `image` drives the cover. export const googleCloudAd: Ad = { company: 'Google Cloud', - description: 'Build what’s next on Google Cloud', + description: + 'Code more, config less. 👩‍💻 Deploy in seconds. Offload the infrastructure to Google Cloud.', link: 'https://cloud.google.com/free', source: 'Google Cloud', image: googleCloudAdImage, From b9abf3b14146b0863763071dda26f1b15e617a2a Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 16:22:41 +0300 Subject: [PATCH 29/38] feat: restore branded upvote animation on cards; new Google Cloud ad cover - Bring back the branded upvote animation in the glass action bar so the engagement card's upvote swaps the icon to the Google Cloud logo again (reverts the earlier removal). - Replace the ad (third) card cover with a self-contained SVG recreation of the 'Wasting time managing infra?' Google Cloud creative (no external host). Co-Authored-By: Claude Opus 4.8 --- .../cards/common/FeedCardGlassActions.tsx | 23 ++++++++++++- .../features/googleCloudTakeover/adCover.ts | 33 +++++++++++++++++++ .../features/googleCloudTakeover/content.ts | 7 ++-- 3 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 packages/shared/src/features/googleCloudTakeover/adCover.ts diff --git a/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx b/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx index 48011695b2c..f2808b9a2eb 100644 --- a/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx +++ b/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx @@ -1,5 +1,5 @@ import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useMemo } from 'react'; import classNames from 'classnames'; import type { ActionButtonsProps } from './ActionButtons'; import { UpvoteButtonIcon } from './UpvoteButtonIcon'; @@ -17,6 +17,7 @@ import { IconSize } from '../../Icon'; import { Tooltip } from '../../tooltip/Tooltip'; import { useFeedPreviewMode } from '../../../hooks/useFeedPreviewMode'; import { useCardActions } from '../../../hooks/cards/useCardActions'; +import { useBrandSponsorship } from '../../../hooks/useBrandSponsorship'; // Full-bleed cover: drop side padding/bottom margin and round only the bottom // corners so the image meets the card edges. Height/crop are untouched. @@ -62,6 +63,7 @@ export function FeedCardGlassActions({ coverScrim = false, }: ActionButtonsProps & { coverScrim?: boolean }): ReactElement | null { const isFeedPreview = useFeedPreviewMode(); + const { getUpvoteAnimation } = useBrandSponsorship(); const { isUpvoteActive, isDownvoteActive, @@ -77,6 +79,24 @@ export function FeedCardGlassActions({ onCopyLinkClick, }); + // Branded upvote animation (icon swaps to the advertiser logo) when the post + // has a sponsored tag (engagement ad). + const brandAnimation = useMemo(() => { + const animationResult = getUpvoteAnimation(post.tags || []); + if ( + !animationResult.shouldAnimate || + !animationResult.colors || + !animationResult.config + ) { + return null; + } + return { + colors: animationResult.colors, + config: animationResult.config, + brandLogo: animationResult.brandLogo, + }; + }, [getUpvoteAnimation, post.tags]); + if (isFeedPreview) { return null; } @@ -113,6 +133,7 @@ export function FeedCardGlassActions({ } > diff --git a/packages/shared/src/features/googleCloudTakeover/adCover.ts b/packages/shared/src/features/googleCloudTakeover/adCover.ts new file mode 100644 index 00000000000..44bda28c41e --- /dev/null +++ b/packages/shared/src/features/googleCloudTakeover/adCover.ts @@ -0,0 +1,33 @@ +// Cover art for the Google Cloud ad (third) card, rebuilt as a self-contained +// SVG data URI so the long-lived demo has no external image dependency. +// Recreates the supplied creative: light surface, multicolor "Google Cloud" +// wordmark, "Wasting time managing infra?" headline, and the gradient circles. + +const svg = ` + + + + + + + + + + + + + + + + Google Cloud + + + Wasting time + managing + infra? + +`; + +export const googleCloudAdCoverDataUri = `data:image/svg+xml,${encodeURIComponent( + svg, +)}`; diff --git a/packages/shared/src/features/googleCloudTakeover/content.ts b/packages/shared/src/features/googleCloudTakeover/content.ts index abc5d997314..f139024f9cd 100644 --- a/packages/shared/src/features/googleCloudTakeover/content.ts +++ b/packages/shared/src/features/googleCloudTakeover/content.ts @@ -6,6 +6,7 @@ import type { Ad, Post } from '../../graphql/posts'; import { PostType } from '../../graphql/posts'; import { googleCloudLogoDataUri } from './GoogleCloudLogo'; +import { googleCloudAdCoverDataUri } from './adCover'; import { hoursAgo } from './relativeTime'; import { googleCloudDiscussionCount } from './fakeDiscussion'; @@ -13,10 +14,6 @@ const googleCloudBlogUrl = 'https://cloud.google.com/blog/topics/inside-google-cloud/whats-new-google-cloud'; const googleCloudBlogImage = 'https://storage.googleapis.com/gweb-cloudblog-publish/images/whats_new_2026_CfhxFWX.max-2500x2500.jpg'; -// A different Google Cloud blog cover for the ad slot, so it doesn't repeat -// the sponsored blog card's image. -const googleCloudAdImage = - 'https://storage.googleapis.com/gweb-cloudblog-publish/images/1148-GC-IO-Header-GC-43-0519.max-2500x2500.jpg'; // Rendered through the real ArticleGrid/ArticleList so the sponsored post // looks identical to an organic feed card. The Google Cloud logo is supplied @@ -58,7 +55,7 @@ export const googleCloudAd: Ad = { 'Code more, config less. 👩‍💻 Deploy in seconds. Offload the infrastructure to Google Cloud.', link: 'https://cloud.google.com/free', source: 'Google Cloud', - image: googleCloudAdImage, + image: googleCloudAdCoverDataUri, companyLogo: googleCloudLogoDataUri, callToAction: 'Start building free', // Advertiser cards carry tags like organic cards; these drive the chips on From f2ab63e7b12ab618f9cdc8a04daadfd733578c4a Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 16:27:57 +0300 Subject: [PATCH 30/38] feat: real ad cover image + takeover on tag feed page - Embed the advertiser's 'Wasting time managing infra?' cover as a base64 data URI (replaces the SVG recreation; no expiring CDN dependency). - Extend the takeover to the tag feed (OtherFeedPage.Tag) so clicking the sponsored 'ai' tag lands on a page that also carries the engagement placement. Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/Feed.tsx | 6 ++- .../features/googleCloudTakeover/adCover.ts | 38 +++---------------- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index 85020e3af9f..419179b33f0 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -220,7 +220,11 @@ export default function Feed({ googleCloudTakeoverEnabled && !isTesting && (feedName === SharedFeedPage.MyFeed || - feedName === SharedFeedPage.Popular) && + feedName === SharedFeedPage.Popular || + // The advertiser takeover follows the user onto a tag feed (e.g. clicking + // the sponsored "ai" tag lands on /tags/ai, which should also carry the + // Google Cloud engagement placements). + feedName === OtherFeedPage.Tag) && !isHorizontal; const showAcquisitionForm = isMyFeed && diff --git a/packages/shared/src/features/googleCloudTakeover/adCover.ts b/packages/shared/src/features/googleCloudTakeover/adCover.ts index 44bda28c41e..7edad17fe95 100644 --- a/packages/shared/src/features/googleCloudTakeover/adCover.ts +++ b/packages/shared/src/features/googleCloudTakeover/adCover.ts @@ -1,33 +1,7 @@ -// Cover art for the Google Cloud ad (third) card, rebuilt as a self-contained -// SVG data URI so the long-lived demo has no external image dependency. -// Recreates the supplied creative: light surface, multicolor "Google Cloud" -// wordmark, "Wasting time managing infra?" headline, and the gradient circles. +// Cover art for the Google Cloud ad (third) card. The advertiser-supplied +// creative ("Wasting time managing infra?"), embedded as a base64 data URI +// so the long-lived demo has no external image dependency (the original CDN +// URL was a signed, expiring link). -const svg = ` - - - - - - - - - - - - - - - - Google Cloud - - - Wasting time - managing - infra? - -`; - -export const googleCloudAdCoverDataUri = `data:image/svg+xml,${encodeURIComponent( - svg, -)}`; +export const googleCloudAdCoverDataUri = + 'data:image/jpeg;base64,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C7FNNNNNNNNNNNNNP087jsY/P1yEfBl6Hn7j5qmmmmmmmmmmmmmmn6HHP+L65kPtpl6v3HxaaaaaaaaaaaaaaafoFvzx9U+Hthq7e/3ISG4+jTTTTTTTTTTTTTT6y8I93+fVaD+Ky9X7nCtmmmmmmmmmmmmmmmn1+AxZ7z9PzMH4z90uG6zTTTTTTTTTTTTTTT6IgLqwc2jK0j9+fphrA+Bt7vukh2c6SLU000000000000001cBscf8n0mywFeRStZZ7/ddD8b000000000000001kZ8ONYgPJ1ev0z4y+zP6dfuX//2gAMAwECAgIDAgAAEPvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvvuvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvuh3PdMftxHFPNNPvvvvvvvvvvvvvvvvvvvvvqvvszXzFvceKHKOFPvvvvvvvvvvvvvvvvvvvvvqvvvvvvjPvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvvv8A7fr/AO+++yvf++++++yj+++++++++++++++q++7z0vTL/LjEPvbz/T/vQPXDn/8At0/vvvvvvvqvvvyW0a1yxx1X6zx23v7m6/f2W1b8/vvvvvvvqvvvvfgU754+125/w73+/r799k6459/vvvvvvvqvvvnvnrjnjnrnrvvm77/vjrvnrvrvvvvvvvvvqvvvvvvvvvvvvvvvvo7vvvvvvvvvvvvvvvvvvvqvvk434+270z7+w+0W3198z9/vvvvvvvvvvvvvqvvh7vw5/wDfcM8+Tf8Artetf7X++++++++++++++q++b+3PLn7fP7fLLwvz6vrTf++++++++++++++q++++++6++++6y+rT++++f3e++++++++++++++q++3+z2NT26y3fe+++++++++++++++++++++++q++XFfndL/fJ2X++++++++++++++++++++++++q++XDqX/wD0u9ev/vvvvvvvvvvvvvvvvvvvvvvvqvvj/wD6+/8A+/f+v++++++/72++++++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'; From ce2baa62b6697193a0ffbf2495c02f926cce7651 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 16:40:39 +0300 Subject: [PATCH 31/38] fix: blog card Read post opens new tab (Google blocks in-app reader embed) Google returns 403 to the in-app reader's embed proxy for cloud.google.com, so revert the first card to a non-reader-gated Share type and open the real blog URL in a new tab on Read post. Co-Authored-By: Claude Opus 4.8 --- .../googleCloudTakeover/GoogleCloudBlogCard.tsx | 11 +---------- .../src/features/googleCloudTakeover/content.ts | 9 ++++----- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx index 8393c0df33d..0a0381d4abf 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudBlogCard.tsx @@ -7,8 +7,6 @@ import ArticlePostModal from '../../components/modals/ArticlePostModal'; import { PostPosition } from '../../hooks/usePostModalNavigation'; import type { Post } from '../../graphql/posts'; import { UserVote } from '../../graphql/posts'; -import { useLazyModal } from '../../hooks/useLazyModal'; -import { LazyModal } from '../../components/modals/common/types'; import { googleCloudBlogPost } from './content'; import { seedGoogleCloudDiscussion } from './fakeDiscussion'; @@ -35,17 +33,10 @@ export const GoogleCloudBlogCard = ({ className, }: GoogleCloudBlogCardProps): ReactElement => { const queryClient = useQueryClient(); - const { openModal } = useLazyModal(); const [isModalOpen, setIsModalOpen] = useState(false); const [post, setPost] = useState(googleCloudBlogPost); const Card = isList ? ArticleList : ArticleGrid; - // Open the real URL inside the daily.dev in-app reader/browser (only this - // first card does this). Opening the ReaderPreview modal directly bypasses - // the per-user reader gate so it works on any demo account. - const openReader = () => - openModal({ type: LazyModal.ReaderPreview, props: { post } }); - // Seed the simulated discussion so the post modal shows engagement. useEffect(() => { seedGoogleCloudDiscussion(queryClient, googleCloudBlogPost.id); @@ -82,7 +73,7 @@ export const GoogleCloudBlogCard = ({ onBookmarkClick={toggleBookmark} onShare={noop} onCopyLinkClick={noop} - onReadArticleClick={openReader} + onReadArticleClick={openBlog} onMenuClick={noop} openNewTab domProps={{ className }} diff --git a/packages/shared/src/features/googleCloudTakeover/content.ts b/packages/shared/src/features/googleCloudTakeover/content.ts index f139024f9cd..018a8b587a8 100644 --- a/packages/shared/src/features/googleCloudTakeover/content.ts +++ b/packages/shared/src/features/googleCloudTakeover/content.ts @@ -28,7 +28,6 @@ export const googleCloudBlogPost: Post = { createdAt: hoursAgo(5), readTime: 6, image: googleCloudBlogImage, - // `domain` drives the reader header favicon when opened in the in-app reader. domain: 'cloud.google.com', source: { id: 'google-cloud-blog', @@ -41,10 +40,10 @@ export const googleCloudBlogPost: Post = { numUpvotes: 312, numComments: googleCloudDiscussionCount, numAwards: 0, - // Article so "Read post" can open the real URL inside the daily.dev in-app - // reader/browser (READER_GATE_ELIGIBLE_TYPES). The card mounts the reader - // explicitly on read; see GoogleCloudBlogCard. - type: PostType.Article, + // Share (not Article) so it is NOT reader-gated: Google blocks its blog from + // loading in the in-app reader's embed proxy (returns a 403), so "Read post" + // opens the real URL in a new tab instead. See GoogleCloudBlogCard. + type: PostType.Share, }; // Rendered through the real AdGrid/AdList so it matches the live ad slot. From 1c9ffc4bca23c30fd8c76e0927d8eb9962abd99c Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 16:42:20 +0300 Subject: [PATCH 32/38] revert: use the original Google Cloud I/O header as the ad cover image Drops the 'Wasting time managing infra' embedded image and restores the prior storage.googleapis.com cover. Co-Authored-By: Claude Opus 4.8 --- .../shared/src/features/googleCloudTakeover/adCover.ts | 7 ------- .../shared/src/features/googleCloudTakeover/content.ts | 7 +++++-- 2 files changed, 5 insertions(+), 9 deletions(-) delete mode 100644 packages/shared/src/features/googleCloudTakeover/adCover.ts diff --git a/packages/shared/src/features/googleCloudTakeover/adCover.ts b/packages/shared/src/features/googleCloudTakeover/adCover.ts deleted file mode 100644 index 7edad17fe95..00000000000 --- a/packages/shared/src/features/googleCloudTakeover/adCover.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Cover art for the Google Cloud ad (third) card. The advertiser-supplied -// creative ("Wasting time managing infra?"), embedded as a base64 data URI -// so the long-lived demo has no external image dependency (the original CDN -// URL was a signed, expiring link). - -export const googleCloudAdCoverDataUri = - 'data:image/jpeg;base64,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C7FNNNNNNNNNNNNNP087jsY/P1yEfBl6Hn7j5qmmmmmmmmmmmmmmn6HHP+L65kPtpl6v3HxaaaaaaaaaaaaaaafoFvzx9U+Hthq7e/3ISG4+jTTTTTTTTTTTTTT6y8I93+fVaD+Ky9X7nCtmmmmmmmmmmmmmmmn1+AxZ7z9PzMH4z90uG6zTTTTTTTTTTTTTTT6IgLqwc2jK0j9+fphrA+Bt7vukh2c6SLU000000000000001cBscf8n0mywFeRStZZ7/ddD8b000000000000001kZ8ONYgPJ1ev0z4y+zP6dfuX//2gAMAwECAgIDAgAAEPvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvvuvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvuh3PdMftxHFPNNPvvvvvvvvvvvvvvvvvvvvvqvvszXzFvceKHKOFPvvvvvvvvvvvvvvvvvvvvvqvvvvvvjPvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvqvvv8A7fr/AO+++yvf++++++yj+++++++++++++++q++7z0vTL/LjEPvbz/T/vQPXDn/8At0/vvvvvvvqvvvyW0a1yxx1X6zx23v7m6/f2W1b8/vvvvvvvqvvvvfgU754+125/w73+/r799k6459/vvvvvvvqvvvnvnrjnjnrnrvvm77/vjrvnrvrvvvvvvvvvqvvvvvvvvvvvvvvvvo7vvvvvvvvvvvvvvvvvvvqvvk434+270z7+w+0W3198z9/vvvvvvvvvvvvvqvvh7vw5/wDfcM8+Tf8Artetf7X++++++++++++++q++b+3PLn7fP7fLLwvz6vrTf++++++++++++++q++++++6++++6y+rT++++f3e++++++++++++++q++3+z2NT26y3fe+++++++++++++++++++++++q++XFfndL/fJ2X++++++++++++++++++++++++q++XDqX/wD0u9ev/vvvvvvvvvvvvvvvvvvvvvvvqvvj/wD6+/8A+/f+v++++++/72++++++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'; diff --git a/packages/shared/src/features/googleCloudTakeover/content.ts b/packages/shared/src/features/googleCloudTakeover/content.ts index 018a8b587a8..13001fc020e 100644 --- a/packages/shared/src/features/googleCloudTakeover/content.ts +++ b/packages/shared/src/features/googleCloudTakeover/content.ts @@ -6,7 +6,6 @@ import type { Ad, Post } from '../../graphql/posts'; import { PostType } from '../../graphql/posts'; import { googleCloudLogoDataUri } from './GoogleCloudLogo'; -import { googleCloudAdCoverDataUri } from './adCover'; import { hoursAgo } from './relativeTime'; import { googleCloudDiscussionCount } from './fakeDiscussion'; @@ -14,6 +13,10 @@ const googleCloudBlogUrl = 'https://cloud.google.com/blog/topics/inside-google-cloud/whats-new-google-cloud'; const googleCloudBlogImage = 'https://storage.googleapis.com/gweb-cloudblog-publish/images/whats_new_2026_CfhxFWX.max-2500x2500.jpg'; +// A different Google Cloud blog cover for the ad slot, so it doesn't repeat +// the sponsored blog card's image. +const googleCloudAdImage = + 'https://storage.googleapis.com/gweb-cloudblog-publish/images/1148-GC-IO-Header-GC-43-0519.max-2500x2500.jpg'; // Rendered through the real ArticleGrid/ArticleList so the sponsored post // looks identical to an organic feed card. The Google Cloud logo is supplied @@ -54,7 +57,7 @@ export const googleCloudAd: Ad = { 'Code more, config less. 👩‍💻 Deploy in seconds. Offload the infrastructure to Google Cloud.', link: 'https://cloud.google.com/free', source: 'Google Cloud', - image: googleCloudAdCoverDataUri, + image: googleCloudAdImage, companyLogo: googleCloudLogoDataUri, callToAction: 'Start building free', // Advertiser cards carry tags like organic cards; these drive the chips on From 875bb679869ae7e681cb312836692e0350c4c03a Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 17:23:09 +0300 Subject: [PATCH 33/38] fix: kill the action-pill spinner during the takeover demo - Suppress app-wide engagement-ads creatives while the takeover is on, so real feed posts no longer animate / fetch a brand logo on upvote (the spinner). The Google Cloud engagement card keeps its animation via its scoped provider. - Make the glass-bar copy-link button a static synchronous clipboard write with no interaction tracking, so it can never render a loading spinner. Co-Authored-By: Claude Opus 4.8 --- .../components/cards/common/FeedCardGlassActions.tsx | 12 ++++++++++-- .../shared/src/contexts/EngagementAdsContext.tsx | 11 ++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx b/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx index f2808b9a2eb..771dae3d15a 100644 --- a/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx +++ b/packages/shared/src/components/cards/common/FeedCardGlassActions.tsx @@ -70,7 +70,6 @@ export function FeedCardGlassActions({ onToggleUpvote, onToggleDownvote, onToggleBookmark, - onCopyLink, } = useCardActions({ post, onUpvoteClick, @@ -217,11 +216,20 @@ export function FeedCardGlassActions({
+ {/* DEMO: static copy button. Does a plain synchronous clipboard + write with no interaction tracking or async state, so it can + never render a loading spinner in the action pill. */} } - onClick={onCopyLink} + onClick={() => { + if (typeof navigator !== 'undefined') { + navigator.clipboard?.writeText( + post.commentsPermalink || post.permalink || '', + ); + } + }} variant={ButtonVariant.Tertiary} color={ButtonColor.Cabbage} className="pointer-events-auto" diff --git a/packages/shared/src/contexts/EngagementAdsContext.tsx b/packages/shared/src/contexts/EngagementAdsContext.tsx index 23917ae4c02..aa69bf0ffde 100644 --- a/packages/shared/src/contexts/EngagementAdsContext.tsx +++ b/packages/shared/src/contexts/EngagementAdsContext.tsx @@ -12,7 +12,8 @@ import { } from '../lib/engagementAds'; import { useIsLightTheme } from '../hooks/utils/useThemedAsset'; import { useAuthContext } from './AuthContext'; -import { isProduction } from '../lib/constants'; +import { isProduction, isTesting } from '../lib/constants'; +import { googleCloudTakeoverEnabled } from '../features/googleCloudTakeover/config'; interface EngagementAdsContextValue { /** All creatives from boot, theme-resolved */ @@ -50,6 +51,14 @@ export const EngagementAdsProvider = ({ const { user } = useAuthContext(); const resolvedCreatives = useMemo(() => { + // DEMO: during the Google Cloud takeover, suppress the app-wide engagement + // creatives so real feed posts don't animate / fetch a brand logo on upvote + // (that's the only Google Cloud engagement ad in the demo, and it's scoped + // to its own provider on the engagement card). + if (googleCloudTakeoverEnabled && !isTesting) { + return []; + } + if (isProduction && user?.isPlus) { return []; } From c2a1e098f1207a1533164a09014d184ff8271db0 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 17:28:25 +0300 Subject: [PATCH 34/38] feat: show takeover on the explore-tag feed too The AI tab in the feed bar opens the explore-tag feed (OtherFeedPage.ExploreTag), not /tags/ai. Add it to the takeover gate so the Google Cloud engagement placements appear there as well. No Plus gating exists on the takeover, so it shows for all users. Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/Feed.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index 419179b33f0..bfeccbaabdf 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -221,10 +221,12 @@ export default function Feed({ !isTesting && (feedName === SharedFeedPage.MyFeed || feedName === SharedFeedPage.Popular || - // The advertiser takeover follows the user onto a tag feed (e.g. clicking - // the sponsored "ai" tag lands on /tags/ai, which should also carry the - // Google Cloud engagement placements). - feedName === OtherFeedPage.Tag) && + // The advertiser takeover follows the user onto a tag feed: both the + // standard tag page (/tags/ai → OtherFeedPage.Tag) and the explore-tag + // feed reached via the feed tab bar (/explore/ai → ExploreTag) carry the + // Google Cloud engagement placements. + feedName === OtherFeedPage.Tag || + feedName === OtherFeedPage.ExploreTag) && !isHorizontal; const showAcquisitionForm = isMyFeed && From 47f1b1ba21a86bbdb70293b8d9eaba0470818e31 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 17:41:23 +0300 Subject: [PATCH 35/38] feat: surface Google Cloud placement at the top of the tag page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: the tag page's post feed ('All posts about …') is the last section, so the in-feed takeover lands far below the fold (under Roadmaps, recommended stories, and who-to-follow). Render the Google Cloud strip prominently near the top of the tag page so the placement is visible without scrolling. Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/tags/TagTopicPage.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/components/tags/TagTopicPage.tsx b/packages/shared/src/components/tags/TagTopicPage.tsx index 74938355a8f..f0562beb19f 100644 --- a/packages/shared/src/components/tags/TagTopicPage.tsx +++ b/packages/shared/src/components/tags/TagTopicPage.tsx @@ -57,7 +57,9 @@ import EntityCardSkeleton from '../cards/entity/EntityCardSkeleton'; import { TagPageNavbar } from './TagPageNavbar'; import { PublicPageSignupBanner } from '../auth/PublicPageSignupBanner'; import { largeNumberFormat } from '../../lib/numberFormat'; -import { webappUrl } from '../../lib/constants'; +import { isTesting, webappUrl } from '../../lib/constants'; +import { GoogleCloudStrip } from '../../features/googleCloudTakeover/GoogleCloudStrip'; +import { googleCloudTakeoverEnabled } from '../../features/googleCloudTakeover/config'; import { Typography, TypographyColor, @@ -461,6 +463,14 @@ export const TagTopicPage = ({
+ {/* DEMO: the tag page's post feed ("All posts about …") is the last + section, so the in-feed takeover lands far below the fold. Surface + the Google Cloud placement prominently at the top of the tag page + too, so it's visible without scrolling. */} + {googleCloudTakeoverEnabled && !isTesting && ( + + )} + {showRoadmap && initialData?.flags?.roadmap && (
Roadmaps From 3a5d9546a54f862e013bb9cd99f8e06dd6bf5f95 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 17:53:01 +0300 Subject: [PATCH 36/38] chore: shorten Google Cloud ad card title Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/features/googleCloudTakeover/content.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/shared/src/features/googleCloudTakeover/content.ts b/packages/shared/src/features/googleCloudTakeover/content.ts index 13001fc020e..672d047790a 100644 --- a/packages/shared/src/features/googleCloudTakeover/content.ts +++ b/packages/shared/src/features/googleCloudTakeover/content.ts @@ -53,8 +53,7 @@ export const googleCloudBlogPost: Post = { // `companyLogo` drives the favicon; `image` drives the cover. export const googleCloudAd: Ad = { company: 'Google Cloud', - description: - 'Code more, config less. 👩‍💻 Deploy in seconds. Offload the infrastructure to Google Cloud.', + description: 'Code more, config less. 👩‍💻 Deploy in seconds.', link: 'https://cloud.google.com/free', source: 'Google Cloud', image: googleCloudAdImage, From 1e41d67cfb52ab3fe84e228c5072d5e9fafeeec7 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 18:04:28 +0300 Subject: [PATCH 37/38] feat: give the engagement card its own distinct discussion The second (engagement) card now seeds a separate set of comments about shipping AI agents to production (tool calling, evals, cost control, guardrails) instead of reusing the blog post's Google Cloud launch discussion. Co-Authored-By: Claude Opus 4.8 --- .../GoogleCloudEngagementCard.tsx | 7 +- .../googleCloudTakeover/engagementContent.ts | 4 +- .../googleCloudTakeover/fakeDiscussion.ts | 196 ++++++++++++++++-- 3 files changed, 188 insertions(+), 19 deletions(-) diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudEngagementCard.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudEngagementCard.tsx index 86c8463cb38..7531504000e 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudEngagementCard.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudEngagementCard.tsx @@ -9,7 +9,7 @@ import type { Post } from '../../graphql/posts'; import { UserVote } from '../../graphql/posts'; import { GoogleCloudEngagementProvider } from './GoogleCloudEngagementProvider'; import { googleCloudEngagementPost } from './engagementContent'; -import { seedGoogleCloudDiscussion } from './fakeDiscussion'; +import { seedGoogleCloudEngagementDiscussion } from './fakeDiscussion'; type GoogleCloudEngagementCardProps = { isList?: boolean; @@ -43,7 +43,10 @@ export const GoogleCloudEngagementCard = ({ const Card = isList ? ArticleList : ArticleGrid; useEffect(() => { - seedGoogleCloudDiscussion(queryClient, googleCloudEngagementPost.id); + seedGoogleCloudEngagementDiscussion( + queryClient, + googleCloudEngagementPost.id, + ); }, [queryClient]); const toggleVote = (vote: UserVote) => diff --git a/packages/shared/src/features/googleCloudTakeover/engagementContent.ts b/packages/shared/src/features/googleCloudTakeover/engagementContent.ts index 43801751e15..a434f807ae1 100644 --- a/packages/shared/src/features/googleCloudTakeover/engagementContent.ts +++ b/packages/shared/src/features/googleCloudTakeover/engagementContent.ts @@ -6,7 +6,7 @@ import type { Post } from '../../graphql/posts'; import { PostType } from '../../graphql/posts'; import { googleCloudLogoDataUri } from './GoogleCloudLogo'; import { hoursAgo } from './relativeTime'; -import { googleCloudDiscussionCount } from './fakeDiscussion'; +import { googleCloudEngagementDiscussionCount } from './fakeDiscussion'; const engagementPostUrl = 'https://huggingface.co/blog/building-production-ai-agents'; @@ -39,7 +39,7 @@ export const googleCloudEngagementPost: Post = { } as unknown as Post['source'], tags: [googleCloudSponsoredTag, 'machine-learning', 'llm', 'python'], numUpvotes: 1843, - numComments: googleCloudDiscussionCount, + numComments: googleCloudEngagementDiscussionCount, numAwards: 0, type: PostType.Share, }; diff --git a/packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts b/packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts index ad19553f4dd..a8fc9c86e4b 100644 --- a/packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts +++ b/packages/shared/src/features/googleCloudTakeover/fakeDiscussion.ts @@ -289,6 +289,152 @@ peak_rps * avg_cpu_ms / 1000 > provisioned_vcpu * 0.6

If that' }, ]; +// A separate discussion for the second (engagement) card, whose post is about +// shipping AI agents to production. Distinct voices/topics from the blog post +// thread above so the two cards don't repeat the same comments. +const engagementSpecs: Spec[] = [ + { + p: 2, + up: 188, + h: 4, + html: `

The single biggest reliability win for us was validating tool arguments before executing, then retrying with the validation error fed back to the model. Naive tool calling failed ~8% of the time; this got us under 1%.

const parsed = toolSchema.safeParse(call.args);
+if (!parsed.success) {
+  messages.push(toolError(call, parsed.error));
+  continue; // let the model correct itself
+}
`, + replies: [ + { + p: 7, + up: 14, + h: 3, + html: `

Do you cap the retries? We've seen a model loop forever re-emitting the same bad args.

`, + }, + { + p: 2, + up: 31, + h: 2, + html: `

Hard cap at 3, then bail to a human. The infinite loop only happens if you don't feed the actual error back; once it can see what was wrong it usually fixes it on the first retry.

`, + }, + ], + }, + { + p: 10, + up: 142, + h: 6, + html: `

Evals are the part nobody wants to build and the part that actually ships you to prod. We treat real agent traces like test fixtures: capture failures, freeze them, assert behavior doesn't regress. Without it you're just vibing in production.

`, + }, + { + p: 4, + up: 8, + h: 9, + html: `

Newer to this. For a support agent over our own docs, is RAG enough or do we actually need to fine-tune a model?

`, + replies: [ + { + p: 8, + up: 47, + h: 8, + html: `

Start with RAG, almost always. Fine-tuning is for style and format, not knowledge. In my experience 90% of "we need fine-tuning" is really "our retrieval is bad".

`, + }, + ], + }, + { + p: 6, + up: 97, + h: 12, + html: `

Cost control deserves its own chapter. Prompt-caching the system prompt and tool definitions cut our per-call cost by about 60%, because that block is byte-identical every turn. Track tokens per resolved task, not per call, or you'll optimize the wrong thing.

`, + }, + { + p: 3, + up: 79, + h: 8, + html: `

Contrarian take: "agentic AI" is a while loop with extra steps. What is actually new here versus a for-loop that calls functions?

`, + replies: [ + { + p: 10, + up: 58, + h: 7, + html: `

Mechanically, sure. The new part is the eval and tracing harness around the loop plus tool-call validation. The loop was never the hard bit; the production scaffolding is.

`, + }, + ], + }, + { + p: 9, + up: 113, + h: 5, + html: `

Per-step tracing changed how we debug agents. Replaying a failed run with every tool call and token inline is the difference between guessing and fixing. Our latency breakdown by step:

agent run latency breakdown by step

Turned out 70% of our wall-clock was one slow retrieval call, not the model.

`, + }, + { + p: 12, + up: 64, + h: 16, + html: `

Treat every tool the agent can call as an attack surface. We allowlist tools per session and never let the model build raw SQL or shell. Prompt injection from retrieved documents is real, and your RAG layer is the front door.

`, + }, + { + p: 5, + up: 21, + h: 19, + html: `

How is everyone handling perceived latency on multi-step agents? Users hate staring at a spinner for eight seconds.

`, + replies: [ + { + p: 11, + up: 26, + h: 18, + html: `

Stream the intermediate steps. Showing "searching docs... reading 3 results..." makes a 6s task feel fast. Same work, the wait just feels different.

`, + }, + ], + }, + { + p: 1, + up: 9, + h: 22, + html: `

Switching to strict structured outputs removed a whole class of parsing bugs. We used to regex the model's prose to pull fields out. Never again.

`, + }, + { + p: 13, + up: 54, + h: 27, + html: `

Context-window management is the unglamorous 80%. We summarize older turns and keep a rolling window. Rough guard before each call:

if (estimateTokens(messages) > LIMIT * 0.7) {
+  messages = [system, summarize(older), ...recent];
+}
`, + }, + { + p: 14, + up: 3, + h: 31, + html: `

Spent two weeks on a clever multi-agent setup, deleted it, replaced it with one good prompt and three tools. Faster, cheaper, and I sleep now.

`, + }, + { + p: 11, + up: 88, + h: 13, + html: `

For anything that writes (refunds, emails, deploys) we gate on human approval. The agent proposes, a person confirms. That gate caught exactly one very expensive mistake in week two and paid for itself.

`, + }, + { + p: 0, + up: 46, + h: 30, + html: `

Resist the urge to build a swarm of agents on day one. One agent with good tools beats five agents passing messages and hallucinating about each other's state.

`, + }, + { + p: 9, + up: 71, + h: 20, + html: `

We default to a cheaper, faster model and only escalate to the frontier one when an eval gate fails. Most steps don't need the big model. Routing on difficulty cut our cost more than any prompt tweak did.

`, + }, + { + p: 8, + up: 1, + h: 40, + html: `

This matches our last year painfully well. Wish I'd read it before, not after.

`, + }, + { + p: 15, + up: 0, + h: 44, + html: `

Saving this for the team offsite. The evals section alone is worth it.

`, + }, +]; + const buildAuthor = (personIndex: number, key: string): Author => { const person = people[personIndex]; return { @@ -313,11 +459,11 @@ const buildAuthor = (personIndex: number, key: string): Author => { } as Author; }; -const buildComments = (): Comment[] => - specs.map((spec, i): Comment => { +const buildComments = (specList: Spec[], idPrefix: string): Comment[] => + specList.map((spec, i): Comment => { const children = (spec.replies ?? []).map((reply, j) => ({ node: { - id: `gcp-comment-${i}-r${j}`, + id: `${idPrefix}-${i}-r${j}`, content: '', contentHtml: reply.html, contentEmbeds: [], @@ -326,13 +472,13 @@ const buildComments = (): Comment[] => permalink: 'https://cloud.google.com/blog', numUpvotes: reply.up, numAwards: 0, - author: buildAuthor(reply.p, `${i}-r${j}`), + author: buildAuthor(reply.p, `${idPrefix}-${i}-r${j}`), children: { edges: [], pageInfo: { hasNextPage: false } }, } as Comment, })); return { - id: `gcp-comment-${i}`, + id: `${idPrefix}-${i}`, content: '', contentHtml: spec.html, contentEmbeds: [], @@ -341,31 +487,38 @@ const buildComments = (): Comment[] => permalink: 'https://cloud.google.com/blog', numUpvotes: spec.up, numAwards: 0, - author: buildAuthor(spec.p, `${i}`), + author: buildAuthor(spec.p, `${idPrefix}-${i}`), children: { edges: children, pageInfo: { hasNextPage: false } }, } as Comment; }); -// Total comments (top-level + replies) so the post header count matches. -export const googleCloudDiscussionCount = specs.reduce( - (sum, spec) => sum + 1 + (spec.replies?.length ?? 0), - 0, -); +// Total comments (top-level + replies) so each post's header count matches. +const countComments = (specList: Spec[]): number => + specList.reduce((sum, spec) => sum + 1 + (spec.replies?.length ?? 0), 0); -export const buildGoogleCloudDiscussion = (): PostCommentsData => ({ +export const googleCloudDiscussionCount = countComments(specs); +export const googleCloudEngagementDiscussionCount = + countComments(engagementSpecs); + +const buildDiscussion = ( + specList: Spec[], + idPrefix: string, +): PostCommentsData => ({ postComments: { - edges: buildComments().map((node) => ({ node })), + edges: buildComments(specList, idPrefix).map((node) => ({ node })), pageInfo: { hasNextPage: false, endCursor: null }, }, }); // Seed every comments-query-key variant for the post and pin it so the live // (empty) refetch can't replace the simulated discussion. -export const seedGoogleCloudDiscussion = ( +const seedDiscussion = ( queryClient: QueryClient, postId: string, + specList: Spec[], + idPrefix: string, ): void => { - const data = buildGoogleCloudDiscussion(); + const data = buildDiscussion(specList, idPrefix); const keys = [ generateCommentsQueryKey({ postId }), ...getAllCommentsQuery(postId), @@ -378,3 +531,16 @@ export const seedGoogleCloudDiscussion = ( queryClient.setQueryData(key, data); }); }; + +// First (blog) card discussion. +export const seedGoogleCloudDiscussion = ( + queryClient: QueryClient, + postId: string, +): void => seedDiscussion(queryClient, postId, specs, 'gcp-comment'); + +// Second (engagement) card discussion — a distinct set of comments. +export const seedGoogleCloudEngagementDiscussion = ( + queryClient: QueryClient, + postId: string, +): void => + seedDiscussion(queryClient, postId, engagementSpecs, 'gcp-eng-comment'); From dc7c3e654c56405a2ea9800758c8133f0a685ab0 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 22 Jun 2026 18:08:21 +0300 Subject: [PATCH 38/38] feat: brand upvote animation in post modal; remove ad card CTA - Wire the branded upvote animation into FocusCardActionBar so opening the engagement post and upvoting shows the Google Cloud icon swap, matching the feed card. Scoped to the engagement card's provider, so real posts are unaffected. - Remove the primary white CTA button from the Google Cloud ad (3rd) card. Co-Authored-By: Claude Opus 4.8 --- .../post/focus/FocusCardActionBar.tsx | 28 +++++++++++++++++-- .../googleCloudTakeover/GoogleCloudHeadAd.tsx | 28 ++----------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index 11783726f2f..974558896ec 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -1,11 +1,12 @@ import type { ReactElement } from 'react'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; import type { Post } from '../../../graphql/posts'; import { UserVote } from '../../../graphql/posts'; import { useViewSize, useVotePost, ViewSize } from '../../../hooks'; import { useBookmarkPost } from '../../../hooks/useBookmarkPost'; import { useBlockPostPanel } from '../../../hooks/post/useBlockPostPanel'; +import { useBrandSponsorship } from '../../../hooks/useBrandSponsorship'; import { useCanAwardUser } from '../../../hooks/useCoresFeature'; import { useLazyModal } from '../../../hooks/useLazyModal'; import { LazyModal } from '../../modals/common/types'; @@ -66,6 +67,25 @@ export const FocusCardActionBar = ({ sendingUser: user, receivingUser: post.author as LoggedUser | undefined, }); + const { getUpvoteAnimation } = useBrandSponsorship(); + + // Branded upvote animation (icon swaps to the advertiser logo) when the post + // has a sponsored tag (engagement ad) — same as the feed card. + const brandAnimation = useMemo(() => { + const animationResult = getUpvoteAnimation(post.tags || []); + if ( + !animationResult.shouldAnimate || + !animationResult.colors || + !animationResult.config + ) { + return null; + } + return { + colors: animationResult.colors, + config: animationResult.config, + brandLogo: animationResult.brandLogo, + }; + }, [getUpvoteAnimation, post.tags]); // Track whether the bar is pinned, and at which edge. The sentinel sits just // above the bar: when it scrolls above the viewport top the bar is pinned at @@ -233,8 +253,10 @@ export const FocusCardActionBar = ({ id="upvote-post-btn" label="Upvote" color={ButtonColor.Avocado} - icon={} - iconPressed={} + icon={} + iconPressed={ + + } count={isPinned ? upvotes : undefined} pressed={isUpvoteActive} onClick={onToggleUpvote} diff --git a/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx b/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx index c33929b9992..dda6ab5ee0d 100644 --- a/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx +++ b/packages/shared/src/features/googleCloudTakeover/GoogleCloudHeadAd.tsx @@ -14,13 +14,7 @@ import { AdPixel } from '../../components/cards/ad/common/AdPixel'; import { AdFavicon } from '../../components/cards/ad/common/AdFavicon'; import { AdList } from '../../components/cards/ad/AdList'; import PostTags from '../../components/cards/common/PostTags'; -import { - Button, - ButtonSize, - ButtonVariant, -} from '../../components/buttons/Button'; import { ActiveFeedContext } from '../../contexts/ActiveFeedContext'; -import { combinedClicks } from '../../lib/click'; import { googleCloudAd } from './content'; type GoogleCloudHeadAdProps = { @@ -32,11 +26,9 @@ const noop = () => undefined; const adFeedContext = { items: [], queryKey: ['gcp-takeover-ad'] }; // The Google Cloud ad slot. Built from the real ad sub-components so it reads -// like a production ad card, with takeover tweaks: -// - the CTA ("Start building free") is hidden and revealed on hover in the -// top-right corner (mirrors the post card's "Read post" hover affordance); -// - the only attribution is "Promoted", styled to match the date / read-time -// metadata of organic post cards (no "Advertise here" / "Remove"). +// like a production ad card, with takeover tweaks: the only attribution is +// "Promoted", styled to match the date / read-time metadata of organic post +// cards (no CTA button / "Advertise here" / "Remove"). // On list/mobile layout there's no hover, so fall back to the standard AdList. export const GoogleCloudHeadAd = ({ isList = false, @@ -86,20 +78,6 @@ export const GoogleCloudHeadAd = ({ ad={googleCloudAd} ImageComponent={CardImage} /> - {!!googleCloudAd.callToAction && ( - - )}