diff --git a/ab-testing/config/abTests.ts b/ab-testing/config/abTests.ts index 906f8059bac..ec977fae768 100644 --- a/ab-testing/config/abTests.ts +++ b/ab-testing/config/abTests.ts @@ -133,6 +133,18 @@ const ABTests: ABTest[] = [ groups: ["control", "variant"], shouldForceMetricsCollection: true, }, + { + name: "webx-world-cup-2026-subnav", + description: + "Test of World Cup 2026 subnav on world cup related content", + owners: ["dotcom.platform@guardian.co.uk:"], + expirationDate: "2026-07-20", + type: "server", + status: "ON", + audienceSize: 0 / 100, + groups: ["enable"], + shouldForceMetricsCollection: false, + }, ]; const activeABtests = ABTests.filter((test) => test.status === "ON"); diff --git a/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx b/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx index 74e0f21aedf..e83bd43497c 100644 --- a/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx +++ b/dotcom-rendering/src/components/DirectoryPageNav.stories.tsx @@ -1,7 +1,18 @@ import { allModes } from '../../.storybook/modes'; import preview from '../../.storybook/preview'; +import { BetaABTests } from '../experiments/lib/beta-ab-tests'; +import { setBetaABTests } from '../lib/useAB'; +import { ConfigProvider } from './ConfigContext'; import { DirectoryPageNav } from './DirectoryPageNav'; +const mockAB = new BetaABTests({ + isServer: true, + serverSideABTests: { + 'webx-world-cup-2026-subnav': 'enable', + }, +}); +setBetaABTests(mockAB); + const meta = preview.meta({ component: DirectoryPageNav, title: 'Components/Directory Page Nav', @@ -22,6 +33,64 @@ export const WomensEuro2025 = meta.story({ }, }); +export const WorldCup2026 = meta.story({ + args: { + pageId: 'football/world-cup-2026', + }, +}); + +export const WorldCup2026MatchCenter = meta.story({ + args: { + pageId: 'football/world-cup-2026/overview', + }, +}); + +export const WorldCup2026ArticleWeb = meta.story({ + args: { + pageId: 'football/2026/may/19/brazils-world-cup-squad-offers-a-hint-of-the-magical-pragmatism-of-1994', + pageTags: [ + { + id: 'football/world-cup-2026', + type: 'Topic', + title: 'World Cup 2026', + }, + ], + }, +}); + +export const WorldCup2026ArticleApp = meta.story({ + render: (args) => ( + + + + ), + args: { + pageId: 'football/2026/may/19/brazils-world-cup-squad-offers-a-hint-of-the-magical-pragmatism-of-1994', + pageTags: [ + { + id: 'football/world-cup-2026', + type: 'Topic', + title: 'World Cup 2026', + }, + ], + }, + parameters: { + chromatic: { + modes: { + 'apps light': allModes['light'], + 'apps dark': allModes['dark'], + }, + }, + }, +}); + export const OtherCompetition = meta.story({ args: { pageId: 'football/premierleague/table', diff --git a/dotcom-rendering/src/components/DirectoryPageNav.tsx b/dotcom-rendering/src/components/DirectoryPageNav.tsx index 660a0297c7f..7de0e7e0cd7 100644 --- a/dotcom-rendering/src/components/DirectoryPageNav.tsx +++ b/dotcom-rendering/src/components/DirectoryPageNav.tsx @@ -3,40 +3,127 @@ import { type Breakpoint, breakpoints, from, - headlineBold15Object, - headlineBold17Object, headlineBold24Object, headlineBold42Object, - headlineMedium15Object, - headlineMedium17Object, palette, + space, + textSans14Object, + textSansBold14Object, } from '@guardian/source/foundations'; import { grid } from '../grid'; import { generateImageURL } from '../lib/image'; +import { useBetaAB } from '../lib/useAB'; +import { + WorldCup2026Icon, + WorldCup2026IconSmall, + worldCup2026PageIds, +} from '../lib/worldCup2026'; +import { palette as themePalette } from '../palette'; +import type { RenderingTarget } from '../types/renderingTarget'; import type { TagType } from '../types/tag'; +import { useConfig } from './ConfigContext'; type Props = { pageId: string; pageTags?: TagType[]; }; +type Color = + | string + | { + web: string; + app: string; + }; + interface DirectoryPageNavConfig { pageIds: string[]; tagIds: string[]; - textColor: string; - backgroundColor: string; + textColor: Color; + textHoverColor?: Color; + backgroundColor: Color; + titleIcon?: React.ReactElement; title: { label: string; id: string }; links: Array<{ label: string; id: string }>; + slimNav?: boolean; backgroundImages?: { mobile: string; mobileLandscape: string; phablet: string; tablet: string; desktop: string; + wide: string; }; } +const worldCup2026Links = [ + { + label: 'Match centre', + id: 'football/world-cup-2026/overview', + }, + { + label: 'Player guide', + id: '', + }, + { + label: 'Bracketology', + id: '', + }, + { + label: 'Golden boot', + id: '', + }, + { + label: 'More football', + id: 'football', + }, +]; + const configs = [ + // World Cup 2026 Fronts + { + pageIds: worldCup2026PageIds, + tagIds: [], + textColor: palette.neutral[100], + backgroundColor: palette.brand[400], + title: { + label: 'World Cup', + id: 'football/world-cup-2026', + }, + titleIcon: , + links: worldCup2026Links, + backgroundImages: { + mobile: 'https://media.guim.co.uk/4ba0caac6d18c1fe6a5a3267b270d8c21ae6f940/0_0_750_376/750.jpg', + mobileLandscape: + 'https://media.guim.co.uk/8e1356cc926c6bbfcdb3da5908252ba0b4cbd3bb/0_0_960_376/960.jpg', + phablet: + 'https://media.guim.co.uk/ed4fe540c6a114db35c1f73fc41ee802c3fea7d3/0_0_1320_282/1320.jpg', + tablet: 'https://media.guim.co.uk/861646115875f3f246313036f754b2f5f1480b1a/0_0_1480_276/1480.jpg', + desktop: + 'https://media.guim.co.uk/167bec4a208bfc7fdc6b2127186b9bb183932259/0_0_1960_276/1960.jpg', + wide: 'https://media.guim.co.uk/4e44f9a88fcc9a3b1b5294f7e581644baa75c904/0_0_2600_276/2600.jpg', + }, + }, + // World Cup 2026 Articles + { + pageIds: [] as string[], + tagIds: ['football/world-cup-2026'], + textColor: { + web: themePalette('--masthead-nav-link-text'), + app: themePalette('--article-text'), + }, + textHoverColor: themePalette('--masthead-nav-link-text-hover'), + backgroundColor: { + web: themePalette('--masthead-nav-background'), + app: themePalette('--article-background'), + }, + title: { + label: 'World Cup 2026', + id: 'football/world-cup-2026', + }, + slimNav: true, + titleIcon: , + links: worldCup2026Links, + }, // Winter Olympics 2026 { pageIds: [ @@ -79,6 +166,7 @@ const configs = [ tablet: 'https://uploads.guim.co.uk/2026/02/03/winter-olympics-740px-thin.jpg', desktop: 'https://uploads.guim.co.uk/2026/02/03/winter-olympics-980px.jpg', + wide: 'https://uploads.guim.co.uk/2026/02/03/winter-olympics-980px.jpg', }, }, // Winter Paralympics 2026 @@ -118,11 +206,27 @@ const configs = [ tablet: 'https://uploads.guim.co.uk/2026/03/03/winter-paralympics-740px-thin.jpg', desktop: 'https://uploads.guim.co.uk/2026/03/03/winter-paralympics-980px.jpg', + wide: 'https://uploads.guim.co.uk/2026/03/03/winter-paralympics-980px.jpg', }, }, ] satisfies DirectoryPageNavConfig[]; +const getColour = (color: Color, renderingTarget: RenderingTarget): string => { + if (typeof color === 'string') { + return color; + } + + return renderingTarget === 'Web' ? color.web : color.app; +}; + export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { + const { renderingTarget } = useConfig(); + + const isWeb = renderingTarget === 'Web'; + const isApps = renderingTarget === 'Apps'; + + const ab = useBetaAB(); + const config = configs.find( (cfg) => cfg.pageIds.includes(pageId) || @@ -135,20 +239,54 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { return null; } - const { textColor, backgroundColor } = config; + if ( + config.title.id === 'football/world-cup-2026' && + ab?.isUserInTest('webx-world-cup-2026-subnav') !== true + ) { + return null; + } + + const { + textColor: configTextColor, + backgroundColor: configBackgroundColour, + slimNav, + } = config; + + const backgroundColor = getColour(configBackgroundColour, renderingTarget); + + const textColor = getColour(configTextColor, renderingTarget); + + const container = css({ + backgroundColor: slimNav ? backgroundColor : 'transparent', + }); const nav = css({ backgroundColor, '&': css(grid.paddedContainer), alignContent: 'space-between', + position: 'relative', + }); + + const navWeb = css({ + '&': css( + grid.paddedContainer, + grid.verticalRules({ + color: themePalette('--masthead-nav-lines'), + }), + ), }); const largeLinkStyles = css({ + position: 'absolute', + top: space[3], + left: 0, ...headlineBold24Object, color: textColor, textDecoration: 'none', '&': css(grid.column.centre), gridRow: 1, + display: 'flex', + alignItems: 'flex-start', [from.tablet]: headlineBold42Object, [from.leftCol]: css( grid.between('left-column-start', 'right-column-end'), @@ -156,38 +294,38 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { }); const list = css({ - display: 'flex', - flexWrap: 'wrap', '&': css(grid.column.all), - gridRow: 2, - alignSelf: 'end', + display: 'flex', + alignItems: 'center', position: 'relative', - '--top-border-gap': '1.55rem', + overflowX: 'scroll', + scrollbarWidth: 'none', + borderTop: slimNav ? undefined : '1px solid', + borderBottom: '1px solid', + borderColor: isWeb + ? themePalette('--masthead-nav-lines') + : themePalette('--article-border'), + padding: `0 ${space[3]}px`, + height: space[10], [from.mobileLandscape]: { - paddingLeft: 10, + padding: `0 ${space[5]}px`, + height: slimNav ? space[10] : space[12], }, - [from.tablet]: { - '--top-border-gap': '3rem', + // This creates a gradient fade on the right side to indicate that there's more to scroll for. + '&:after': { + content: '""', + position: 'sticky', + right: -space[3], + top: 0, + height: '100%', + minWidth: 40, + background: `linear-gradient(to left, ${backgroundColor}, transparent)`, + [from.mobileLandscape]: { + right: `-${space[5]}px`, + }, }, - backgroundImage: ` - linear-gradient( - ${textColor} 0, - ${textColor} 1px, - transparent 1px, - transparent var(--top-border-gap), - ${textColor} var(--top-border-gap), - ${textColor} calc(var(--top-border-gap) + 1px), - transparent 1px, - transparent var(--top-border-gap) - ) - `, }); - const selectedStyles = { - '--selected-height': '4px', - '--selected-opacity': '1', - }; - const listItem = css({ position: 'relative', '&::before': { @@ -202,81 +340,128 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { backgroundColor: textColor, transition: 'height 0.3s ease-in-out, opacity 0.05s 0.1s linear', }, - '&:hover': selectedStyles, - [from.leftCol]: { - flexBasis: 160, - }, }); - const smallLink = css({ - ...headlineBold15Object, - padding: '4px 10px 6px', - display: 'block', - lineHeight: 1, - color: textColor, - textDecoration: 'none', + const primaryLinkStyles = css({ + display: 'flex', + alignItems: 'center', + paddingRight: space[6], + svg: { + marginRight: space[2], + }, + // small right border '&::after': { content: '""', display: 'block', position: 'absolute', - top: 0, - right: 0, + right: space[3], + top: '50%', + transform: 'translateY(-50%)', width: 1, - height: '1.3rem', - backgroundColor: textColor, + height: space[3], + backgroundColor: themePalette('--masthead-nav-lines'), }, - [from.tablet]: headlineBold17Object, - [from.leftCol]: { - padding: '9px 10px 10px', + }); + + const primaryLinkHoverStylesWeb = css({ + '&:not(:hover)': { + color: palette.sport[600], + 'svg rect, svg circle': { + fill: palette.sport[600], + }, + }, + [from.desktop]: { + '&:hover': { + textDecoration: 'underline', + color: themePalette('--masthead-nav-link-text-hover'), + }, + }, + }); + + const primaryLinkHoverStylesApp = css({ + '&:not(:hover)': { + color: palette.sport[400], + 'svg rect, svg circle': { + fill: palette.sport[400], + }, }, }); - const lastSmallLink = css(smallLink, { - ...headlineMedium15Object, + const smallLink = css({ + ...textSans14Object, + paddingRight: space[3], + display: 'block', lineHeight: 1, - [from.tablet]: headlineMedium17Object, + color: textColor, + textDecoration: 'none', + whiteSpace: 'nowrap', + }); + + const smallLinkWeb = css({ + '&:hover': { + textDecoration: 'underline', + color: config.textHoverColor, + 'svg rect, svg circle': { + fill: config.textHoverColor, + }, + }, + }); + + const boldSmallLink = css({ + ...textSansBold14Object, }); return ( - + + + )} + + + + ); }; -const heightStyles = css({ - height: 116, - [from.tablet]: { - height: 140, - }, - [from.desktop]: { - height: 150, - }, -}); - const BackgroundImage = (props: { images: DirectoryPageNavConfig['backgroundImages']; }) => { @@ -291,7 +476,6 @@ const BackgroundImage = (props: { '&': css(grid.column.all), gridRow: '1/3', }, - heightStyles, ]} > @@ -311,7 +495,8 @@ const BackgroundImage = (props: { alt="Winter Olympics background graphic" css={{ width: '100%', - height: '100%', + height: 'auto', + display: 'block', objectFit: 'cover', objectPosition: 'top', }} @@ -352,8 +537,9 @@ const breakpointToImageSize = (breakpoint: Breakpoint): ImageSize => { return 'tablet'; case 'desktop': case 'leftCol': - case 'wide': return 'desktop'; + case 'wide': + return 'wide'; } }; diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index ad089db43e1..f60ae0ffc5d 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -89,6 +89,7 @@ const paddedContainer = ` type VerticalRuleOptions = { centre?: boolean; + color?: string; }; /** @@ -121,7 +122,7 @@ const verticalRules = (options: VerticalRuleOptions = {}): string => ` top: 0; bottom: 0; width: 1px; - background-color: ${palette('--article-border')}; + background-color: ${options.color ?? palette('--article-border')}; content: ''; } diff --git a/dotcom-rendering/src/layouts/AudioLayout.tsx b/dotcom-rendering/src/layouts/AudioLayout.tsx index d3b68d8c2ba..c08637668a7 100644 --- a/dotcom-rendering/src/layouts/AudioLayout.tsx +++ b/dotcom-rendering/src/layouts/AudioLayout.tsx @@ -20,6 +20,7 @@ import { ArticleTitle } from '../components/ArticleTitle'; import { AudioPlayerWrapper } from '../components/AudioPlayerWrapper.island'; import { Border } from '../components/Border'; import { Carousel } from '../components/Carousel.island'; +import { DirectoryPageNav } from '../components/DirectoryPageNav'; import { DiscussionLayout } from '../components/DiscussionLayout'; import { Footer } from '../components/Footer'; import { GridItem } from '../components/GridItem'; @@ -45,6 +46,8 @@ import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideStoryPackageTrails } from '../lib/decideTrail'; import { parse } from '../lib/slot-machine-flags'; +import { useBetaAB } from '../lib/useAB'; +import { worldCupTagId } from '../lib/worldCup2026'; import type { NavType } from '../model/extract-nav'; import { palette as themePalette } from '../palette'; import type { ArticleDeprecated } from '../types/article'; @@ -164,6 +167,12 @@ export const AudioLayout = (props: WebProps | AppProps) => { const isLabs = format.theme === ArticleSpecial.Labs; + const ab = useBetaAB(); + + const isWorldCup2026 = + article.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const renderAds = canRenderAds(article); return ( @@ -191,7 +200,7 @@ export const AudioLayout = (props: WebProps | AppProps) => { discussionApiUrl={article.config.discussionApiUrl} idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={!isLabs} + showSubNav={!isLabs && !isWorldCup2026} showSlimNav={false} hasPageSkinContentSelfConstrain={true} pageId={article.pageId} @@ -227,6 +236,11 @@ export const AudioLayout = (props: WebProps | AppProps) => { )} + +
{ const contributionsServiceUrl = getContributionsServiceUrl(article); + const ab = useBetaAB(); + + const isWorldCup2026 = + article.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const renderAds = canRenderAds(article); return ( @@ -325,7 +334,7 @@ export const CommentLayout = (props: WebProps | AppsProps) => { discussionApiUrl={article.config.discussionApiUrl} idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={true} + showSubNav={!isWorldCup2026} showSlimNav={false} hasPageSkin={false} hasPageSkinContentSelfConstrain={false} @@ -343,6 +352,10 @@ export const CommentLayout = (props: WebProps | AppsProps) => { )} +
{ const hasPageSkin = renderAds && hasPageSkinConfig; + const ab = useBetaAB(); + + const isWorldCup2026 = + worldCup2026PageIds.includes(pageId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const filteredCollections = front.pressedPage.collections.filter( (collection) => !isHighlights(collection), ); @@ -227,7 +235,7 @@ export const FrontLayout = ({ front, NAV }: Props) => { discussionApiUrl={front.config.discussionApiUrl} contributionsServiceUrl={contributionsServiceUrl} idApiUrl={front.config.idApiUrl} - showSubNav={!isPaidContent} + showSubNav={!isPaidContent && !isWorldCup2026} showSlimNav={false} hasPageSkin={hasPageSkin} hasPageSkinContentSelfConstrain={true} diff --git a/dotcom-rendering/src/layouts/GalleryLayout.tsx b/dotcom-rendering/src/layouts/GalleryLayout.tsx index b851b5aa5c9..c0715e21476 100644 --- a/dotcom-rendering/src/layouts/GalleryLayout.tsx +++ b/dotcom-rendering/src/layouts/GalleryLayout.tsx @@ -16,6 +16,7 @@ import { ArticleMetaApps } from '../components/ArticleMeta.apps'; import { ArticleMeta } from '../components/ArticleMeta.web'; import { ArticleTitle } from '../components/ArticleTitle'; import { Caption } from '../components/Caption'; +import { DirectoryPageNav } from '../components/DirectoryPageNav'; import { DiscussionLayout } from '../components/DiscussionLayout'; import { FetchMoreGalleriesData } from '../components/FetchMoreGalleriesData.island'; import { Footer } from '../components/Footer'; @@ -45,6 +46,8 @@ import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideMainMediaCaption } from '../lib/decide-caption'; import type { EditionId } from '../lib/edition'; +import { useBetaAB } from '../lib/useAB'; +import { worldCupTagId } from '../lib/worldCup2026'; import type { NavType } from '../model/extract-nav'; import { palette } from '../palette'; import type { ArticleDeprecated, Gallery } from '../types/article'; @@ -105,6 +108,12 @@ export const GalleryLayout = (props: WebProps | AppProps) => { const isLabs = format.theme === ArticleSpecial.Labs; + const ab = useBetaAB(); + + const isWorldCup2026 = + frontendData.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const renderAds = canRenderAds(frontendData); const showMerchandisingHigh = isWeb && renderAds && !isLabs; @@ -124,6 +133,7 @@ export const GalleryLayout = (props: WebProps | AppProps) => { contributionsServiceUrl={contributionsServiceUrl} pageId={frontendData.pageId} tagIds={frontendData.tags.map((tag) => tag.id)} + showSlimNav={!isWorldCup2026} /> ) : null} { )} +
(
{props.renderAds ? ( @@ -404,7 +419,7 @@ const BannerAndMasthead = (props: { idApiUrl={props.config.idApiUrl} contributionsServiceUrl={props.contributionsServiceUrl} showSubNav={false} - showSlimNav={true} + showSlimNav={props.showSlimNav ?? true} hasPageSkin={false} hasPageSkinContentSelfConstrain={false} pageId={props.pageId} diff --git a/dotcom-rendering/src/layouts/LiveLayout.tsx b/dotcom-rendering/src/layouts/LiveLayout.tsx index ff64da7bd6f..af659375f8d 100644 --- a/dotcom-rendering/src/layouts/LiveLayout.tsx +++ b/dotcom-rendering/src/layouts/LiveLayout.tsx @@ -20,6 +20,7 @@ import { ArticleMeta } from '../components/ArticleMeta.web'; import { ArticleTitle } from '../components/ArticleTitle'; import { Carousel } from '../components/Carousel.island'; import { DecideLines } from '../components/DecideLines'; +import { DirectoryPageNav } from '../components/DirectoryPageNav'; import { DiscussionLayout } from '../components/DiscussionLayout'; import { FilterKeyEventsToggle } from '../components/FilterKeyEventsToggle.island'; import { FootballMatchHeaderFallback } from '../components/FootballMatchHeader/FootballMatchHeaderFallback'; @@ -50,6 +51,8 @@ import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideStoryPackageTrails } from '../lib/decideTrail'; import { getZIndex } from '../lib/getZIndex'; +import { useBetaAB } from '../lib/useAB'; +import { worldCupTagId } from '../lib/worldCup2026'; import type { NavType } from '../model/extract-nav'; import { palette as themePalette } from '../palette'; import type { ArticleDeprecated } from '../types/article'; @@ -305,6 +308,12 @@ export const LiveLayout = (props: WebProps | AppsProps) => { const renderAds = canRenderAds(article); + const ab = useBetaAB(); + + const isWorldCup2026 = + article.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const isWeb = renderingTarget === 'Web'; const isApps = renderingTarget === 'Apps'; @@ -337,7 +346,7 @@ export const LiveLayout = (props: WebProps | AppsProps) => { discussionApiUrl={article.config.discussionApiUrl} idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={true} + showSubNav={!isWorldCup2026} showSlimNav={false} hasPageSkin={false} hasPageSkinContentSelfConstrain={false} @@ -354,6 +363,10 @@ export const LiveLayout = (props: WebProps | AppsProps) => { )}
+ {isWeb && renderAds && hasLiveBlogTopAd && (
{ const renderAds = canRenderAds(article); + const ab = useBetaAB(); + + const isWorldCup2026 = + article.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const avatarUrl = getSoleContributor(article.tags, article.byline) ?.bylineLargeImageUrl; @@ -303,7 +312,7 @@ export const PictureLayout = (props: WebProps | AppsProps) => { discussionApiUrl={article.config.discussionApiUrl} idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={true} + showSubNav={!isWorldCup2026} showSlimNav={false} hasPageSkin={false} hasPageSkinContentSelfConstrain={false} @@ -330,6 +339,10 @@ export const PictureLayout = (props: WebProps | AppsProps) => { )} +
{ const contributionsServiceUrl = getContributionsServiceUrl(article); + const ab = useBetaAB(); + + const isWorldCup2026 = + article.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const renderAds = canRenderAds(article); const isLabs = format.theme === ArticleSpecial.Labs; @@ -282,7 +290,7 @@ export const ShowcaseLayout = (props: WebProps | AppsProps) => { contributionsServiceUrl={ contributionsServiceUrl } - showSubNav={true} + showSubNav={!isWorldCup2026} showSlimNav={false} hasPageSkin={false} hasPageSkinContentSelfConstrain={false} diff --git a/dotcom-rendering/src/layouts/SportDataPageLayout.tsx b/dotcom-rendering/src/layouts/SportDataPageLayout.tsx index 90f7ddfbe06..3314b8b0783 100644 --- a/dotcom-rendering/src/layouts/SportDataPageLayout.tsx +++ b/dotcom-rendering/src/layouts/SportDataPageLayout.tsx @@ -14,6 +14,8 @@ import { StickyBottomBanner } from '../components/StickyBottomBanner.island'; import { SubNav } from '../components/SubNav.island'; import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; +import { useBetaAB } from '../lib/useAB'; +import { worldCup2026PageIds } from '../lib/worldCup2026'; import { palette as themePalette } from '../palette'; import type { AppSportDataPage, @@ -97,9 +99,14 @@ export const SportDataPageLayout = ( const isApps = props.renderingTarget === 'Apps'; const pageFooter = sportData.pageFooter; const renderAds = canRenderAds(sportData); + const ab = useBetaAB(); const contributionsServiceUrl = getContributionsServiceUrl(sportData); + const isWorldCup2026 = + worldCup2026PageIds.includes(sportData.config.pageId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + return ( <> {isWeb && ( @@ -126,7 +133,7 @@ export const SportDataPageLayout = ( discussionApiUrl={sportData.config.discussionApiUrl} idApiUrl={sportData.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={true} + showSubNav={!isWorldCup2026} showSlimNav={false} hasPageSkin={sportData.config.hasPageSkin} pageId={sportData.config.pageId} diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 65ebfbc5994..abb72147805 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -57,6 +57,8 @@ import { decideStoryPackageTrails } from '../lib/decideTrail'; import type { EditionId } from '../lib/edition'; import { safeParseURL } from '../lib/parse'; import { parse } from '../lib/slot-machine-flags'; +import { useBetaAB } from '../lib/useAB'; +import { worldCupTagId } from '../lib/worldCup2026'; import type { NavType } from '../model/extract-nav'; import { palette as themePalette } from '../palette'; import type { ArticleDeprecated } from '../types/article'; @@ -383,6 +385,12 @@ export const StandardLayout = (props: WebProps | AppProps) => { const isLabs = format.theme === ArticleSpecial.Labs; + const ab = useBetaAB(); + + const isWorldCup2026 = + article.tags.some((tag) => tag.id === worldCupTagId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + const renderAds = canRenderAds(article); return ( @@ -410,7 +418,7 @@ export const StandardLayout = (props: WebProps | AppProps) => { discussionApiUrl={article.config.discussionApiUrl} idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={!isLabs} + showSubNav={!isLabs && !isWorldCup2026} showSlimNav={false} hasPageSkinContentSelfConstrain={true} pageId={article.pageId} diff --git a/dotcom-rendering/src/layouts/TagPageLayout.tsx b/dotcom-rendering/src/layouts/TagPageLayout.tsx index cf16f5e16ea..10933b47239 100644 --- a/dotcom-rendering/src/layouts/TagPageLayout.tsx +++ b/dotcom-rendering/src/layouts/TagPageLayout.tsx @@ -25,6 +25,8 @@ import { getTagPageBannerAdPositions, getTagPageMobileAdPositions, } from '../lib/getTagPageAdPositions'; +import { useBetaAB } from '../lib/useAB'; +import { worldCup2026PageIds } from '../lib/worldCup2026'; import { enhanceTags } from '../model/enhanceTags'; import type { NavType } from '../model/extract-nav'; import type { TagPage as TagPageModel } from '../types/tagPage'; @@ -66,6 +68,12 @@ export const TagPageLayout = ({ tagPage, NAV }: Props) => { const isAccessibilityPage = tagPage.config.pageId === 'help/accessibility-help'; + const ab = useBetaAB(); + + const isWorldCup2026 = + worldCup2026PageIds.includes(pageId) && + ab?.isUserInTest('webx-world-cup-2026-subnav'); + return ( <>
@@ -91,7 +99,7 @@ export const TagPageLayout = ({ tagPage, NAV }: Props) => { discussionApiUrl={tagPage.config.discussionApiUrl} idApiUrl={tagPage.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} - showSubNav={true} + showSubNav={!isWorldCup2026} showSlimNav={false} hasPageSkin={hasPageSkin} pageId={pageId} diff --git a/dotcom-rendering/src/lib/worldCup2026.tsx b/dotcom-rendering/src/lib/worldCup2026.tsx new file mode 100644 index 00000000000..8a4befa4ae5 --- /dev/null +++ b/dotcom-rendering/src/lib/worldCup2026.tsx @@ -0,0 +1,50 @@ +export const worldCup2026PageIds = [ + 'football/world-cup-2026', + 'football/world-cup-2026/fixtures', + 'football/world-cup-2026/overview', +]; + +export const worldCupTagId = 'football/world-cup-2026'; + +export const WorldCup2026Icon = () => ( + + + + + + +); + +// Smaller version has slightly different proportions to better fit in the nav when the header isn't shown. +export const WorldCup2026IconSmall = () => ( + + + + + + +);