diff --git a/packages/shared/src/components/FeedItemComponent.tsx b/packages/shared/src/components/FeedItemComponent.tsx index 9240716cd2..1b505fe618 100644 --- a/packages/shared/src/components/FeedItemComponent.tsx +++ b/packages/shared/src/components/FeedItemComponent.tsx @@ -103,7 +103,7 @@ export type FeedItemComponentProps = { /** * When set, render the post as a wide featured highlight card spanning * the given number of grid columns. Only used for article-like post - * types with an active `postHighlight`. + * types with an active `hero`. */ wideColSpan?: FeaturedWideColSpan; } & Pick & diff --git a/packages/shared/src/components/cards/article/ArticleFeaturedWideGridCard.spec.tsx b/packages/shared/src/components/cards/article/ArticleFeaturedWideGridCard.spec.tsx index 89d3b58720..1d695bb16a 100644 --- a/packages/shared/src/components/cards/article/ArticleFeaturedWideGridCard.spec.tsx +++ b/packages/shared/src/components/cards/article/ArticleFeaturedWideGridCard.spec.tsx @@ -8,8 +8,8 @@ import post from '../../../../__tests__/fixture/post'; import type { PostCardProps } from '../common/common'; import type { Post, - PostHighlight, - PostHighlightSignificance, + PostHero, + PostHeroSignificance, } from '../../../graphql/posts'; import { TestBootProvider } from '../../../../__tests__/helpers/boot'; import { ArticleFeaturedWideGridCard } from './ArticleFeaturedWideGridCard'; @@ -39,13 +39,10 @@ const defaultProps: PostCardProps = { onReadArticleClick: jest.fn(), }; -const makeHighlight = ( - significance: PostHighlightSignificance | null, -): PostHighlight | null => +const makeHero = (significance: PostHeroSignificance | null): PostHero | null => significance ? { id: 'h1', - channel: 'vibes', highlightedAt: '2026-05-25T00:00:00.000Z', headline: 'A breaking event', significance, @@ -61,12 +58,12 @@ const renderComponent = ( , ); -const postWith = (significance: PostHighlightSignificance | null): Post => ({ +const postWith = (significance: PostHeroSignificance | null): Post => ({ ...post, - postHighlight: makeHighlight(significance), + hero: makeHero(significance), }); -it.each<[PostHighlightSignificance, string]>([ +it.each<[PostHeroSignificance, string]>([ ['breaking', 'Breaking'], ['major', 'Major'], ['notable', 'Notable'], diff --git a/packages/shared/src/components/cards/article/ArticleFeaturedWideGridCard.tsx b/packages/shared/src/components/cards/article/ArticleFeaturedWideGridCard.tsx index 5cff68ad03..0a0b5092d6 100644 --- a/packages/shared/src/components/cards/article/ArticleFeaturedWideGridCard.tsx +++ b/packages/shared/src/components/cards/article/ArticleFeaturedWideGridCard.tsx @@ -8,7 +8,7 @@ import { useBlockPostPanel } from '../../../hooks/post/useBlockPostPanel'; import { useHiddenFeedbackPanel } from '../../../hooks/post/useHiddenFeedbackPanel'; import { usePostFeedback } from '../../../hooks'; import { isVideoPost, PostType } from '../../../graphql/posts'; -import type { PostHighlightSignificance } from '../../../graphql/posts'; +import type { PostHeroSignificance } from '../../../graphql/posts'; import { PostTagsPanel } from '../../post/block/PostTagsPanel'; import { CardSpace, @@ -45,17 +45,19 @@ const IMAGE_COL_SPAN: Record = { 4: 'col-span-3', }; -const CHIP_LABEL: Partial> = { +const CHIP_LABEL: Partial> = { breaking: 'Breaking', major: 'Major', notable: 'Notable', + breakout: 'Breaking out', + evergreen: 'Evergreen', }; const HighlightChip = ({ significance, className, }: { - significance: PostHighlightSignificance | null | undefined; + significance: PostHeroSignificance | null | undefined; className?: string; }): ReactElement | null => { if (!significance) { @@ -115,7 +117,7 @@ export const ArticleFeaturedWideGridCard = forwardRef( const isVideoType = isVideoPost(post); const image = usePostImage(post); const { overlay } = useCardCover({ post, onShare }); - const significance = post.postHighlight?.significance ?? null; + const significance = post.hero?.significance ?? null; const isTweetPost = post.type === PostType.SocialTwitter || post.sharedPost?.type === PostType.SocialTwitter; diff --git a/packages/shared/src/graphql/fragments.ts b/packages/shared/src/graphql/fragments.ts index b613f9c475..75ef09754c 100644 --- a/packages/shared/src/graphql/fragments.ts +++ b/packages/shared/src/graphql/fragments.ts @@ -696,11 +696,10 @@ export const FEED_POST_FRAGMENT = gql` name } } - postHighlight { + hero { id - significance headline - channel + significance highlightedAt } } diff --git a/packages/shared/src/graphql/posts.ts b/packages/shared/src/graphql/posts.ts index 49740cbafa..3df2c73cb8 100644 --- a/packages/shared/src/graphql/posts.ts +++ b/packages/shared/src/graphql/posts.ts @@ -242,6 +242,11 @@ export type PostHighlightSignificance = | 'notable' | 'routine'; +export type PostHeroSignificance = + | PostHighlightSignificance + | 'breakout' + | 'evergreen'; + export interface PostHighlight { id: string; channel: string; @@ -315,6 +320,14 @@ export interface Post { liveRoom?: LiveRoomPost | null; analytics?: Partial>; postHighlight?: PostHighlight | null; + hero?: PostHero | null; +} + +export interface PostHero { + id: string; + headline: string; + significance: PostHeroSignificance; + highlightedAt: string; } export type RelatedPost = Pick< diff --git a/packages/shared/src/hooks/feed/useLogImpression.ts b/packages/shared/src/hooks/feed/useLogImpression.ts index ed8807595a..25c39bf4e4 100644 --- a/packages/shared/src/hooks/feed/useLogImpression.ts +++ b/packages/shared/src/hooks/feed/useLogImpression.ts @@ -41,7 +41,7 @@ export default function useLogImpression( if (item.type === 'post') { const eventKey = generatePostLogEventKey(item.post.id); if (inView && !item.post.impressionStatus) { - const significance = item.post.postHighlight?.significance; + const significance = item.post.hero?.significance; logEventStart( eventKey, postLogEvent(LogEvent.Impression, item.post, { diff --git a/packages/shared/src/lib/feedHighlightColSpan.spec.ts b/packages/shared/src/lib/feedHighlightColSpan.spec.ts index 17bed7578a..641acd4f91 100644 --- a/packages/shared/src/lib/feedHighlightColSpan.spec.ts +++ b/packages/shared/src/lib/feedHighlightColSpan.spec.ts @@ -1,4 +1,4 @@ -import type { Ad, Post, PostHighlightSignificance } from '../graphql/posts'; +import type { Ad, Post, PostHeroSignificance } from '../graphql/posts'; import { PostType } from '../graphql/posts'; import type { FeedItem } from '../hooks/useFeed'; import { FeedItemType } from '../components/cards/common/common'; @@ -6,7 +6,7 @@ import { computePlacements, requestedColSpan } from './feedHighlightColSpan'; const makePost = ( overrides: Partial & { - significance?: PostHighlightSignificance | null; + significance?: PostHeroSignificance | null; } = {}, ): Post => { const { significance, ...rest } = overrides; @@ -16,10 +16,9 @@ const makePost = ( image: '', commentsPermalink: '', ...(significance !== undefined && { - postHighlight: significance + hero: significance ? { id: 'h1', - channel: 'vibes', highlightedAt: '2026-05-25T00:00:00.000Z', headline: 'h', significance, @@ -46,11 +45,11 @@ const makeAdItem = (): FeedItem => } as FeedItem); describe('requestedColSpan', () => { - it('returns 1 for items without postHighlight', () => { + it('returns 1 for items without hero', () => { expect(requestedColSpan(makePostItem(makePost()))).toBe(1); }); - it.each<[PostHighlightSignificance, number]>([ + it.each<[PostHeroSignificance, number]>([ ['breaking', 4], ['major', 3], ['notable', 2], diff --git a/packages/shared/src/lib/feedHighlightColSpan.ts b/packages/shared/src/lib/feedHighlightColSpan.ts index 1d9d8b4e2b..2b4be72459 100644 --- a/packages/shared/src/lib/feedHighlightColSpan.ts +++ b/packages/shared/src/lib/feedHighlightColSpan.ts @@ -1,13 +1,15 @@ import type { FeedItem } from '../hooks/useFeed'; import { FeedItemType } from '../components/cards/common/common'; import { PostType } from '../graphql/posts'; -import type { PostHighlightSignificance } from '../graphql/posts'; +import type { PostHeroSignificance } from '../graphql/posts'; -const SIGNIFICANCE_COL_SPAN: Record = { +const SIGNIFICANCE_COL_SPAN: Record = { breaking: 4, major: 3, notable: 2, routine: 1, + breakout: 4, + evergreen: 3, }; const WIDENABLE_POST_TYPES = new Set([ @@ -27,7 +29,7 @@ const LARGE_CARD_DENSITY = { maxLarge: 1, perItems: 10 } as const; * Returns the column span a feed item is asking for, before any clamping * for column count or fit-to-row. * - * Only Post items with an article-like type and an active `postHighlight` + * Only Post items with an article-like type and an active `hero` * request a wide colSpan. Ads, highlight strip items, placeholders, * marketing items and non-article post types always stay at 1. */ @@ -40,7 +42,7 @@ export const requestedColSpan = (item: FeedItem): number => { return 1; } - const significance = item.post.postHighlight?.significance; + const significance = item.post.hero?.significance; if (!significance) { return 1; }