From f2d317efd7f2fcbc4697bc282574d8a816fe817f Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 11 Mar 2026 12:44:55 +0000 Subject: [PATCH 01/80] Type tidying --- dotcom-rendering/src/layouts/StandardLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 8d24f6d3b8f..68d5c58af2c 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -45,7 +45,7 @@ import { Standfirst } from '../components/Standfirst'; import { StickyBottomBanner } from '../components/StickyBottomBanner.island'; import { SubMeta } from '../components/SubMeta'; import { SubNav } from '../components/SubNav.island'; -import { grid } from '../grid'; +import { type ColumnPreset, grid } from '../grid'; import { ArticleDesign, ArticleDisplay, From d087f53827c4aeb73f08aed499096749816fd28d Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 11 Mar 2026 13:00:05 +0000 Subject: [PATCH 02/80] Move all column settings into layout config --- dotcom-rendering/src/layouts/StandardLayout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 68d5c58af2c..4e8dc66015c 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -45,7 +45,7 @@ import { Standfirst } from '../components/Standfirst'; import { StickyBottomBanner } from '../components/StickyBottomBanner.island'; import { SubMeta } from '../components/SubMeta'; import { SubNav } from '../components/SubNav.island'; -import { type ColumnPreset, grid } from '../grid'; +import { grid } from '../grid'; import { ArticleDesign, ArticleDisplay, @@ -69,6 +69,7 @@ import { type LayoutType, } from './lib/articleArrangements'; import { BannerWrapper, Stuck } from './lib/stickiness'; +import { Grid } from 'src/components/Masthead/Titlepiece/Grid'; const stretchLines = css` ${until.phablet} { From 744539d92ad1215238b8712193b976594a64d5ab Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 11 Mar 2026 14:39:49 +0000 Subject: [PATCH 03/80] Remove maxWidth wrapper divs --- dotcom-rendering/src/layouts/StandardLayout.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 4e8dc66015c..9c2fadb1e4a 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -10,7 +10,6 @@ import { Hide } from '@guardian/source/react-components'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import { AdPortals } from '../components/AdPortals.island'; import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; -import { AffiliateDisclaimer } from '../components/AffiliateDisclaimer'; import { AppsEpic } from '../components/AppsEpic.island'; import { AppsFooter } from '../components/AppsFooter.island'; import { ArticleBody } from '../components/ArticleBody'; @@ -69,7 +68,6 @@ import { type LayoutType, } from './lib/articleArrangements'; import { BannerWrapper, Stuck } from './lib/stickiness'; -import { Grid } from 'src/components/Masthead/Titlepiece/Grid'; const stretchLines = css` ${until.phablet} { From 6eb25a82d549b624dcfba636ef8f1b7a7999ea2a Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 19 May 2026 15:25:40 +0100 Subject: [PATCH 04/80] Rough first pass --- .../src/layouts/StandardLayout.tsx | 832 ++++++++++-------- .../src/layouts/lib/articleArrangements.ts | 30 +- 2 files changed, 470 insertions(+), 392 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 9c2fadb1e4a..0c24acb106d 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -10,6 +10,7 @@ import { Hide } from '@guardian/source/react-components'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import { AdPortals } from '../components/AdPortals.island'; import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; +import { AffiliateDisclaimer } from '../components/AffiliateDisclaimer'; import { AppsEpic } from '../components/AppsEpic.island'; import { AppsFooter } from '../components/AppsFooter.island'; import { ArticleBody } from '../components/ArticleBody'; @@ -19,7 +20,6 @@ import { ArticleMetaApps } from '../components/ArticleMeta.apps'; import { ArticleMeta } from '../components/ArticleMeta.web'; import { ArticleTitle } from '../components/ArticleTitle'; import { Carousel } from '../components/Carousel.island'; -import { CricketMatchHeaderWrapper } from '../components/CricketMatchHeaderWrapper.island'; import { DecideLines } from '../components/DecideLines'; import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland'; import { DiscussionLayout } from '../components/DiscussionLayout'; @@ -33,7 +33,6 @@ import { LabsHeader } from '../components/LabsHeader'; import { ListenToArticle } from '../components/ListenToArticle.island'; import { MainMedia } from '../components/MainMedia'; import { Masthead } from '../components/Masthead/Masthead'; -import { MatchHeaderFallback } from '../components/MatchHeaderFallback'; import { MostViewedFooterData } from '../components/MostViewedFooterData.island'; import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; import { MostViewedRightWithAd } from '../components/MostViewedRightWithAd.island'; @@ -45,6 +44,7 @@ import { StickyBottomBanner } from '../components/StickyBottomBanner.island'; import { SubMeta } from '../components/SubMeta'; import { SubNav } from '../components/SubNav.island'; import { grid } from '../grid'; +import { getAgeWarning } from '../lib/age-warning'; import { ArticleDesign, ArticleDisplay, @@ -54,9 +54,9 @@ import { import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideStoryPackageTrails } from '../lib/decideTrail'; +import type { EditionId } from '../lib/edition'; import { safeParseURL } from '../lib/parse'; import { parse } from '../lib/slot-machine-flags'; -import { useAB } from '../lib/useAB'; import { worldCupTagId } from '../lib/worldCup2026'; import type { NavType } from '../model/extract-nav'; import { palette as themePalette } from '../palette'; @@ -64,6 +64,7 @@ import type { ArticleDeprecated } from '../types/article'; import type { RenderingTarget } from '../types/renderingTarget'; import { type Area, + getLayoutType, gridItemCss, type LayoutType, } from './lib/articleArrangements'; @@ -120,6 +121,23 @@ interface AppProps extends Props { renderingTarget: 'Apps'; } +/** + * Works out the orientation of an image from its Guardian media URL, which + * encodes the crop dimensions in the path (e.g. `/1000_600_800_480/`). + * Falls back to 'landscape' if the URL doesn't match the expected pattern. + */ +const getImageOrientation = ( + url: string, +): 'portrait' | 'landscape' | 'square' => { + const match = url.match(/\/\d+_\d+_(\d+)_(\d+)\/\d+\.\w+$/); + if (!match) return 'landscape'; + const [, width, height] = match.map(Number); + if (width == null || height == null) return 'landscape'; + if (height > width) return 'portrait'; + if (width > height) return 'landscape'; + return 'square'; +}; + export const StandardLayout = (props: WebProps | AppProps) => { const { article, format, renderingTarget, serverTime } = props; const { @@ -149,25 +167,26 @@ export const StandardLayout = (props: WebProps | AppProps) => { ? article.matchStatsUrl : undefined; - const isFootballMatchReport = - format.design === ArticleDesign.MatchReport && !!footballMatchUrl; - - const cricketMatchUrl = - article.matchType === 'CricketMatchType' - ? article.matchStatsUrl + const footballMatchHeaderUrl = + article.matchType === 'FootballMatchType' + ? article.matchHeaderUrl : undefined; - const isCricketMatchReport = - format.design === ArticleDesign.MatchReport && !!cricketMatchUrl; + const footballMatchLeagueName = article.sectionLabel; + const footballMatchLeagueUrl = `${article.guardianBaseURL}/${article.sectionUrl}`; + + const isMatchReport = + format.design === ArticleDesign.MatchReport && !!footballMatchUrl; const isMedia = format.design === ArticleDesign.Video || format.design === ArticleDesign.Audio; + const isShowcase = format.display === ArticleDisplay.Showcase; + const isImmersive = format.display === ArticleDisplay.Immersive; + const isFeature = format.design === ArticleDesign.Feature; const isVideo = format.design === ArticleDesign.Video; - const isShowcase = format.display === ArticleDisplay.Showcase; - const showComments = article.isCommentable && !isPaidContent; const { branding } = article.commercialProperties[article.editionId]; @@ -180,11 +199,36 @@ export const StandardLayout = (props: WebProps | AppProps) => { const renderAds = canRenderAds(article); - const layoutType: LayoutType = isMedia - ? 'media' - : isShowcase - ? 'showcase' - : 'standard'; + const firstMainMediaElement = article.mainMediaElements[0]; + const mainMediaUrl: string | undefined = + firstMainMediaElement?._type === + 'model.dotcomrendering.pageElements.ImageBlockElement' + ? firstMainMediaElement.media.allImages[0]?.url + : undefined; + + const mainMediaOrientation = + mainMediaUrl != null ? getImageOrientation(mainMediaUrl) : 'landscape'; + + const layoutType = getLayoutType({ + isImmersive, + isFeature, + orientation: mainMediaOrientation, + isVideo, + isShowcase, + }); + const contentLayoutName = `${ArticleDisplay[format.display]}Layout`; + + const isImmersivePortrait = + layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature'; + const isImmersiveLandscape = + layoutType === 'immersiveLandscapeDefault' || + layoutType === 'immersiveLandscapeFeature'; + + const ageWarning = getAgeWarning( + article.tags, + article.webPublicationDateDeprecated, + ); return ( <> @@ -242,18 +286,19 @@ export const StandardLayout = (props: WebProps | AppProps) => { /> {isWeb && renderAds && hasSurveyAd && ( )} -
+
{isApps && renderAds && ( @@ -261,84 +306,146 @@ export const StandardLayout = (props: WebProps | AppProps) => { )} {/* GridItem order matters — mobile layout relies on DOM order for grid placement. - See furnitureArrangements.ts if reordering. */} - {/* This element is used to replace the article with the scorecard when the scorecard tab is clicked */} -
-
- - - - - - - - - - - - - + `, + ]} + > + + + + + + + + + + + + + + {layoutType !== 'immersivePortraitDefault' && (
{isWeb && format.theme === ArticleSpecial.Labs && @@ -351,70 +458,30 @@ export const StandardLayout = (props: WebProps | AppProps) => { /> )}
- {isApps ? ( - <> - - - - - - {!!article.affiliateLinksDisclaimer && ( - - )} - - - ) : ( - <> + )} + {isApps ? ( + <> + + + + { secondaryDateline={ article.webPublicationSecondaryDateDisplay } + webPublicationDate={ + article.webPublicationDate + } isCommentable={article.isCommentable} discussionApiUrl={ article.config.discussionApiUrl @@ -437,190 +507,211 @@ export const StandardLayout = (props: WebProps | AppProps) => { mainMediaElements={ article.mainMediaElements } - webPublicationDate={ - article.webPublicationDate - } /> {!!article.affiliateLinksDisclaimer && ( )} - - )} -
- - {/* Only show Listen to Article button on App landscape views */} - {isApps && ( - - {!isVideo && ( -
- - - -
- )}
- )} - - + ) : ( + <> + - - - {isApps && ( - - - + {!!article.affiliateLinksDisclaimer && ( + )} - - {showBodyEndSlot && ( - + )} +
+ + {/* Only show Listen to Article button on App landscape views */} + {isApps && ( + + {!isVideo && ( +
- - + + + +
)} - - - -
- + )} + + - + tags={article.tags} + isPaidContent={!!article.config.isPaidContent} + contributionsServiceUrl={ + contributionsServiceUrl + } + contentType={article.contentType} + isPreview={article.config.isPreview} + idUrl={article.config.idUrl ?? ''} + isDev={!!article.config.isDev} + keywordIds={article.config.keywordIds} + abTests={article.config.abTests} + tableOfContents={article.tableOfContents} + lang={article.lang} + isRightToLeftLang={article.isRightToLeftLang} + editionId={article.editionId} + shouldHideAds={article.shouldHideAds} + idApiUrl={article.config.idApiUrl} + /> + + + {isApps && ( + + + + )} + + {showBodyEndSlot && ( - - - -
-
+ )} + + + + + + + + + + + + + {isWeb && renderAds && !isLabs && (
{ /> - {renderAds && ( - - )} + )} @@ -851,82 +940,43 @@ export const StandardLayout = (props: WebProps | AppProps) => { }; const MatchHeaderContainer = ({ - isFootballMatchReport, - isCricketMatchReport, + isMatchReport, + footballMatchHeaderUrl, + leagueName, + leagueUrl, + editionId, renderingTarget, - article, - format, }: { - isFootballMatchReport: boolean; - isCricketMatchReport: boolean; + isMatchReport: boolean; + footballMatchHeaderUrl: string | undefined; + leagueName: string; + leagueUrl: string; + editionId: EditionId; renderingTarget: RenderingTarget; - article: ArticleDeprecated; - format: ArticleFormat; }) => { - const footballMatchHeaderUrl = - article.matchType === 'FootballMatchType' - ? article.matchHeaderUrl - : undefined; - - const footballMatchLeagueName = article.sectionLabel; - const footballMatchLeagueUrl = `${article.guardianBaseURL}/${article.sectionUrl}`; - - const cricketMatchHeaderUrl = - article.matchType === 'CricketMatchType' - ? article.matchHeaderUrl - : undefined; - - const ab = useAB(); - const isCricketRedesignEnabled = Boolean( - ab?.isUserInTestGroup('webx-cricket-redesign', 'enable'), - ); - - const isApps = renderingTarget === 'Apps'; - - if (isFootballMatchReport && footballMatchHeaderUrl) { - return ( - <> - - - - - - ); - } + if (isMatchReport && !!footballMatchHeaderUrl) { + const parsedUrl = safeParseURL(footballMatchHeaderUrl); + if (!parsedUrl.ok) { + log( + 'dotcom', + new Error( + `Failed to parse match header URL: ${footballMatchHeaderUrl}`, + ), + ); - if ( - !isApps && - cricketMatchHeaderUrl && - isCricketMatchReport && - isCricketRedesignEnabled - ) { + return null; + } return ( - <> - - - - - + + + ); } diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 2cf5fb6843f..00c81dc3efb 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -2,7 +2,7 @@ import { css, type SerializedStyles } from '@emotion/react'; import { from, until } from '@guardian/source/foundations'; import { grid } from '../../grid'; -export type LayoutType = 'standard' | 'showcase' | 'media'; +export type LayoutType = 'standard' | 'showcase' | 'media' | 'portrait'; export type Area = | 'title' @@ -149,10 +149,38 @@ const mediaCss: LayoutCssMap = { }, }; +const portraitCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + leftCol: 'grid-row: 1; grid-column: left-column-start / 9;', + }, + headline: { + mobile: 'grid-row: 2;', + leftCol: 'grid-row: 2; grid-column: left-column-start / 9;', + }, + media: { + mobile: 'grid-row: 3;', + leftCol: 'grid-row: 1 / span 3; grid-column: 10 / right-column-end;', + }, + standfirst: { + mobile: 'grid-row: 4;', + leftCol: 'grid-row: 3; grid-column: centre-column-start / 9;', + }, + meta: { + mobile: 'grid-row: 5;', + leftCol: + 'grid-row: 3; grid-column: left-column-start / left-column-end;', + }, + body: { + mobile: 'grid-row: 6;', + }, +}; + const layoutCssMaps: Record = { standard: standardCss, showcase: showcaseCss, media: mediaCss, + portrait: portraitCss, }; /** From 211c9c2af784c307bcd298964d8ce2312b4f0b2f Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 21 May 2026 16:23:11 +0100 Subject: [PATCH 05/80] Refactor grid centre rule, more breakpoints --- .../src/layouts/StandardLayout.tsx | 5 ++++ .../src/layouts/lib/articleArrangements.ts | 25 ++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 0c24acb106d..bf1083cb0d6 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -317,6 +317,7 @@ export const StandardLayout = (props: WebProps | AppProps) => { grid.container, grid.outerRules(), !isLabs && + layoutType !== 'portrait' && css` ${from.leftCol} { ${grid.centreRule( @@ -340,6 +341,10 @@ export const StandardLayout = (props: WebProps | AppProps) => { : '90px'}; } `, + layoutType === 'portrait' && + css` + grid-template-rows: 0.25fr 1fr auto; + `, ]} > = { mobile: until.tablet, tablet: from.tablet, desktop: from.desktop, leftCol: from.leftCol, + wide: from.wide, }; // Raw CSS overrides per area per breakpoint. Entries are only needed when an area @@ -152,28 +153,46 @@ const mediaCss: LayoutCssMap = { const portraitCss: LayoutCssMap = { title: { mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: 'grid-row: 1; grid-column: centre-column-start / 8;', leftCol: 'grid-row: 1; grid-column: left-column-start / 9;', }, headline: { mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: 'grid-row: 2; grid-column: centre-column-start / 8;', leftCol: 'grid-row: 2; grid-column: left-column-start / 9;', + wide: 'grid-row: 2; grid-column: left-column-start / 10;', }, media: { mobile: 'grid-row: 3;', - leftCol: 'grid-row: 1 / span 3; grid-column: 10 / right-column-end;', + tablet: 'grid-row: 3;', + desktop: 'grid-row: 1 / span 4; grid-column: 8 / right-column-end;', + leftCol: 'grid-row: 1 / span 3; grid-column: 9 / right-column-end;', + wide: 'grid-row: 1 / span 3; grid-column: 10 / right-column-end;', }, standfirst: { mobile: 'grid-row: 4;', - leftCol: 'grid-row: 3; grid-column: centre-column-start / 9;', + tablet: 'grid-row: 4;', + desktop: 'grid-row: 3; grid-column: centre-column-start / 7;', + leftCol: 'grid-row: 3; grid-column: centre-column-start / 8;', }, meta: { mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: 'grid-row: 4; grid-column: centre-column-start / 8;', leftCol: 'grid-row: 3; grid-column: left-column-start / left-column-end;', }, body: { mobile: 'grid-row: 6;', }, + 'right-column': { + desktop: + 'grid-row: 5; grid-column: right-column-start / right-column-end;', + leftCol: + 'grid-row: 4; grid-column: right-column-start / right-column-end;', + }, }; const layoutCssMaps: Record = { From 6471809c8891350b8831264b76cd3073074b1c80 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 26 May 2026 16:50:39 +0100 Subject: [PATCH 06/80] Rough implementation of portrait/landscape variants --- .../src/components/ArticleHeadline.tsx | 151 +-- .../src/components/ArticleTitle.tsx | 10 +- .../src/components/Standfirst.tsx | 1 - dotcom-rendering/src/layouts/DecideLayout.tsx | 5 +- .../src/layouts/ImmersiveLayout.tsx | 1024 ----------------- .../src/layouts/StandardLayout.tsx | 5 - .../src/layouts/lib/articleArrangements.ts | 53 +- dotcom-rendering/src/paletteDeclarations.ts | 15 +- 8 files changed, 112 insertions(+), 1152 deletions(-) delete mode 100644 dotcom-rendering/src/layouts/ImmersiveLayout.tsx diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 3b0c141fc86..8beda81e2eb 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -27,7 +27,6 @@ import { ArticleSpecial, Pillar, } from '../lib/articleFormat'; -import { getZIndex } from '../lib/getZIndex'; import { palette as themePalette } from '../palette'; import type { StarRating as Rating } from '../types/content'; import type { TagType } from '../types/tag'; @@ -45,6 +44,7 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; + isInverted?: boolean; }; const topPadding = css` @@ -214,30 +214,20 @@ const invertedStyles = css` box-decoration-break: clone; `; -const immersiveStyles = css` - min-height: 112px; - padding-bottom: ${space[6]}px; - padding-left: ${space[1]}px; - - ${from.mobileLandscape} { - padding-left: ${space[3]}px; - } - - ${from.tablet} { - padding-left: ${space[1]}px; - } - - margin-right: ${space[5]}px; -`; - const darkBackground = css` background-color: ${themePalette('--headline-background')}; `; const invertedText = css` - white-space: pre-wrap; - padding-bottom: ${space[1]}px; - padding-right: ${space[1]}px; + ${from.desktop} { + color: white; + background-color: black; + white-space: pre-wrap; + padding-bottom: ${space[1]}px; + padding-right: ${space[1]}px; + margin-left: -10px; + padding-left: 10px; + } `; const maxWidth = css` @@ -255,35 +245,6 @@ const invertedWrapper = css` margin-left: 6px; `; -const immersiveWrapper = css` - /* - Make sure we vertically align the headline font with the body font - */ - margin-left: 6px; - - ${from.tablet} { - margin-left: 16px; - } - - ${from.leftCol} { - margin-left: 25px; - } - - /* - We need this grow to ensure the headline fills the main content column - */ - flex-grow: 1; - /* - This z-index is what ensures the headline text shows above the pseudo black - box that extends the black background to the right - */ - z-index: ${getZIndex('articleHeadline')}; - - ${until.mobileLandscape} { - margin-right: 40px; - } -`; - // Due to MainMedia using position: relative, this seems to effect the rendering order // To mitigate we use z-index // TODO: find a cleaner solution @@ -292,59 +253,58 @@ const zIndex = css` `; const ageWarningMargins = (format: ArticleFormat) => { - if (format.design === ArticleDesign.Gallery) { + if ( + format.design === ArticleDesign.Gallery || + format.display === ArticleDisplay.Immersive + ) { return ''; } - return format.display === ArticleDisplay.Immersive - ? css` - margin-left: 0px; - margin-bottom: 0px; - - ${from.tablet} { - margin-left: 10px; - } + return css` + margin-top: 12px; + margin-left: -10px; + margin-bottom: 6px; - ${from.leftCol} { - margin-left: 20px; - } - ` - : css` - margin-top: 12px; - margin-left: -10px; - margin-bottom: 6px; - - ${from.tablet} { - margin-left: -20px; - } + ${from.tablet} { + margin-left: -20px; + } - ${from.leftCol} { - margin-left: -10px; - margin-top: 0; - } - `; + ${from.leftCol} { + margin-left: -10px; + margin-top: 0; + } + `; }; -const backgroundStyles = css` - background-color: ${themePalette('--age-warning-wrapper-background')}; -`; - const WithAgeWarning = ({ tags, webPublicationDateDeprecated, format, children, + snapToInverted = false, }: { tags: TagType[]; webPublicationDateDeprecated: string; format: ArticleFormat; children: React.ReactNode; + snapToInverted?: boolean; }) => { const age = getAgeWarning(tags, webPublicationDateDeprecated); if (age) { return ( <> -
+
{children} @@ -439,6 +399,7 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, + isInverted = false, }: Props) => { switch (format.display) { case ArticleDisplay.Immersive: { @@ -465,12 +426,13 @@ export const ArticleHeadline = ({ format.theme === ArticleSpecial.Labs ? labsFont : headlineFont(format), - invertedText, - css` - color: ${themePalette( - '--headline-colour', - )}; - `, + isInverted + ? [invertedText, darkBackground] + : css` + color: ${themePalette( + '--headline-colour', + )}; + `, ]} > {headlineString} @@ -496,16 +458,17 @@ export const ArticleHeadline = ({ webPublicationDateDeprecated } format={format} + snapToInverted={true} >

diff --git a/dotcom-rendering/src/components/ArticleTitle.tsx b/dotcom-rendering/src/components/ArticleTitle.tsx index de44e1eb319..2cf4d717f3a 100644 --- a/dotcom-rendering/src/components/ArticleTitle.tsx +++ b/dotcom-rendering/src/components/ArticleTitle.tsx @@ -28,17 +28,11 @@ const sectionStyles = css` `; const immersiveMargins = css` - max-width: 400px; + max-width: 500px; min-width: 200px; margin-bottom: 4px; - /* - Make sure we vertically align the title font with the body font - */ ${from.tablet} { - margin-left: 16px; - } - ${from.leftCol} { - margin-left: 25px; + margin-left: -4px; } `; diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index 0567d1dbc99..e95212f3302 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -268,7 +268,6 @@ const standfirstStyles = ({ display, design, theme }: ArticleFormat) => { `; default: return css` - max-width: 280px; ${from.tablet} { max-width: 460px; } diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index b6dd7f4a27c..3e1ccd79cb8 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -10,7 +10,6 @@ import { GalleryLayout } from './GalleryLayout'; import { HostedArticleLayout } from './HostedArticleLayout'; import { HostedGalleryLayout } from './HostedGalleryLayout'; import { HostedVideoLayout } from './HostedVideoLayout'; -import { ImmersiveLayout } from './ImmersiveLayout'; import { InteractiveLayout } from './InteractiveLayout'; import { LiveLayout } from './LiveLayout'; import { NewsletterSignupLayout } from './NewsletterSignupLayout'; @@ -57,7 +56,7 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { } default: { return ( - { } default: { return ( - ( -
- {children} -
-); - -const maxWidth = css` - ${from.desktop} { - max-width: 620px; - } -`; - -const linesMargin = css` - ${from.leftCol} { - margin-top: ${space[5]}px; - } -`; - -const stretchLines = css` - ${until.phablet} { - margin-left: -20px; - margin-right: -20px; - } - ${until.mobileLandscape} { - margin-left: -10px; - margin-right: -10px; - } -`; - -interface CommonProps { - article: ArticleDeprecated; - format: ArticleFormat; - serverTime?: number; -} - -interface WebProps extends CommonProps { - NAV: NavType; - renderingTarget: 'Web'; -} - -interface AppProps extends CommonProps { - renderingTarget: 'Apps'; -} - -const Box = ({ children }: { children: React.ReactNode }) => ( -
- {children} -
-); - -export const ImmersiveLayout = (props: WebProps | AppProps) => { - const { article, format, renderingTarget, serverTime } = props; - - const { - config: { isPaidContent, host, hasSurveyAd }, - editionId, - } = article; - const isWeb = renderingTarget === 'Web'; - const isApps = renderingTarget === 'Apps'; - - const showBodyEndSlot = - isWeb && - (parse(article.slotMachineFlags ?? '').showBodyEnd || - article.config.switches.slotBodyEnd); - - // TODO: - // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render - // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. - - const showComments = article.isCommentable && !isPaidContent; - - const mainMedia = article.mainMediaElements[0]; - - const captionText = decideMainMediaCaption(mainMedia); - - const HEADLINE_OFFSET = mainMedia ? 120 : 0; - - const { branding } = article.commercialProperties[article.editionId]; - - const contributionsServiceUrl = getContributionsServiceUrl(article); - - const isLabs = format.theme === ArticleSpecial.Labs; - - /** - We need change the height values depending on whether the labs header is there or not to keep - the headlines appearing at a consistent height between labs and non labs immersive articles. - */ - - const labsHeaderHeight = LABS_HEADER_HEIGHT; - const combinedHeight = (minHeaderHeightPx + labsHeaderHeight).toString(); - - const navAndLabsHeaderHeight = isLabs - ? `${combinedHeight}px` - : `${minHeaderHeightPx}px`; - - const hasMainMediaStyles = css` - height: calc(80vh - ${navAndLabsHeaderHeight}); - /** - 80vh is normally enough but don't let the content shrink vertically too - much just in case - */ - min-height: calc(25rem - ${navAndLabsHeaderHeight}); - ${from.desktop} { - height: calc(100vh - ${navAndLabsHeaderHeight}); - min-height: calc(31.25rem - ${navAndLabsHeaderHeight}); - } - ${from.wide} { - min-height: calc(50rem - ${navAndLabsHeaderHeight}); - } - `; - const LeftColCaption = () => ( -
- -
- ); - - const renderAds = canRenderAds(article); - - return ( - <> - {isWeb && ( - tag.id)} - sectionId={article.config.section} - contentType={article.contentType} - /> - )} - - - - {format.theme === ArticleSpecial.Labs && ( - -
- -
-
- )} - -
-
- -
- {mainMedia && ( - <> -
-
} - > - -
- -
- -
-
-
- - )} -
- - {isWeb && renderAds && hasSurveyAd && ( - - )} - -
- {isApps && renderAds && ( - - - - )} -
- - {/* Above leftCol, the Caption is controlled by Section ^^ */} - - - - - - - {format.design === ArticleDesign.PhotoEssay ? ( - <> - ) : ( - - )} - - - <> - {!mainMedia && ( -
- -
- )} - -
- - <> - {!mainMedia && ( -
- -
- )} - -
- - - - - {!!article.byline && ( - - )} - {/* Only show Listen to Article button on App landscape views */} - {isApps && ( - -
- - - -
-
- )} -
- - {format.design === ArticleDesign.PhotoEssay && - !isLabs ? ( - <> - ) : ( -
-
- {format.theme === - ArticleSpecial.Labs ? ( - - ) : ( - - )} -
-
- )} -
- {isApps ? ( - <> - - - - - - {!!article.affiliateLinksDisclaimer && ( - - )} - - - ) : ( - <> - - {!!article.affiliateLinksDisclaimer && ( - - )} - - )} -
-
- - - - {showBodyEndSlot && ( - - - - )} - - - - - -
- - <> - {mainMedia && isWeb && renderAds && ( -
- { - - } -
- )} - -
-
-
-
-
- {!isLabs && isWeb && renderAds && ( -
- -
- )} - - {article.storyPackage && ( -
- - - -
- )} - - - - - - {showComments && ( -
- -
- )} - {!isPaidContent && ( -
- - - - - -
- )} - {!isLabs && isWeb && renderAds && ( -
- -
- )} -
- - {isWeb && props.NAV.subNavSections && ( -
- - - -
- )} - - {isWeb && ( - <> -
-
-
- - - - - - - {renderAds && ( - - )} - - )} - {isApps && ( -
- - - -
- )} - - ); -}; diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index bf1083cb0d6..0c24acb106d 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -317,7 +317,6 @@ export const StandardLayout = (props: WebProps | AppProps) => { grid.container, grid.outerRules(), !isLabs && - layoutType !== 'portrait' && css` ${from.leftCol} { ${grid.centreRule( @@ -341,10 +340,6 @@ export const StandardLayout = (props: WebProps | AppProps) => { : '90px'}; } `, - layoutType === 'portrait' && - css` - grid-template-rows: 0.25fr 1fr auto; - `, ]} > = { standard: standardCss, showcase: showcaseCss, media: mediaCss, - portrait: portraitCss, + immersiveLandscape: immersiveLandscapeCss, + immersivePortrait: immersivePortraitCss, }; /** diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 22aee146b06..781d9d974ae 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -88,7 +88,7 @@ const textblockTextDark: PaletteFunction = () => 'inherit'; const headlineTextLight: PaletteFunction = ({ design, display, theme }) => { switch (display) { case ArticleDisplay.Immersive: - return sourcePalette.neutral[97]; + return sourcePalette.neutral[7]; default: { switch (design) { case ArticleDesign.Editorial: @@ -195,19 +195,8 @@ const headlineMatchTextLight: PaletteFunction = (format) => const headlineMatchTextDark: PaletteFunction = (format) => seriesTitleMatchTextDark(format); -const headlineBackgroundLight: PaletteFunction = ({ - display, - design, - theme, -}) => { +const headlineBackgroundLight: PaletteFunction = ({ display, design }) => { switch (display) { - case ArticleDisplay.Immersive: - switch (theme) { - case ArticleSpecial.SpecialReport: - return sourcePalette.specialReport[300]; - default: - return sourcePalette.neutral[7]; - } case ArticleDisplay.Showcase: case ArticleDisplay.NumberedList: case ArticleDisplay.Standard: From a87148c07a6cb987fcccc74b3ef7382233b36aaa Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 11 Jun 2026 15:41:01 +0100 Subject: [PATCH 07/80] Feature specific styling --- .../src/components/ArticleHeadline.tsx | 5 +- dotcom-rendering/src/components/Figure.tsx | 53 ++++++++++++++++++- dotcom-rendering/src/components/MainMedia.tsx | 1 + dotcom-rendering/src/grid.ts | 2 + 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 8beda81e2eb..96917e0fa74 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -44,7 +44,6 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; - isInverted?: boolean; }; const topPadding = css` @@ -399,8 +398,10 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, - isInverted = false, }: Props) => { + const isInverted = + format.design === ArticleDesign.Standard && + format.display === ArticleDisplay.Immersive; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index ccb113fb4b1..ab99567f7a6 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -1,6 +1,18 @@ import { css } from '@emotion/react'; -import { breakpoints, from, space, until } from '@guardian/source/foundations'; -import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; +import { + breakpoints, + from, + palette as sourcePalette, + space, + until, +} from '@guardian/source/foundations'; +import { + ArticleDesign, + ArticleDisplay, + type ArticleFormat, +} from '../lib/articleFormat'; +import { transparentColour } from '../lib/transparentColour'; +import { palette } from '../palette'; import type { FEElement, RoleType } from '../types/content'; type Props = { @@ -14,6 +26,32 @@ type Props = { isTimeline?: boolean; }; +const overlayMaskGradientStyles = (angle: string) => css` + mask-image: linear-gradient( + ${angle}, + rgb(0, 0, 0) 0%, + rgba(0, 0, 0, 0.9619) 12.5%, + rgba(0, 0, 0, 0.8536) 25%, + rgba(0, 0, 0, 0.6913) 37.5%, + rgba(0, 0, 0, 0.5) 50%, + rgba(0, 0, 0, 0.3087) 62.5%, + rgba(0, 0, 0, 0.1464) 75%, + rgba(0, 0, 0, 0.0381) 87.5%, + transparent 100% + ); +`; + +const blurStyles = css` + position: absolute; + inset: 0; + background-color: ${palette('--article-background')}; + backdrop-filter: blur(12px) brightness(0.5); + @supports not (backdrop-filter: blur(12px)) { + background-color: ${transparentColour(sourcePalette.neutral[10], 0.7)}; + } + ${overlayMaskGradientStyles('0deg')}; +`; + const roleCss = { inline: css` margin-top: ${space[3]}px; @@ -273,6 +311,17 @@ export const Figure = ({ return (
{children} + {format.display === ArticleDisplay.Immersive && ( +
+ )}
); } diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx index e912f480019..96869f41479 100644 --- a/dotcom-rendering/src/components/MainMedia.tsx +++ b/dotcom-rendering/src/components/MainMedia.tsx @@ -12,6 +12,7 @@ import type { ServerSideTests, Switches } from '../types/config'; import type { FEElement } from '../types/content'; const mainMedia = css` + position: relative; height: 100%; ${until.tablet} { diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index a02e5c3f861..5e7a0111455 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -159,6 +159,7 @@ const outerRules = (color?: string): string => ` &::before { grid-column: centre-column-start; transform: translateX(-${columnGap}); + z-index: 10; ${fromBreakpoint.leftCol} { grid-column: left-column-start; @@ -169,6 +170,7 @@ const outerRules = (color?: string): string => ` &::after { grid-column: right-column-end; transform: translateX(-1px); + z-index: 10; ${betweenBreakpoint.tablet.and.desktop} { grid-column: centre-column-end; From dd621369b3c01c74d85d92e3cd8368edcd48fd78 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 23 Jun 2026 15:43:13 +0100 Subject: [PATCH 08/80] Use grid module helpers for columns --- .../src/layouts/lib/articleArrangements.ts | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index b79d70fb720..0da7bcc5219 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -159,45 +159,42 @@ const immersivePortraitCss: LayoutCssMap = { title: { mobile: 'grid-row: 1;', tablet: 'grid-row: 1;', - desktop: 'grid-row: 1; grid-column: centre-column-start / 8;', - leftCol: 'grid-row: 1; grid-column: left-column-start / 9;', + desktop: `grid-row: 1; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 1; ${grid.between('left-column-start', 9)};`, }, headline: { mobile: 'grid-row: 2;', tablet: 'grid-row: 2;', - desktop: 'grid-row: 2; grid-column: centre-column-start / 8;', - leftCol: 'grid-row: 2; grid-column: left-column-start / 9;', - wide: 'grid-row: 2; grid-column: left-column-start / 10;', + desktop: `grid-row: 2; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 2; ${grid.between('left-column-start', 9)};`, + wide: `grid-row: 2; ${grid.between('left-column-start', 10)};`, }, media: { mobile: 'grid-row: 3;', tablet: 'grid-row: 3;', - desktop: 'grid-row: 1 / span 4; grid-column: 8 / right-column-end;', - leftCol: 'grid-row: 1 / span 3; grid-column: 9 / right-column-end;', - wide: 'grid-row: 1 / span 3; grid-column: 10 / right-column-end;', + desktop: `grid-row: 1 / span 4; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, + wide: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, }, standfirst: { mobile: 'grid-row: 4;', tablet: 'grid-row: 4;', - desktop: 'grid-row: 3; grid-column: centre-column-start / 7;', - leftCol: 'grid-row: 3; grid-column: centre-column-start / 8;', - wide: 'grid-row: 3; grid-column: centre-column-start / 9;', + desktop: `grid-row: 3; ${grid.between('centre-column-start', 7)};`, + leftCol: `grid-row: 3; ${grid.between('centre-column-start', 8)};`, + wide: `grid-row: 3; ${grid.between('centre-column-start', 9)};`, }, meta: { mobile: 'grid-row: 5;', tablet: 'grid-row: 5;', - desktop: 'grid-row: 4; grid-column: centre-column-start / 8;', - leftCol: - 'grid-row: 3; grid-column: left-column-start / left-column-end;', + desktop: `grid-row: 4; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 3; ${grid.column.left};`, }, body: { mobile: 'grid-row: 6;', }, 'right-column': { - desktop: - 'grid-row: 5; grid-column: right-column-start / right-column-end;', - leftCol: - 'grid-row: 4; grid-column: right-column-start / right-column-end;', + desktop: `grid-row: 5; ${grid.column.right};`, + leftCol: `grid-row: 4; ${grid.column.right};`, }, }; @@ -215,10 +212,8 @@ const immersiveLandscapeCss: LayoutCssMap = { media: { mobile: 'grid-row: 3;', tablet: 'grid-row: 3;', - desktop: - 'grid-row: 1 / span 3; grid-column: centre-column-start / right-column-end;', - leftCol: - 'grid-row: 1 / span 3; grid-column: left-column-start / right-column-end;', + desktop: `grid-row: 1 / span 3; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, }, standfirst: { mobile: 'grid-row: 4;', @@ -228,16 +223,14 @@ const immersiveLandscapeCss: LayoutCssMap = { meta: { mobile: 'grid-row: 5;', tablet: 'grid-row: 5;', - desktop: 'grid-row: 6; grid-column: centre-column-start / 8;', - leftCol: - 'grid-row: 6 / span 2; grid-column: left-column-start / left-column-end;', + desktop: `grid-row: 6; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 6 / span 2; ${grid.column.left};`, }, body: { mobile: 'grid-row: 6;', }, 'right-column': { - desktop: - 'grid-row: 5 / span 3; grid-column: right-column-start / right-column-end;', + desktop: `grid-row: 5 / span 3; ${grid.column.right};`, }, }; From 231cf0228727ca04bd7ca61e16a5fb4e1845210c Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 23 Jun 2026 16:14:51 +0100 Subject: [PATCH 09/80] First, clumsy pass at immersive forking --- .../src/layouts/lib/articleArrangements.ts | 83 +++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 0da7bcc5219..5a192931bce 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -6,8 +6,10 @@ export type LayoutType = | 'standard' | 'showcase' | 'media' - | 'immersiveLandscape' - | 'immersivePortrait'; + | 'immersiveLandscapeDefault' + | 'immersiveLandscapeFeature' + | 'immersivePortraitDefault' + | 'immersivePortraitFeature'; export type Area = | 'title' @@ -155,7 +157,7 @@ const mediaCss: LayoutCssMap = { }, }; -const immersivePortraitCss: LayoutCssMap = { +const immersivePortraitDefaultCss: LayoutCssMap = { title: { mobile: 'grid-row: 1;', tablet: 'grid-row: 1;', @@ -198,7 +200,50 @@ const immersivePortraitCss: LayoutCssMap = { }, }; -const immersiveLandscapeCss: LayoutCssMap = { +const immersivePortraitFeatureCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: `grid-row: 1; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 1; ${grid.between('left-column-start', 9)};`, + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: `grid-row: 2; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 2; ${grid.between('left-column-start', 9)};`, + wide: `grid-row: 2; ${grid.between('left-column-start', 10)};`, + }, + media: { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: `grid-row: 1 / span 4; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, + wide: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + desktop: `grid-row: 3; ${grid.between('centre-column-start', 7)};`, + leftCol: `grid-row: 3; ${grid.between('centre-column-start', 8)};`, + wide: `grid-row: 3; ${grid.between('centre-column-start', 9)};`, + }, + meta: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: `grid-row: 4; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 3; ${grid.column.left};`, + }, + body: { + mobile: 'grid-row: 6;', + }, + 'right-column': { + desktop: `grid-row: 5; ${grid.column.right};`, + leftCol: `grid-row: 4; ${grid.column.right};`, + }, +}; + +const immersiveLandscapeDefaultCss: LayoutCssMap = { title: { mobile: 'grid-row: 1;', tablet: 'grid-row: 1;', @@ -234,12 +279,38 @@ const immersiveLandscapeCss: LayoutCssMap = { }, }; +const immersiveLandscapeFeatureCss: LayoutCssMap = { + title: { + desktop: 'grid-row: 2;', + }, + headline: { + desktop: 'grid-row: 3 / span 2;', + }, + media: { + mobile: `${grid.column.all}`, + desktop: `grid-row: 1 / span 3; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, + }, + standfirst: { + desktop: 'grid-row: 5;', + }, + meta: { + desktop: `grid-row: 6; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 6 / span 2; ${grid.column.left};`, + }, + 'right-column': { + desktop: `grid-row: 5 / span 3; ${grid.column.right};`, + }, +}; + const layoutCssMaps: Record = { standard: standardCss, showcase: showcaseCss, media: mediaCss, - immersiveLandscape: immersiveLandscapeCss, - immersivePortrait: immersivePortraitCss, + immersiveLandscapeDefault: immersiveLandscapeDefaultCss, + immersiveLandscapeFeature: immersiveLandscapeFeatureCss, + immersivePortraitDefault: immersivePortraitDefaultCss, + immersivePortraitFeature: immersivePortraitFeatureCss, }; /** From 39b646d97277623ab1edd7a1d5927d9072852cba Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 30 Jun 2026 16:47:05 +0100 Subject: [PATCH 10/80] Distinguish between feature and non-feature immersives --- .../src/components/ArticleHeadline.tsx | 4 +-- dotcom-rendering/src/components/Figure.tsx | 23 ++++++------ .../src/layouts/lib/articleArrangements.ts | 35 +++++++++++++++++++ 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 96917e0fa74..f7951d9f145 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -400,8 +400,8 @@ export const ArticleHeadline = ({ starRating, }: Props) => { const isInverted = - format.design === ArticleDesign.Standard && - format.display === ArticleDisplay.Immersive; + format.display === ArticleDisplay.Immersive && + format.design === ArticleDesign.Standard; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index ab99567f7a6..7534acd1cd1 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -311,17 +311,18 @@ export const Figure = ({ return (
{children} - {format.display === ArticleDisplay.Immersive && ( -
- )} + {format.display === ArticleDisplay.Immersive && + format.design === ArticleDesign.Feature && ( +
+ )}
); } diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 5a192931bce..2812f4f6c49 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -350,3 +350,38 @@ export const gridItemCss = ( ${breakpointCss} `; }; + +/** + * Determines which {@link LayoutType} to render. Immersive layouts are + * split by orientation (portrait vs. landscape/square) and by whether the + * format is a Feature, since each combination has a distinct grid + * arrangement. Non-immersive formats fall back to media/showcase/standard. + */ +export const getLayoutType = ({ + isImmersive, + isFeature, + orientation, + isVideo, + isShowcase, +}: { + isImmersive: boolean; + isFeature: boolean; + orientation: 'portrait' | 'landscape' | 'square'; + isVideo: boolean; + isShowcase: boolean; +}): LayoutType => { + if (isImmersive) { + if (orientation === 'portrait') { + return isFeature + ? 'immersivePortraitFeature' + : 'immersivePortraitDefault'; + } + // Square images are treated the same as landscape for immersive layouts. + return isFeature + ? 'immersiveLandscapeFeature' + : 'immersiveLandscapeDefault'; + } + if (isVideo) return 'media'; + if (isShowcase) return 'showcase'; + return 'standard'; +}; From 86a943f7d7e1e9d0e495af6dfd1d3f59e815731f Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 30 Jun 2026 17:46:29 +0100 Subject: [PATCH 11/80] Fix portrait main media config --- .../src/layouts/lib/articleArrangements.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 2812f4f6c49..e6e64cffe34 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -174,9 +174,9 @@ const immersivePortraitDefaultCss: LayoutCssMap = { media: { mobile: 'grid-row: 3;', tablet: 'grid-row: 3;', - desktop: `grid-row: 1 / span 4; ${grid.between('centre-column-start', 'right-column-end')};`, - leftCol: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, - wide: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, + desktop: `grid-row: 1 / span 4; ${grid.between(8, 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between(9, 'right-column-end')};`, + wide: `grid-row: 1 / span 3; ${grid.between(10, 'right-column-end')};`, }, standfirst: { mobile: 'grid-row: 4;', @@ -217,9 +217,9 @@ const immersivePortraitFeatureCss: LayoutCssMap = { media: { mobile: 'grid-row: 3;', tablet: 'grid-row: 3;', - desktop: `grid-row: 1 / span 4; ${grid.between('centre-column-start', 'right-column-end')};`, - leftCol: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, - wide: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, + desktop: `grid-row: 1 / span 4; ${grid.between(8, 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between(9, 'right-column-end')};`, + wide: `grid-row: 1 / span 3; ${grid.between(10, 'right-column-end')};`, }, standfirst: { mobile: 'grid-row: 4;', From 05d9d264024af840d6b513d0de5f0529323efd1a Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 30 Jun 2026 18:21:43 +0100 Subject: [PATCH 12/80] Tie main media styling to article arrangement --- dotcom-rendering/src/components/Figure.tsx | 32 ++--- dotcom-rendering/src/components/MainMedia.tsx | 4 + .../src/layouts/StandardLayout.tsx | 132 ++++++++++++------ dotcom-rendering/src/lib/renderElement.tsx | 4 + 4 files changed, 109 insertions(+), 63 deletions(-) diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index 7534acd1cd1..83a444ac608 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -6,11 +6,8 @@ import { space, until, } from '@guardian/source/foundations'; -import { - ArticleDesign, - ArticleDisplay, - type ArticleFormat, -} from '../lib/articleFormat'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; +import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; import { transparentColour } from '../lib/transparentColour'; import { palette } from '../palette'; import type { FEElement, RoleType } from '../types/content'; @@ -24,6 +21,7 @@ type Props = { className?: string; type?: FEElement['_type']; isTimeline?: boolean; + articleArrangement?: LayoutType; }; const overlayMaskGradientStyles = (angle: string) => css` @@ -301,6 +299,7 @@ export const Figure = ({ className = '', type, isTimeline = false, + articleArrangement = 'standard', }: Props) => { if (isMainMedia && !isTimeline) { // Don't add in-body styles for main media elements @@ -311,18 +310,17 @@ export const Figure = ({ return (
{children} - {format.display === ArticleDisplay.Immersive && - format.design === ArticleDesign.Feature && ( -
- )} + {articleArrangement === 'immersiveLandscapeFeature' && ( +
+ )}
); } diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx index 96869f41479..a3b2f1897ba 100644 --- a/dotcom-rendering/src/components/MainMedia.tsx +++ b/dotcom-rendering/src/components/MainMedia.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/react'; import { breakpoints, space, until } from '@guardian/source/foundations'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { ArticleDesign, ArticleDisplay, @@ -97,6 +98,7 @@ type Props = { shouldHideAds: boolean; contentType?: string; contentLayout?: string; + articleArrangement?: LayoutType; }; export const MainMedia = ({ @@ -115,6 +117,7 @@ export const MainMedia = ({ shouldHideAds, contentType, contentLayout, + articleArrangement, }: Props) => { return (
@@ -138,6 +141,7 @@ export const MainMedia = ({ shouldHideAds={shouldHideAds} contentType={contentType} contentLayout={contentLayout} + articleArrangement={articleArrangement} /> ))}
diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 0c24acb106d..47934ee2269 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -20,6 +20,7 @@ import { ArticleMetaApps } from '../components/ArticleMeta.apps'; import { ArticleMeta } from '../components/ArticleMeta.web'; import { ArticleTitle } from '../components/ArticleTitle'; import { Carousel } from '../components/Carousel.island'; +import { CricketMatchHeaderWrapper } from '../components/CricketMatchHeaderWrapper.island'; import { DecideLines } from '../components/DecideLines'; import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland'; import { DiscussionLayout } from '../components/DiscussionLayout'; @@ -33,6 +34,7 @@ import { LabsHeader } from '../components/LabsHeader'; import { ListenToArticle } from '../components/ListenToArticle.island'; import { MainMedia } from '../components/MainMedia'; import { Masthead } from '../components/Masthead/Masthead'; +import { MatchHeaderFallback } from '../components/MatchHeaderFallback'; import { MostViewedFooterData } from '../components/MostViewedFooterData.island'; import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; import { MostViewedRightWithAd } from '../components/MostViewedRightWithAd.island'; @@ -54,9 +56,9 @@ import { import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideStoryPackageTrails } from '../lib/decideTrail'; -import type { EditionId } from '../lib/edition'; import { safeParseURL } from '../lib/parse'; import { parse } from '../lib/slot-machine-flags'; +import { useAB } from '../lib/useAB'; import { worldCupTagId } from '../lib/worldCup2026'; import type { NavType } from '../model/extract-nav'; import { palette as themePalette } from '../palette'; @@ -167,16 +169,16 @@ export const StandardLayout = (props: WebProps | AppProps) => { ? article.matchStatsUrl : undefined; - const footballMatchHeaderUrl = - article.matchType === 'FootballMatchType' - ? article.matchHeaderUrl - : undefined; + const isFootballMatchReport = + format.design === ArticleDesign.MatchReport && !!footballMatchUrl; - const footballMatchLeagueName = article.sectionLabel; - const footballMatchLeagueUrl = `${article.guardianBaseURL}/${article.sectionUrl}`; + const cricketMatchUrl = + article.matchType === 'CricketMatchType' + ? article.matchStatsUrl + : undefined; - const isMatchReport = - format.design === ArticleDesign.MatchReport && !!footballMatchUrl; + const isCricketMatchReport = + format.design === ArticleDesign.MatchReport && !!cricketMatchUrl; const isMedia = format.design === ArticleDesign.Video || @@ -286,12 +288,11 @@ export const StandardLayout = (props: WebProps | AppProps) => { /> {isWeb && renderAds && hasSurveyAd && ( @@ -604,7 +605,7 @@ export const StandardLayout = (props: WebProps | AppProps) => { idApiUrl={article.config.idApiUrl} /> @@ -940,43 +941,82 @@ export const StandardLayout = (props: WebProps | AppProps) => { }; const MatchHeaderContainer = ({ - isMatchReport, - footballMatchHeaderUrl, - leagueName, - leagueUrl, - editionId, + isFootballMatchReport, + isCricketMatchReport, renderingTarget, + article, + format, }: { - isMatchReport: boolean; - footballMatchHeaderUrl: string | undefined; - leagueName: string; - leagueUrl: string; - editionId: EditionId; + isFootballMatchReport: boolean; + isCricketMatchReport: boolean; renderingTarget: RenderingTarget; + article: ArticleDeprecated; + format: ArticleFormat; }) => { - if (isMatchReport && !!footballMatchHeaderUrl) { - const parsedUrl = safeParseURL(footballMatchHeaderUrl); - if (!parsedUrl.ok) { - log( - 'dotcom', - new Error( - `Failed to parse match header URL: ${footballMatchHeaderUrl}`, - ), - ); + const footballMatchHeaderUrl = + article.matchType === 'FootballMatchType' + ? article.matchHeaderUrl + : undefined; - return null; - } + const footballMatchLeagueName = article.sectionLabel; + const footballMatchLeagueUrl = `${article.guardianBaseURL}/${article.sectionUrl}`; + + const cricketMatchHeaderUrl = + article.matchType === 'CricketMatchType' + ? article.matchHeaderUrl + : undefined; + + const ab = useAB(); + const isCricketRedesignEnabled = Boolean( + ab?.isUserInTestGroup('webx-cricket-redesign', 'enable'), + ); + + const isApps = renderingTarget === 'Apps'; + + if (isFootballMatchReport && footballMatchHeaderUrl) { return ( - - - + <> + + + + + + ); + } + + if ( + !isApps && + cricketMatchHeaderUrl && + isCricketMatchReport && + isCricketRedesignEnabled + ) { + return ( + <> + + + + + ); } diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index f877210f059..694334c9a58 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -68,6 +68,7 @@ import { } from '../components/WitnessBlockComponent'; import { YoutubeBlockComponent } from '../components/YoutubeBlockComponent.island'; import { YoutubeEmbedBlockComponent } from '../components/YoutubeEmbedBlockComponent'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { interactiveLegacyFigureClasses, isInteractive, @@ -107,6 +108,7 @@ type Props = { shouldHideAds: boolean; contentType?: string; contentLayout?: string; + articleArrangement?: LayoutType; idApiUrl?: string; }; @@ -1050,6 +1052,7 @@ export const RenderArticleElement = ({ shouldHideAds, contentType, contentLayout, + articleArrangement, idApiUrl, }: Props) => { const withUpdatedRole = updateRole(element, format); @@ -1103,6 +1106,7 @@ export const RenderArticleElement = ({ type={element._type} format={format} isTimeline={isTimeline} + articleArrangement={articleArrangement} > {el} From 0dafbd31efaaa836d1d696ce32435025ad6a74a9 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 1 Jul 2026 14:17:56 +0100 Subject: [PATCH 13/80] Restore wrapper --- .../src/layouts/StandardLayout.tsx | 718 +++++++++--------- 1 file changed, 370 insertions(+), 348 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 47934ee2269..48580b4d356 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -307,182 +307,227 @@ export const StandardLayout = (props: WebProps | AppProps) => { )} {/* GridItem order matters — mobile layout relies on DOM order for grid placement. - See furnitureArrangements.ts if reordering. */} -
+
- - - - - - - - - - - - - - {layoutType !== 'immersivePortraitDefault' && ( -
- {isWeb && - format.theme === ArticleSpecial.Labs && - format.design !== ArticleDesign.Video ? ( - - ) : ( - - )} -
- )} - {isApps ? ( - <> - - - - + > + +
+ + + + + + + + + + + {layoutType !== 'immersivePortraitDefault' && ( +
+ {isWeb && + format.theme === ArticleSpecial.Labs && + format.design !== ArticleDesign.Video ? ( + + ) : ( + + )} +
+ )} + {isApps ? ( + <> + + + + + + {!!article.affiliateLinksDisclaimer && ( + + )} + + + ) : ( + <> { {!!article.affiliateLinksDisclaimer && ( )} + + )} +
+ + {/* Only show Listen to Article button on App landscape views */} + {isApps && ( + + {!isVideo && ( +
+ + + +
+ )}
- - ) : ( - <> - + + - {!!article.affiliateLinksDisclaimer && ( - + + {isApps && ( + + + )} - - )} -
- - {/* Only show Listen to Article button on App landscape views */} - {isApps && ( - - {!isVideo && ( -
- - - -
+ + )} -
- )} - - + + +
+ - - - {isApps && ( - - - - )} - - {showBodyEndSlot && ( + `} + > + - - )} - - - - - - - - - - - -
+ + +
+

{isWeb && renderAds && !isLabs && (
Date: Tue, 7 Jul 2026 17:18:14 +0100 Subject: [PATCH 14/80] Enhance article meta and layout handling for immersive designs --- .../src/components/ArticleMeta.web.tsx | 7 +++++-- dotcom-rendering/src/layouts/StandardLayout.tsx | 3 ++- dotcom-rendering/src/lib/articleMeta.ts | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index 5abd00eede4..116b61e94e3 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -1,9 +1,10 @@ import { css } from '@emotion/react'; import { isUndefined } from '@guardian/libs'; -import { between, from, space, until } from '@guardian/source/foundations'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; +import { between, from, space, until } from '@guardian/source/foundations'; import type { CSSProperties } from 'react'; import type { FEArticle } from '../frontend/feArticle'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { interactiveLegacyClasses } from '../layouts/lib/interactiveLegacyStyling'; import { ArticleDesign, @@ -40,6 +41,7 @@ import { TimeDateline } from './TimeDateline'; type Props = { format: ArticleFormat; + layoutType: LayoutType; pageId: string; webTitle: string; byline?: string; @@ -276,6 +278,7 @@ const metaNumbersExtrasLiveBlog = css` export const ArticleMeta = ({ branding, format, + layoutType, pageId, webTitle, byline, @@ -369,7 +372,7 @@ export const ArticleMeta = ({ /> )} - {shouldShowContributor(format) && ( + {shouldShowContributor(format, layoutType) && ( { { } }; -export const shouldShowContributor = (format: ArticleFormat): boolean => { +export const shouldShowContributor = ( + format: ArticleFormat, + layoutType: LayoutType, +): boolean => { switch (format.display) { case ArticleDisplay.NumberedList: return true; case ArticleDisplay.Immersive: - return false; + switch (layoutType) { + case 'immersivePortraitDefault': + case 'immersivePortraitFeature': + return true; + default: + return false; + } case ArticleDisplay.Showcase: case ArticleDisplay.Standard: { switch (format.design) { From 4afd212eb77425d52e19a3792cdfa8d7cdaa8728 Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Fri, 17 Jul 2026 15:32:09 +0100 Subject: [PATCH 15/80] show byline for immersive landscape --- dotcom-rendering/src/lib/articleMeta.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotcom-rendering/src/lib/articleMeta.ts b/dotcom-rendering/src/lib/articleMeta.ts index 7f35cd1080e..44bc184fe11 100644 --- a/dotcom-rendering/src/lib/articleMeta.ts +++ b/dotcom-rendering/src/lib/articleMeta.ts @@ -39,6 +39,8 @@ export const shouldShowContributor = ( switch (layoutType) { case 'immersivePortraitDefault': case 'immersivePortraitFeature': + case 'immersiveLandscapeDefault': + case 'immersiveLandscapeFeature': return true; default: return false; From ec861657661ead1220c9b9b791675e7f626d6e36 Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Tue, 21 Jul 2026 16:49:43 +0100 Subject: [PATCH 16/80] show byline and avatar in a row, show series tag, reintroduce borders to top and bottom of headline container, hide stripes above meta --- .../src/components/ArticleMeta.web.tsx | 103 ++++++++++++++++-- .../src/components/ArticleTitle.tsx | 6 +- .../src/components/SeriesSectionLink.tsx | 36 +++++- .../src/layouts/StandardLayout.tsx | 40 ++++--- dotcom-rendering/src/lib/articleMeta.ts | 15 ++- 5 files changed, 172 insertions(+), 28 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index 116b61e94e3..c650651b8c6 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -248,6 +248,25 @@ const MetaAvatarContainer = ({ children }: { children: React.ReactNode }) => ( ); +const ImmersiveMetaAvatarContainer = ({ + children, +}: { + children: React.ReactNode; +}) => ( +
+ {children} +
+); + const RowBelowLeftCol = ({ children }: { children: React.ReactNode }) => (
- <> + {isImmersive && isPortraitOrLandscapeImmersive ? ( +
{!!avatarUrl && ( - + - + )}
{isAudio && podcast && seriesTag && ( @@ -403,9 +435,66 @@ export const ArticleMeta = ({ /> )}
- - +
+ ) : ( + + <> + {!!avatarUrl && ( + + + + )} +
+ {isAudio && podcast && seriesTag && ( + + )} + {shouldShowContributor(format, layoutType) && ( + + )} + + {crossword?.creator && ( + + )} + + {!isUndefined(webPublicationDate) && + isFilterArticle ? ( + + ) : ( + + )} +
+ +
+ )}
{ export const SeriesSectionLink = ({ format, + layoutType, tags, sectionLabel, sectionUrl, @@ -394,7 +397,38 @@ export const SeriesSectionLink = ({
); } - // Immersives show nothing at all if there's no series tag + if ( + layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature' || + layoutType === 'immersiveLandscapeDefault' || + layoutType === 'immersiveLandscapeFeature' + ) { + return ( + <> + + {sectionLabel} + + + ); + } + // Other types of immersives show nothing at all if there's no series tag return null; } if (tag) { diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index f61f479d81e..a333066a1ab 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -388,6 +388,7 @@ export const StandardLayout = (props: WebProps | AppProps) => { > { area="headline" layoutType={layoutType} css={ - layoutType === 'immersivePortraitDefault' + layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature' ? css` ${from.desktop} { border-bottom: 1px solid @@ -448,22 +450,26 @@ export const StandardLayout = (props: WebProps | AppProps) => { : undefined } > - {layoutType !== 'immersivePortraitDefault' && ( -
- {isWeb && - format.theme === ArticleSpecial.Labs && - format.design !== ArticleDesign.Video ? ( - - ) : ( - - )} -
- )} + {layoutType !== 'immersivePortraitDefault' && + layoutType !== 'immersivePortraitFeature' && + layoutType !== 'immersiveLandscapeDefault' && + layoutType !== 'immersiveLandscapeFeature' && ( +
+ {isWeb && + format.theme === ArticleSpecial.Labs && + format.design !== + ArticleDesign.Video ? ( + + ) : ( + + )} +
+ )} {isApps ? ( <> diff --git a/dotcom-rendering/src/lib/articleMeta.ts b/dotcom-rendering/src/lib/articleMeta.ts index 44bc184fe11..6bf34183322 100644 --- a/dotcom-rendering/src/lib/articleMeta.ts +++ b/dotcom-rendering/src/lib/articleMeta.ts @@ -6,10 +6,21 @@ import { type ArticleFormat, } from './articleFormat'; -export const shouldShowAvatar = (format: ArticleFormat): boolean => { +export const shouldShowAvatar = ( + format: ArticleFormat, + layoutType: LayoutType, +): boolean => { switch (format.display) { case ArticleDisplay.Immersive: - return false; + switch (layoutType) { + case 'immersivePortraitDefault': + case 'immersivePortraitFeature': + case 'immersiveLandscapeDefault': + case 'immersiveLandscapeFeature': + return true; + default: + return false; + } case ArticleDisplay.Showcase: case ArticleDisplay.NumberedList: case ArticleDisplay.Standard: { From d582bd9d4d864f618633cc4f9149ebde64bf4df4 Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Wed, 22 Jul 2026 12:07:14 +0100 Subject: [PATCH 17/80] add testing for new layoutType parameters influencing contributor showing --- .../src/components/ArticleMeta.apps.tsx | 7 ++-- .../src/components/ArticleMeta.web.tsx | 14 ++------ dotcom-rendering/src/lib/articleMeta.test.ts | 33 +++++++++++++++++++ dotcom-rendering/src/lib/articleMeta.ts | 4 +-- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.apps.tsx b/dotcom-rendering/src/components/ArticleMeta.apps.tsx index de5333c68e5..9acf40bdfbe 100644 --- a/dotcom-rendering/src/components/ArticleMeta.apps.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.apps.tsx @@ -4,6 +4,7 @@ import { from, space, until } from '@guardian/source/foundations'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import type { ReactNode } from 'react'; import { grid } from '../grid'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { interactiveLegacyClasses } from '../layouts/lib/interactiveLegacyStyling'; import { ArticleDesign, @@ -36,6 +37,7 @@ import { PodcastMeta } from './PodcastMeta'; type Props = { format: ArticleFormat; + layoutType: LayoutType; byline?: string; tags: TagType[]; primaryDateline: string; @@ -242,6 +244,7 @@ const galleryMetaContainer = css` export const ArticleMetaApps = ({ branding, format, + layoutType, byline, tags, primaryDateline, @@ -255,7 +258,7 @@ export const ArticleMetaApps = ({ }: Props) => { const soleContributor = getSoleContributor(tags, byline); const authorName = soleContributor?.title ?? 'Author Image'; - const avatarUrl = shouldShowAvatar(format) + const avatarUrl = shouldShowAvatar(format, layoutType) ? soleContributor?.bylineLargeImageUrl : undefined; @@ -328,7 +331,7 @@ export const ArticleMetaApps = ({ )} - {shouldShowContributor(format) && !!byline && ( + {shouldShowContributor(format, layoutType) && !!byline && ( - hasButton - ? { - marginBottom: 8, - } - : {}; + hasButton ? { marginBottom: 8 } : {}; const stretchLines = css` display: block; @@ -151,11 +147,7 @@ const metaExtras = (isPictureContent: boolean) => css` `; const preferredSourceMetaExtras = (hasButton: boolean): CSSProperties => - hasButton - ? { - paddingTop: 8, - } - : {}; + hasButton ? { paddingTop: 8 } : {}; const metaNumbers = (isPictureContent: boolean) => css` border-top: 1px solid ${themePalette('--article-border')}; diff --git a/dotcom-rendering/src/lib/articleMeta.test.ts b/dotcom-rendering/src/lib/articleMeta.test.ts index dd7eebd8ebd..19fb2fb9003 100644 --- a/dotcom-rendering/src/lib/articleMeta.test.ts +++ b/dotcom-rendering/src/lib/articleMeta.test.ts @@ -28,6 +28,23 @@ describe('shouldShowContributor', () => { display: ArticleDisplay.Immersive, }; + const immersivePortraitDefault = { + ...immersive, + layoutType: 'immersivePortraitDefault', + }; + const immersivePortraitFeature = { + ...immersive, + layoutType: 'immersivePortraitFeature', + }; + const immersiveLandscapeDefault = { + ...immersive, + layoutType: 'immersiveLandscapeDefault', + }; + const immersiveLandscapeFeature = { + ...immersive, + layoutType: 'immersiveLandscapeFeature', + }; + it('should return true if Standard display and Standard design', () => { expect(shouldShowContributor(standardFormat)).toBe(true); }); @@ -51,4 +68,20 @@ describe('shouldShowContributor', () => { it('should return false if Immersive display', () => { expect(shouldShowContributor(immersive)).toBe(false); }); + + it('should return true if Immersive display and immersivePortraitDefault layout', () => { + expect(shouldShowContributor(immersivePortraitDefault)).toBe(true); + }); + + it('should return true if Immersive display and immersivePortraitFeature layout', () => { + expect(shouldShowContributor(immersivePortraitFeature)).toBe(true); + }); + + it('should return true if Immersive display and immersiveLandscapeDefault layout', () => { + expect(shouldShowContributor(immersiveLandscapeDefault)).toBe(true); + }); + + it('should return true if Immersive display and immersiveLandscapeFeature layout', () => { + expect(shouldShowContributor(immersiveLandscapeFeature)).toBe(true); + }); }); diff --git a/dotcom-rendering/src/lib/articleMeta.ts b/dotcom-rendering/src/lib/articleMeta.ts index 6bf34183322..6f95ffe66be 100644 --- a/dotcom-rendering/src/lib/articleMeta.ts +++ b/dotcom-rendering/src/lib/articleMeta.ts @@ -8,7 +8,7 @@ import { export const shouldShowAvatar = ( format: ArticleFormat, - layoutType: LayoutType, + layoutType?: LayoutType, ): boolean => { switch (format.display) { case ArticleDisplay.Immersive: @@ -41,7 +41,7 @@ export const shouldShowAvatar = ( export const shouldShowContributor = ( format: ArticleFormat, - layoutType: LayoutType, + layoutType?: LayoutType, ): boolean => { switch (format.display) { case ArticleDisplay.NumberedList: From f92e0850bbd6afb914c5b2ef973311f0306dec67 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 19 May 2026 15:25:40 +0100 Subject: [PATCH 18/80] Rough first pass --- .../src/layouts/StandardLayoutArticleGrid.tsx | 164 ++++++++++++--- .../src/layouts/lib/articleArrangements.ts | 197 +++++++++++++++++- 2 files changed, 335 insertions(+), 26 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 4d6aab78791..006e0b2b1a6 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -22,6 +22,7 @@ import { SlotBodyEnd } from '../components/SlotBodyEnd.island'; import { Standfirst } from '../components/Standfirst'; import { SubMeta } from '../components/SubMeta'; import { grid } from '../grid'; +import { getAgeWarning } from '../lib/age-warning'; import { ArticleDesign, ArticleDisplay, @@ -36,6 +37,7 @@ import type { ArticleDeprecated } from '../types/article'; import type { RenderingTarget } from '../types/renderingTarget'; import { type Area, + getLayoutType, gridItemCss, type LayoutType, } from './lib/articleArrangements'; @@ -59,6 +61,23 @@ interface GridItemProps { children: React.ReactNode; } +/** + * Works out the orientation of an image from its Guardian media URL, which + * encodes the crop dimensions in the path (e.g. `/1000_600_800_480/`). + * Falls back to 'landscape' if the URL doesn't match the expected pattern. + */ +const getImageOrientation = ( + url: string, +): 'portrait' | 'landscape' | 'square' => { + const match = url.match(/\/\d+_\d+_(\d+)_(\d+)\/\d+\.\w+$/); + if (!match) return 'landscape'; + const [, width, height] = match.map(Number); + if (width == null || height == null) return 'landscape'; + if (height > width) return 'portrait'; + if (width > height) return 'landscape'; + return 'square'; +}; + const GridItem = ({ area, layoutType, @@ -110,6 +129,8 @@ export const StandardLayoutArticleGrid = ({ format.design === ArticleDesign.Video || format.design === ArticleDesign.Audio; const isShowcase = format.display === ArticleDisplay.Showcase; + const isImmersive = format.display === ArticleDisplay.Immersive; + const isFeature = format.design === ArticleDesign.Feature; const isVideo = format.design === ArticleDesign.Video; @@ -121,11 +142,36 @@ export const StandardLayoutArticleGrid = ({ const isFootballMatchReport = format.design === ArticleDesign.MatchReport && !!footballMatchUrl; - const layoutType: LayoutType = isMedia - ? 'media' - : isShowcase - ? 'showcase' - : 'standard'; + const firstMainMediaElement = article.mainMediaElements[0]; + const mainMediaUrl: string | undefined = + firstMainMediaElement?._type === + 'model.dotcomrendering.pageElements.ImageBlockElement' + ? firstMainMediaElement.media.allImages[0]?.url + : undefined; + + const mainMediaOrientation = + mainMediaUrl != null ? getImageOrientation(mainMediaUrl) : 'landscape'; + + const layoutType = getLayoutType({ + isImmersive, + isFeature, + orientation: mainMediaOrientation, + isVideo, + isShowcase, + }); + const contentLayoutName = `${ArticleDisplay[format.display]}Layout`; + + const isImmersivePortrait = + layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature'; + const isImmersiveLandscape = + layoutType === 'immersiveLandscapeDefault' || + layoutType === 'immersiveLandscapeFeature'; + + const ageWarning = getAgeWarning( + article.tags, + article.webPublicationDateDeprecated, + ); return (
- + - + - + - -
- {isWeb && - format.theme === ArticleSpecial.Labs && - format.design !== ArticleDesign.Video ? ( - - ) : ( - - )} -
+ + {layoutType !== 'immersivePortraitDefault' && ( +
+ {isWeb && + format.theme === ArticleSpecial.Labs && + format.design !== ArticleDesign.Video ? ( + + ) : ( + + )} +
+ )} {isApps ? ( <> @@ -263,11 +379,11 @@ export const StandardLayoutArticleGrid = ({ secondaryDateline={ article.webPublicationSecondaryDateDisplay } + webPublicationDate={article.webPublicationDate} isCommentable={article.isCommentable} discussionApiUrl={article.config.discussionApiUrl} shortUrlId={article.config.shortUrlId} mainMediaElements={article.mainMediaElements} - webPublicationDate={article.webPublicationDate} /> {!!article.affiliateLinksDisclaimer && ( diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 2cf5fb6843f..e6e64cffe34 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -2,7 +2,14 @@ import { css, type SerializedStyles } from '@emotion/react'; import { from, until } from '@guardian/source/foundations'; import { grid } from '../../grid'; -export type LayoutType = 'standard' | 'showcase' | 'media'; +export type LayoutType = + | 'standard' + | 'showcase' + | 'media' + | 'immersiveLandscapeDefault' + | 'immersiveLandscapeFeature' + | 'immersivePortraitDefault' + | 'immersivePortraitFeature'; export type Area = | 'title' @@ -13,13 +20,14 @@ export type Area = | 'body' | 'right-column'; -type Breakpoint = 'mobile' | 'tablet' | 'desktop' | 'leftCol'; +type Breakpoint = 'mobile' | 'tablet' | 'desktop' | 'leftCol' | 'wide'; const breakpointQueries: Record = { mobile: until.tablet, tablet: from.tablet, desktop: from.desktop, leftCol: from.leftCol, + wide: from.wide, }; // Raw CSS overrides per area per breakpoint. Entries are only needed when an area @@ -149,10 +157,160 @@ const mediaCss: LayoutCssMap = { }, }; +const immersivePortraitDefaultCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: `grid-row: 1; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 1; ${grid.between('left-column-start', 9)};`, + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: `grid-row: 2; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 2; ${grid.between('left-column-start', 9)};`, + wide: `grid-row: 2; ${grid.between('left-column-start', 10)};`, + }, + media: { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: `grid-row: 1 / span 4; ${grid.between(8, 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between(9, 'right-column-end')};`, + wide: `grid-row: 1 / span 3; ${grid.between(10, 'right-column-end')};`, + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + desktop: `grid-row: 3; ${grid.between('centre-column-start', 7)};`, + leftCol: `grid-row: 3; ${grid.between('centre-column-start', 8)};`, + wide: `grid-row: 3; ${grid.between('centre-column-start', 9)};`, + }, + meta: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: `grid-row: 4; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 3; ${grid.column.left};`, + }, + body: { + mobile: 'grid-row: 6;', + }, + 'right-column': { + desktop: `grid-row: 5; ${grid.column.right};`, + leftCol: `grid-row: 4; ${grid.column.right};`, + }, +}; + +const immersivePortraitFeatureCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: `grid-row: 1; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 1; ${grid.between('left-column-start', 9)};`, + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: `grid-row: 2; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 2; ${grid.between('left-column-start', 9)};`, + wide: `grid-row: 2; ${grid.between('left-column-start', 10)};`, + }, + media: { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: `grid-row: 1 / span 4; ${grid.between(8, 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between(9, 'right-column-end')};`, + wide: `grid-row: 1 / span 3; ${grid.between(10, 'right-column-end')};`, + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + desktop: `grid-row: 3; ${grid.between('centre-column-start', 7)};`, + leftCol: `grid-row: 3; ${grid.between('centre-column-start', 8)};`, + wide: `grid-row: 3; ${grid.between('centre-column-start', 9)};`, + }, + meta: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: `grid-row: 4; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 3; ${grid.column.left};`, + }, + body: { + mobile: 'grid-row: 6;', + }, + 'right-column': { + desktop: `grid-row: 5; ${grid.column.right};`, + leftCol: `grid-row: 4; ${grid.column.right};`, + }, +}; + +const immersiveLandscapeDefaultCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: 'grid-row: 2;', + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: 'grid-row: 3 / span 2;', + }, + media: { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: `grid-row: 1 / span 3; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + desktop: 'grid-row: 5;', + }, + meta: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: `grid-row: 6; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 6 / span 2; ${grid.column.left};`, + }, + body: { + mobile: 'grid-row: 6;', + }, + 'right-column': { + desktop: `grid-row: 5 / span 3; ${grid.column.right};`, + }, +}; + +const immersiveLandscapeFeatureCss: LayoutCssMap = { + title: { + desktop: 'grid-row: 2;', + }, + headline: { + desktop: 'grid-row: 3 / span 2;', + }, + media: { + mobile: `${grid.column.all}`, + desktop: `grid-row: 1 / span 3; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, + }, + standfirst: { + desktop: 'grid-row: 5;', + }, + meta: { + desktop: `grid-row: 6; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 6 / span 2; ${grid.column.left};`, + }, + 'right-column': { + desktop: `grid-row: 5 / span 3; ${grid.column.right};`, + }, +}; + const layoutCssMaps: Record = { standard: standardCss, showcase: showcaseCss, media: mediaCss, + immersiveLandscapeDefault: immersiveLandscapeDefaultCss, + immersiveLandscapeFeature: immersiveLandscapeFeatureCss, + immersivePortraitDefault: immersivePortraitDefaultCss, + immersivePortraitFeature: immersivePortraitFeatureCss, }; /** @@ -192,3 +350,38 @@ export const gridItemCss = ( ${breakpointCss} `; }; + +/** + * Determines which {@link LayoutType} to render. Immersive layouts are + * split by orientation (portrait vs. landscape/square) and by whether the + * format is a Feature, since each combination has a distinct grid + * arrangement. Non-immersive formats fall back to media/showcase/standard. + */ +export const getLayoutType = ({ + isImmersive, + isFeature, + orientation, + isVideo, + isShowcase, +}: { + isImmersive: boolean; + isFeature: boolean; + orientation: 'portrait' | 'landscape' | 'square'; + isVideo: boolean; + isShowcase: boolean; +}): LayoutType => { + if (isImmersive) { + if (orientation === 'portrait') { + return isFeature + ? 'immersivePortraitFeature' + : 'immersivePortraitDefault'; + } + // Square images are treated the same as landscape for immersive layouts. + return isFeature + ? 'immersiveLandscapeFeature' + : 'immersiveLandscapeDefault'; + } + if (isVideo) return 'media'; + if (isShowcase) return 'showcase'; + return 'standard'; +}; From ad4cd74d09458370e57bee46884c58a348168afb Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 26 May 2026 16:50:39 +0100 Subject: [PATCH 19/80] Rough implementation of portrait/landscape variants --- .../src/components/ArticleHeadline.tsx | 151 +-- .../src/components/ArticleTitle.tsx | 10 +- .../src/components/Standfirst.tsx | 1 - dotcom-rendering/src/layouts/DecideLayout.tsx | 5 +- .../src/layouts/ImmersiveLayout.tsx | 1022 ----------------- dotcom-rendering/src/paletteDeclarations.ts | 15 +- 6 files changed, 62 insertions(+), 1142 deletions(-) delete mode 100644 dotcom-rendering/src/layouts/ImmersiveLayout.tsx diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 3b0c141fc86..8beda81e2eb 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -27,7 +27,6 @@ import { ArticleSpecial, Pillar, } from '../lib/articleFormat'; -import { getZIndex } from '../lib/getZIndex'; import { palette as themePalette } from '../palette'; import type { StarRating as Rating } from '../types/content'; import type { TagType } from '../types/tag'; @@ -45,6 +44,7 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; + isInverted?: boolean; }; const topPadding = css` @@ -214,30 +214,20 @@ const invertedStyles = css` box-decoration-break: clone; `; -const immersiveStyles = css` - min-height: 112px; - padding-bottom: ${space[6]}px; - padding-left: ${space[1]}px; - - ${from.mobileLandscape} { - padding-left: ${space[3]}px; - } - - ${from.tablet} { - padding-left: ${space[1]}px; - } - - margin-right: ${space[5]}px; -`; - const darkBackground = css` background-color: ${themePalette('--headline-background')}; `; const invertedText = css` - white-space: pre-wrap; - padding-bottom: ${space[1]}px; - padding-right: ${space[1]}px; + ${from.desktop} { + color: white; + background-color: black; + white-space: pre-wrap; + padding-bottom: ${space[1]}px; + padding-right: ${space[1]}px; + margin-left: -10px; + padding-left: 10px; + } `; const maxWidth = css` @@ -255,35 +245,6 @@ const invertedWrapper = css` margin-left: 6px; `; -const immersiveWrapper = css` - /* - Make sure we vertically align the headline font with the body font - */ - margin-left: 6px; - - ${from.tablet} { - margin-left: 16px; - } - - ${from.leftCol} { - margin-left: 25px; - } - - /* - We need this grow to ensure the headline fills the main content column - */ - flex-grow: 1; - /* - This z-index is what ensures the headline text shows above the pseudo black - box that extends the black background to the right - */ - z-index: ${getZIndex('articleHeadline')}; - - ${until.mobileLandscape} { - margin-right: 40px; - } -`; - // Due to MainMedia using position: relative, this seems to effect the rendering order // To mitigate we use z-index // TODO: find a cleaner solution @@ -292,59 +253,58 @@ const zIndex = css` `; const ageWarningMargins = (format: ArticleFormat) => { - if (format.design === ArticleDesign.Gallery) { + if ( + format.design === ArticleDesign.Gallery || + format.display === ArticleDisplay.Immersive + ) { return ''; } - return format.display === ArticleDisplay.Immersive - ? css` - margin-left: 0px; - margin-bottom: 0px; - - ${from.tablet} { - margin-left: 10px; - } + return css` + margin-top: 12px; + margin-left: -10px; + margin-bottom: 6px; - ${from.leftCol} { - margin-left: 20px; - } - ` - : css` - margin-top: 12px; - margin-left: -10px; - margin-bottom: 6px; - - ${from.tablet} { - margin-left: -20px; - } + ${from.tablet} { + margin-left: -20px; + } - ${from.leftCol} { - margin-left: -10px; - margin-top: 0; - } - `; + ${from.leftCol} { + margin-left: -10px; + margin-top: 0; + } + `; }; -const backgroundStyles = css` - background-color: ${themePalette('--age-warning-wrapper-background')}; -`; - const WithAgeWarning = ({ tags, webPublicationDateDeprecated, format, children, + snapToInverted = false, }: { tags: TagType[]; webPublicationDateDeprecated: string; format: ArticleFormat; children: React.ReactNode; + snapToInverted?: boolean; }) => { const age = getAgeWarning(tags, webPublicationDateDeprecated); if (age) { return ( <> -
+
{children} @@ -439,6 +399,7 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, + isInverted = false, }: Props) => { switch (format.display) { case ArticleDisplay.Immersive: { @@ -465,12 +426,13 @@ export const ArticleHeadline = ({ format.theme === ArticleSpecial.Labs ? labsFont : headlineFont(format), - invertedText, - css` - color: ${themePalette( - '--headline-colour', - )}; - `, + isInverted + ? [invertedText, darkBackground] + : css` + color: ${themePalette( + '--headline-colour', + )}; + `, ]} > {headlineString} @@ -496,16 +458,17 @@ export const ArticleHeadline = ({ webPublicationDateDeprecated } format={format} + snapToInverted={true} >

diff --git a/dotcom-rendering/src/components/ArticleTitle.tsx b/dotcom-rendering/src/components/ArticleTitle.tsx index de44e1eb319..2cf4d717f3a 100644 --- a/dotcom-rendering/src/components/ArticleTitle.tsx +++ b/dotcom-rendering/src/components/ArticleTitle.tsx @@ -28,17 +28,11 @@ const sectionStyles = css` `; const immersiveMargins = css` - max-width: 400px; + max-width: 500px; min-width: 200px; margin-bottom: 4px; - /* - Make sure we vertically align the title font with the body font - */ ${from.tablet} { - margin-left: 16px; - } - ${from.leftCol} { - margin-left: 25px; + margin-left: -4px; } `; diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index 0567d1dbc99..e95212f3302 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -268,7 +268,6 @@ const standfirstStyles = ({ display, design, theme }: ArticleFormat) => { `; default: return css` - max-width: 280px; ${from.tablet} { max-width: 460px; } diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index b6dd7f4a27c..3e1ccd79cb8 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -10,7 +10,6 @@ import { GalleryLayout } from './GalleryLayout'; import { HostedArticleLayout } from './HostedArticleLayout'; import { HostedGalleryLayout } from './HostedGalleryLayout'; import { HostedVideoLayout } from './HostedVideoLayout'; -import { ImmersiveLayout } from './ImmersiveLayout'; import { InteractiveLayout } from './InteractiveLayout'; import { LiveLayout } from './LiveLayout'; import { NewsletterSignupLayout } from './NewsletterSignupLayout'; @@ -57,7 +56,7 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { } default: { return ( - { } default: { return ( - ( -
- {children} -
-); - -const maxWidth = css` - ${from.desktop} { - max-width: 620px; - } -`; - -const linesMargin = css` - ${from.leftCol} { - margin-top: ${space[5]}px; - } -`; - -const stretchLines = css` - ${until.phablet} { - margin-left: -20px; - margin-right: -20px; - } - ${until.mobileLandscape} { - margin-left: -10px; - margin-right: -10px; - } -`; - -interface CommonProps { - article: ArticleDeprecated; - format: ArticleFormat; - serverTime?: number; -} - -interface WebProps extends CommonProps { - NAV: NavType; - renderingTarget: 'Web'; -} - -interface AppProps extends CommonProps { - renderingTarget: 'Apps'; -} - -const Box = ({ children }: { children: React.ReactNode }) => ( -
- {children} -
-); - -export const ImmersiveLayout = (props: WebProps | AppProps) => { - const { article, format, renderingTarget, serverTime } = props; - - const { - config: { isPaidContent, host, hasSurveyAd }, - editionId, - } = article; - const isWeb = renderingTarget === 'Web'; - const isApps = renderingTarget === 'Apps'; - - const showBodyEndSlot = - isWeb && - (parse(article.slotMachineFlags ?? '').showBodyEnd || - article.config.switches.slotBodyEnd); - - // TODO: - // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render - // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. - - const showComments = article.isCommentable && !isPaidContent; - - const mainMedia = article.mainMediaElements[0]; - - const captionText = decideMainMediaCaption(mainMedia); - - const HEADLINE_OFFSET = mainMedia ? 120 : 0; - - const { branding } = article.commercialProperties[article.editionId]; - - const contributionsServiceUrl = getContributionsServiceUrl(article); - - const isLabs = format.theme === ArticleSpecial.Labs; - - /** - We need change the height values depending on whether the labs header is there or not to keep - the headlines appearing at a consistent height between labs and non labs immersive articles. - */ - - const labsHeaderHeight = LABS_HEADER_HEIGHT; - const combinedHeight = (minHeaderHeightPx + labsHeaderHeight).toString(); - - const navAndLabsHeaderHeight = isLabs - ? `${combinedHeight}px` - : `${minHeaderHeightPx}px`; - - const hasMainMediaStyles = css` - height: calc(80vh - ${navAndLabsHeaderHeight}); - /** - 80vh is normally enough but don't let the content shrink vertically too - much just in case - */ - min-height: calc(25rem - ${navAndLabsHeaderHeight}); - ${from.desktop} { - height: calc(100vh - ${navAndLabsHeaderHeight}); - min-height: calc(31.25rem - ${navAndLabsHeaderHeight}); - } - ${from.wide} { - min-height: calc(50rem - ${navAndLabsHeaderHeight}); - } - `; - const LeftColCaption = () => ( -
- -
- ); - - const renderAds = canRenderAds(article); - - return ( - <> - {isWeb && ( - tag.id)} - sectionId={article.config.section} - contentType={article.contentType} - /> - )} - - - - {format.theme === ArticleSpecial.Labs && ( - -
- -
-
- )} - -
-
- -
- {mainMedia && ( - <> -
-
} - > - -
- -
- -
-
-
- - )} -
- - {isWeb && renderAds && hasSurveyAd && ( - - )} - -
- {isApps && renderAds && ( - - - - )} -
- - {/* Above leftCol, the Caption is controlled by Section ^^ */} - - - - - - - {format.design === ArticleDesign.PhotoEssay ? ( - <> - ) : ( - - )} - - - <> - {!mainMedia && ( -
- -
- )} - -
- - <> - {!mainMedia && ( -
- -
- )} - -
- - - - - {!!article.byline && ( - - )} - {/* Only show Listen to Article button on App landscape views */} - {isApps && ( - -
- - - -
-
- )} -
- - {format.design === ArticleDesign.PhotoEssay && - !isLabs ? ( - <> - ) : ( -
-
- {format.theme === - ArticleSpecial.Labs ? ( - - ) : ( - - )} -
-
- )} -
- {isApps ? ( - <> - - - - - - {!!article.affiliateLinksDisclaimer && ( - - )} - - - ) : ( - <> - - {!!article.affiliateLinksDisclaimer && ( - - )} - - )} -
-
- - - - {showBodyEndSlot && ( - - - - )} - - - - - -
- - <> - {mainMedia && isWeb && renderAds && ( -
- { - - } -
- )} - -
-
-
-
-
- {!isLabs && isWeb && renderAds && ( -
- -
- )} - - {article.storyPackage && ( -
- - - -
- )} - - - - - - {showComments && ( -
- -
- )} - {!isPaidContent && ( -
- - - - - -
- )} - {!isLabs && isWeb && renderAds && ( -
- -
- )} -
- - {isWeb && props.NAV.subNavSections && ( -
- - - -
- )} - - {isWeb && ( - <> -
-
-
- - - - - - - {renderAds && ( - - )} - - )} - {isApps && ( -
- - - -
- )} - - ); -}; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 3fb76dea59d..b0bf5c22d02 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -88,7 +88,7 @@ const textblockTextDark: PaletteFunction = () => 'inherit'; const headlineTextLight: PaletteFunction = ({ design, display, theme }) => { switch (display) { case ArticleDisplay.Immersive: - return sourcePalette.neutral[97]; + return sourcePalette.neutral[7]; default: { switch (design) { case ArticleDesign.Editorial: @@ -195,19 +195,8 @@ const headlineMatchTextLight: PaletteFunction = (format) => const headlineMatchTextDark: PaletteFunction = (format) => seriesTitleMatchTextDark(format); -const headlineBackgroundLight: PaletteFunction = ({ - display, - design, - theme, -}) => { +const headlineBackgroundLight: PaletteFunction = ({ display, design }) => { switch (display) { - case ArticleDisplay.Immersive: - switch (theme) { - case ArticleSpecial.SpecialReport: - return sourcePalette.specialReport[300]; - default: - return sourcePalette.neutral[7]; - } case ArticleDisplay.Showcase: case ArticleDisplay.NumberedList: case ArticleDisplay.Standard: From 1eb6a80ae8233496acb39a47572d2bdc17f0b75b Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 11 Jun 2026 15:41:01 +0100 Subject: [PATCH 20/80] Feature specific styling --- .../src/components/ArticleHeadline.tsx | 5 +- dotcom-rendering/src/components/Figure.tsx | 53 ++++++++++++++++++- dotcom-rendering/src/components/MainMedia.tsx | 1 + dotcom-rendering/src/grid.ts | 2 + 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 8beda81e2eb..96917e0fa74 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -44,7 +44,6 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; - isInverted?: boolean; }; const topPadding = css` @@ -399,8 +398,10 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, - isInverted = false, }: Props) => { + const isInverted = + format.design === ArticleDesign.Standard && + format.display === ArticleDisplay.Immersive; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index ccb113fb4b1..ab99567f7a6 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -1,6 +1,18 @@ import { css } from '@emotion/react'; -import { breakpoints, from, space, until } from '@guardian/source/foundations'; -import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; +import { + breakpoints, + from, + palette as sourcePalette, + space, + until, +} from '@guardian/source/foundations'; +import { + ArticleDesign, + ArticleDisplay, + type ArticleFormat, +} from '../lib/articleFormat'; +import { transparentColour } from '../lib/transparentColour'; +import { palette } from '../palette'; import type { FEElement, RoleType } from '../types/content'; type Props = { @@ -14,6 +26,32 @@ type Props = { isTimeline?: boolean; }; +const overlayMaskGradientStyles = (angle: string) => css` + mask-image: linear-gradient( + ${angle}, + rgb(0, 0, 0) 0%, + rgba(0, 0, 0, 0.9619) 12.5%, + rgba(0, 0, 0, 0.8536) 25%, + rgba(0, 0, 0, 0.6913) 37.5%, + rgba(0, 0, 0, 0.5) 50%, + rgba(0, 0, 0, 0.3087) 62.5%, + rgba(0, 0, 0, 0.1464) 75%, + rgba(0, 0, 0, 0.0381) 87.5%, + transparent 100% + ); +`; + +const blurStyles = css` + position: absolute; + inset: 0; + background-color: ${palette('--article-background')}; + backdrop-filter: blur(12px) brightness(0.5); + @supports not (backdrop-filter: blur(12px)) { + background-color: ${transparentColour(sourcePalette.neutral[10], 0.7)}; + } + ${overlayMaskGradientStyles('0deg')}; +`; + const roleCss = { inline: css` margin-top: ${space[3]}px; @@ -273,6 +311,17 @@ export const Figure = ({ return (
{children} + {format.display === ArticleDisplay.Immersive && ( +
+ )}
); } diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx index 871c4606715..7b980173c99 100644 --- a/dotcom-rendering/src/components/MainMedia.tsx +++ b/dotcom-rendering/src/components/MainMedia.tsx @@ -12,6 +12,7 @@ import type { Switches } from '../types/config'; import type { FEElement } from '../types/content'; const mainMedia = css` + position: relative; height: 100%; ${until.tablet} { diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index a02e5c3f861..5e7a0111455 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -159,6 +159,7 @@ const outerRules = (color?: string): string => ` &::before { grid-column: centre-column-start; transform: translateX(-${columnGap}); + z-index: 10; ${fromBreakpoint.leftCol} { grid-column: left-column-start; @@ -169,6 +170,7 @@ const outerRules = (color?: string): string => ` &::after { grid-column: right-column-end; transform: translateX(-1px); + z-index: 10; ${betweenBreakpoint.tablet.and.desktop} { grid-column: centre-column-end; From adbec3e029c1e3609cef744a9c990349657a4561 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 30 Jun 2026 16:47:05 +0100 Subject: [PATCH 21/80] Distinguish between feature and non-feature immersives --- .../src/components/ArticleHeadline.tsx | 4 ++-- dotcom-rendering/src/components/Figure.tsx | 23 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 96917e0fa74..f7951d9f145 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -400,8 +400,8 @@ export const ArticleHeadline = ({ starRating, }: Props) => { const isInverted = - format.design === ArticleDesign.Standard && - format.display === ArticleDisplay.Immersive; + format.display === ArticleDisplay.Immersive && + format.design === ArticleDesign.Standard; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index ab99567f7a6..7534acd1cd1 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -311,17 +311,18 @@ export const Figure = ({ return (
{children} - {format.display === ArticleDisplay.Immersive && ( -
- )} + {format.display === ArticleDisplay.Immersive && + format.design === ArticleDesign.Feature && ( +
+ )}
); } From 57534d97cdb961aed5914645c7999f5ba9a4c7ac Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 30 Jun 2026 18:21:43 +0100 Subject: [PATCH 22/80] Tie main media styling to article arrangement --- dotcom-rendering/src/components/Figure.tsx | 32 +++++++++---------- dotcom-rendering/src/components/MainMedia.tsx | 4 +++ dotcom-rendering/src/lib/renderElement.tsx | 4 +++ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index 7534acd1cd1..83a444ac608 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -6,11 +6,8 @@ import { space, until, } from '@guardian/source/foundations'; -import { - ArticleDesign, - ArticleDisplay, - type ArticleFormat, -} from '../lib/articleFormat'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; +import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; import { transparentColour } from '../lib/transparentColour'; import { palette } from '../palette'; import type { FEElement, RoleType } from '../types/content'; @@ -24,6 +21,7 @@ type Props = { className?: string; type?: FEElement['_type']; isTimeline?: boolean; + articleArrangement?: LayoutType; }; const overlayMaskGradientStyles = (angle: string) => css` @@ -301,6 +299,7 @@ export const Figure = ({ className = '', type, isTimeline = false, + articleArrangement = 'standard', }: Props) => { if (isMainMedia && !isTimeline) { // Don't add in-body styles for main media elements @@ -311,18 +310,17 @@ export const Figure = ({ return (
{children} - {format.display === ArticleDisplay.Immersive && - format.design === ArticleDesign.Feature && ( -
- )} + {articleArrangement === 'immersiveLandscapeFeature' && ( +
+ )}
); } diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx index 7b980173c99..3a2d30e23f5 100644 --- a/dotcom-rendering/src/components/MainMedia.tsx +++ b/dotcom-rendering/src/components/MainMedia.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/react'; import { breakpoints, space, until } from '@guardian/source/foundations'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { ArticleDesign, ArticleDisplay, @@ -96,6 +97,7 @@ type Props = { shouldHideAds: boolean; contentType?: string; contentLayout?: string; + articleArrangement?: LayoutType; }; export const MainMedia = ({ @@ -113,6 +115,7 @@ export const MainMedia = ({ shouldHideAds, contentType, contentLayout, + articleArrangement, }: Props) => { return (
@@ -135,6 +138,7 @@ export const MainMedia = ({ shouldHideAds={shouldHideAds} contentType={contentType} contentLayout={contentLayout} + articleArrangement={articleArrangement} /> ))}
diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index 5ac80d2d25f..503c3526785 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -68,6 +68,7 @@ import { } from '../components/WitnessBlockComponent'; import { YoutubeBlockComponent } from '../components/YoutubeBlockComponent.island'; import { YoutubeEmbedBlockComponent } from '../components/YoutubeEmbedBlockComponent'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { interactiveLegacyFigureClasses, isInteractive, @@ -106,6 +107,7 @@ type Props = { shouldHideAds: boolean; contentType?: string; contentLayout?: string; + articleArrangement?: LayoutType; idApiUrl?: string; }; @@ -1042,6 +1044,7 @@ export const RenderArticleElement = ({ shouldHideAds, contentType, contentLayout, + articleArrangement, idApiUrl, }: Props) => { const withUpdatedRole = updateRole(element, format); @@ -1094,6 +1097,7 @@ export const RenderArticleElement = ({ type={element._type} format={format} isTimeline={isTimeline} + articleArrangement={articleArrangement} > {el} From 2c36282ac9d658e5e11d2a77684b34297048d5ba Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 7 Jul 2026 16:15:55 +0100 Subject: [PATCH 23/80] Rebase tidying --- dotcom-rendering/src/components/ArticleHeadline.tsx | 5 ++--- .../src/layouts/StandardLayoutArticleGrid.tsx | 10 +++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index f7951d9f145..bcfd9d920b0 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -44,6 +44,7 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; + isInverted?: boolean; }; const topPadding = css` @@ -398,10 +399,8 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, + isInverted, }: Props) => { - const isInverted = - format.display === ArticleDisplay.Immersive && - format.design === ArticleDesign.Standard; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 006e0b2b1a6..edaca8e0ecd 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -236,6 +236,7 @@ export const StandardLayoutArticleGrid = ({ shouldHideAds={article.shouldHideAds} contentType={article.contentType} contentLayout={contentLayoutName} + articleArrangement={layoutType} /> @@ -391,7 +393,13 @@ export const StandardLayoutArticleGrid = ({ )} - + {/* Only show Listen to Article button on App landscape views */} {isApps && ( From e33d5d181d533fff796eda070878818641af6acc Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 16 Jul 2026 14:10:48 +0100 Subject: [PATCH 24/80] Rich review tweaks Slim nav, display bylines in the right place, grid adjustments --- .../src/components/DecideLines.tsx | 8 +++- .../src/components/Standfirst.tsx | 2 - .../src/layouts/StandardLayout.tsx | 4 +- .../src/layouts/StandardLayoutArticleGrid.tsx | 44 +++++++++++++++---- .../src/layouts/lib/articleArrangements.ts | 9 ++-- dotcom-rendering/src/lib/articleMeta.ts | 1 - 6 files changed, 50 insertions(+), 18 deletions(-) diff --git a/dotcom-rendering/src/components/DecideLines.tsx b/dotcom-rendering/src/components/DecideLines.tsx index 5d356323691..ef90047f62b 100644 --- a/dotcom-rendering/src/components/DecideLines.tsx +++ b/dotcom-rendering/src/components/DecideLines.tsx @@ -3,6 +3,7 @@ import { palette } from '@guardian/source/foundations'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import { ArticleDesign, + ArticleDisplay, type ArticleFormat, Pillar, } from '../lib/articleFormat'; @@ -41,7 +42,12 @@ const DottedLines = ({ ); export const DecideLines = ({ format, color }: Props) => { - const count = format.design === ArticleDesign.Comment ? 8 : 4; + const count = + format.design === ArticleDesign.Comment + ? 8 + : format.display === ArticleDisplay.Immersive + ? 1 + : 4; switch (format.theme) { case Pillar.Sport: diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index e95212f3302..c517894fcaa 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -213,7 +213,6 @@ const decidePadding = ({ display, design }: ArticleFormat) => { case ArticleDisplay.Immersive: return css` padding-bottom: 0; - padding-top: ${space[2]}px; ${from.tablet} { padding-bottom: 0; @@ -238,7 +237,6 @@ const decidePadding = ({ display, design }: ArticleFormat) => { case ArticleDisplay.Immersive: return css` padding-bottom: 0; - padding-top: ${space[2]}px; ${from.tablet} { padding-bottom: 0; diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 8a855b830a5..95318a5727e 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -116,7 +116,9 @@ export const StandardLayout = (props: WebProps | AppProps) => { idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} showSubNav={!isLabs && !isWorldCup2026} - showSlimNav={false} + showSlimNav={ + format.display === ArticleDisplay.Immersive + } hasPageSkinContentSelfConstrain={true} pageId={article.pageId} tagIds={article.tags.map((tag) => tag.id)} diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index edaca8e0ecd..8929bbadf4e 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -188,7 +188,10 @@ export const StandardLayoutArticleGrid = ({ isImmersivePortrait ? 4 : isImmersiveLandscape - ? 3 + ? layoutType === + 'immersiveLandscapeFeature' + ? 3 + : 4 : 1, )} } @@ -210,7 +213,7 @@ export const StandardLayoutArticleGrid = ({ - + { case ArticleDisplay.NumberedList: return true; case ArticleDisplay.Immersive: - return false; case ArticleDisplay.Showcase: case ArticleDisplay.Standard: { switch (format.design) { From eeaa9cebc47c8d855e2e98140b2b8ff28cd7daa2 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 21 Jul 2026 16:40:23 +0100 Subject: [PATCH 25/80] Caption grid area and meta section --- .../src/components/ArticleMeta.web.tsx | 23 +++++++ dotcom-rendering/src/components/Caption.tsx | 32 ++-------- .../src/layouts/StandardLayoutArticleGrid.tsx | 36 +++++++++-- .../src/layouts/lib/articleArrangements.ts | 60 +++++++++++++------ dotcom-rendering/src/lib/articleMeta.test.ts | 8 --- 5 files changed, 100 insertions(+), 59 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index 5abd00eede4..812aad98dc5 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/react'; import { isUndefined } from '@guardian/libs'; import { between, from, space, until } from '@guardian/source/foundations'; +import { Hide } from '@guardian/source/react-components'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import type { CSSProperties } from 'react'; import type { FEArticle } from '../frontend/feArticle'; @@ -20,6 +21,7 @@ import { } from '../lib/articleMeta'; import { getAudioData } from '../lib/audio-data'; import { getSoleContributor } from '../lib/byline'; +import { decideMainMediaCaption } from '../lib/decide-caption'; import { palette as themePalette } from '../palette'; import { hasPreferredSourceButton } from '../preferredSource'; import type { Branding as BrandingType } from '../types/branding'; @@ -27,6 +29,7 @@ import type { FEElement } from '../types/content'; import type { TagType } from '../types/tag'; import { Avatar } from './Avatar'; import { Branding } from './Branding.island'; +import { Caption } from './Caption'; import { CommentCount } from './CommentCount.island'; import { useConfig } from './ConfigContext'; import { Contributor } from './Contributor'; @@ -479,6 +482,26 @@ export const ArticleMeta = ({

{showPreferredSource ? : null} + {format.display === ArticleDisplay.Immersive && + mainMediaElements?.[0] && ( + +
+ +
+
+ )} ); diff --git a/dotcom-rendering/src/components/Caption.tsx b/dotcom-rendering/src/components/Caption.tsx index 536aea4105e..b708a9c0129 100644 --- a/dotcom-rendering/src/components/Caption.tsx +++ b/dotcom-rendering/src/components/Caption.tsx @@ -34,7 +34,6 @@ type Props = { }; type IconProps = { - format: ArticleFormat; isMainMedia?: boolean; }; @@ -161,12 +160,6 @@ const bigLeftMargin = css` } `; -const hideIconBelowLeftCol = css` - ${until.leftCol} { - display: none; - } -`; - const pictureRatio = (13 / 18) * 100; const videoRatio = (23 / 36) * 100; @@ -251,30 +244,17 @@ const galleryStyles = css` } `; -const CameraIcon = ({ format, isMainMedia }: IconProps) => { +const CameraIcon = ({ isMainMedia }: IconProps) => { return ( - + ); }; -const VideoIcon = ({ format, isMainMedia }: IconProps) => { +const VideoIcon = ({ isMainMedia }: IconProps) => { return ( - + ); @@ -325,9 +305,9 @@ export const Caption = ({ data-spacefinder-role="inline" > {mediaType === 'YoutubeVideo' || mediaType === 'SelfHostedVideo' ? ( - + ) : ( - + )} {!!captionText && ( + {isImmersive && ( + + + + + + )} { ...standardFormat, display: ArticleDisplay.NumberedList, }; - const immersive = { - ...standardFormat, - display: ArticleDisplay.Immersive, - }; it('should return true if Standard display and Standard design', () => { expect(shouldShowContributor(standardFormat)).toBe(true); @@ -47,8 +43,4 @@ describe('shouldShowContributor', () => { it('should return true if Numbered list display', () => { expect(shouldShowContributor(numberedList)).toBe(true); }); - - it('should return false if Immersive display', () => { - expect(shouldShowContributor(immersive)).toBe(false); - }); }); From b0df0ebf448c45dac2139ffecafbb5f5e0c4a0e4 Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Tue, 28 Jul 2026 14:56:39 +0100 Subject: [PATCH 26/80] make layoutType optional for ArticleMeta web and app components --- dotcom-rendering/src/components/ArticleMeta.apps.tsx | 2 +- dotcom-rendering/src/components/ArticleMeta.web.tsx | 2 +- dotcom-rendering/src/layouts/StandardLayout.tsx | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.apps.tsx b/dotcom-rendering/src/components/ArticleMeta.apps.tsx index 9acf40bdfbe..0eae4a9da8c 100644 --- a/dotcom-rendering/src/components/ArticleMeta.apps.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.apps.tsx @@ -37,7 +37,7 @@ import { PodcastMeta } from './PodcastMeta'; type Props = { format: ArticleFormat; - layoutType: LayoutType; + layoutType?: LayoutType; byline?: string; tags: TagType[]; primaryDateline: string; diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index 2a680ef125e..122e7dd03b2 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -41,7 +41,7 @@ import { TimeDateline } from './TimeDateline'; type Props = { format: ArticleFormat; - layoutType: LayoutType; + layoutType?: LayoutType; pageId: string; webTitle: string; byline?: string; diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index a333066a1ab..c7800eba299 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -1,6 +1,5 @@ import { css } from '@emotion/react'; import { log } from '@guardian/libs'; -import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import { from, palette as sourcePalette, @@ -8,6 +7,7 @@ import { until, } from '@guardian/source/foundations'; import { Hide } from '@guardian/source/react-components'; +import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import { AdPortals } from '../components/AdPortals.island'; import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; import { AffiliateDisclaimer } from '../components/AffiliateDisclaimer'; @@ -475,6 +475,7 @@ export const StandardLayout = (props: WebProps | AppProps) => { { Date: Tue, 28 Jul 2026 15:01:17 +0100 Subject: [PATCH 27/80] make layoutType optional for ArticleTitle and children --- dotcom-rendering/src/components/ArticleTitle.tsx | 2 +- dotcom-rendering/src/components/SeriesSectionLink.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleTitle.tsx b/dotcom-rendering/src/components/ArticleTitle.tsx index d5600e0966e..e78a65319ea 100644 --- a/dotcom-rendering/src/components/ArticleTitle.tsx +++ b/dotcom-rendering/src/components/ArticleTitle.tsx @@ -12,7 +12,7 @@ import { SeriesSectionLink } from './SeriesSectionLink'; type Props = { format: ArticleFormat; - layoutType: LayoutType; + layoutType?: LayoutType; sectionLabel: string; tags: TagType[]; sectionUrl: string; diff --git a/dotcom-rendering/src/components/SeriesSectionLink.tsx b/dotcom-rendering/src/components/SeriesSectionLink.tsx index 5b75619dd45..bd3caa2692f 100644 --- a/dotcom-rendering/src/components/SeriesSectionLink.tsx +++ b/dotcom-rendering/src/components/SeriesSectionLink.tsx @@ -28,7 +28,7 @@ import { PulsingDot } from './PulsingDot.island'; type Props = { format: ArticleFormat; - layoutType: LayoutType; + layoutType?: LayoutType; tags: TagType[]; sectionLabel: string; sectionUrl: string; From 7c88363f5ec5b0ac80a942ca3e76369edd46d6aa Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Tue, 28 Jul 2026 15:49:41 +0100 Subject: [PATCH 28/80] fix issues post merge --- .../src/layouts/StandardLayout.tsx | 20 +++---------------- .../src/layouts/StandardLayoutArticleGrid.tsx | 3 ++- dotcom-rendering/src/lib/articleMeta.test.ts | 5 +++++ 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index a7346a23743..0635b64a37d 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -52,23 +52,6 @@ interface AppProps extends Props { renderingTarget: 'Apps'; } -/** - * Works out the orientation of an image from its Guardian media URL, which - * encodes the crop dimensions in the path (e.g. `/1000_600_800_480/`). - * Falls back to 'landscape' if the URL doesn't match the expected pattern. - */ -const getImageOrientation = ( - url: string, -): 'portrait' | 'landscape' | 'square' => { - const match = url.match(/\/\d+_\d+_(\d+)_(\d+)\/\d+\.\w+$/); - if (!match) return 'landscape'; - const [, width, height] = match.map(Number); - if (width == null || height == null) return 'landscape'; - if (height > width) return 'portrait'; - if (width > height) return 'landscape'; - return 'square'; -}; - export const StandardLayout = (props: WebProps | AppProps) => { const { article, format, renderingTarget, serverTime } = props; const { @@ -79,6 +62,8 @@ export const StandardLayout = (props: WebProps | AppProps) => { const isWeb = renderingTarget === 'Web'; const isApps = renderingTarget === 'Apps'; + const contentLayoutName = `${ArticleDisplay[format.display]}Layout`; + // TODO: // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. @@ -187,6 +172,7 @@ export const StandardLayout = (props: WebProps | AppProps) => {
diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index a4b207497ed..9bd605900a7 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -98,10 +98,12 @@ const GridItem = ({ export const StandardLayoutArticleGrid = ({ article, + contentLayoutName, format, renderingTarget, }: { article: ArticleDeprecated; + contentLayoutName: string; format: ArticleFormat; renderingTarget: RenderingTarget; }) => { @@ -162,7 +164,6 @@ export const StandardLayoutArticleGrid = ({ isVideo, isShowcase, }); - const contentLayoutName = `${ArticleDisplay[format.display]}Layout`; const isImmersivePortrait = layoutType === 'immersivePortraitDefault' || diff --git a/dotcom-rendering/src/lib/articleMeta.test.ts b/dotcom-rendering/src/lib/articleMeta.test.ts index 249adcf6356..49c7206577d 100644 --- a/dotcom-rendering/src/lib/articleMeta.test.ts +++ b/dotcom-rendering/src/lib/articleMeta.test.ts @@ -24,6 +24,11 @@ describe('shouldShowContributor', () => { display: ArticleDisplay.NumberedList, }; + const immersive = { + ...standardFormat, + display: ArticleDisplay.Immersive, + }; + const immersivePortraitDefault = { ...immersive, layoutType: 'immersivePortraitDefault', From 6739f4c6dcac9f6e08be40f878e52168f6ff84ab Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Tue, 28 Jul 2026 16:20:09 +0100 Subject: [PATCH 29/80] fix duplicate prop in ArticleHeadline component --- dotcom-rendering/src/components/ArticleHeadline.tsx | 8 +++++--- .../src/layouts/StandardLayoutArticleGrid.tsx | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 5529b52715d..d93e6cea507 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -18,6 +18,7 @@ import { until, } from '@guardian/source/foundations'; import { grid } from '../../src/grid'; +import { type LayoutType } from '../layouts/lib/articleArrangements'; import { interactiveLegacyClasses } from '../layouts/lib/interactiveLegacyStyling'; import { getAgeWarning } from '../lib/age-warning'; import { @@ -38,13 +39,13 @@ import { StarRating } from './StarRating/StarRating'; type Props = { headlineString: string; format: ArticleFormat; + layoutType?: LayoutType; byline?: string; tags: TagType[]; webPublicationDateDeprecated: string; hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; - isInverted?: boolean; }; const topPadding = css` @@ -393,17 +394,18 @@ const galleryStyles = css` export const ArticleHeadline = ({ headlineString, format, + layoutType, tags, byline, webPublicationDateDeprecated, hasAvatar, isMatch, starRating, - isInverted, }: Props) => { const isInverted = format.display === ArticleDisplay.Immersive && - format.design === ArticleDesign.Standard; + format.design === ArticleDesign.Standard && + layoutType === 'immersiveLandscapeDefault'; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 9bd605900a7..1855ea52306 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -297,6 +297,7 @@ export const StandardLayoutArticleGrid = ({ > Date: Tue, 19 May 2026 15:25:40 +0100 Subject: [PATCH 30/80] Rough first pass --- .../src/layouts/StandardLayoutArticleGrid.tsx | 167 ++++++++++++--- .../src/layouts/lib/articleArrangements.ts | 197 +++++++++++++++++- 2 files changed, 336 insertions(+), 28 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 84fb7928e6d..21a5f4cdb17 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -22,6 +22,7 @@ import { SlotBodyEnd } from '../components/SlotBodyEnd.island'; import { Standfirst } from '../components/Standfirst'; import { SubMeta } from '../components/SubMeta'; import { grid } from '../grid'; +import { getAgeWarning } from '../lib/age-warning'; import { ArticleDesign, ArticleDisplay, @@ -36,6 +37,7 @@ import type { ArticleDeprecated } from '../types/article'; import type { RenderingTarget } from '../types/renderingTarget'; import { type Area, + getLayoutType, gridItemCss, type LayoutType, } from './lib/articleArrangements'; @@ -59,6 +61,23 @@ interface GridItemProps { children: React.ReactNode; } +/** + * Works out the orientation of an image from its Guardian media URL, which + * encodes the crop dimensions in the path (e.g. `/1000_600_800_480/`). + * Falls back to 'landscape' if the URL doesn't match the expected pattern. + */ +const getImageOrientation = ( + url: string, +): 'portrait' | 'landscape' | 'square' => { + const match = url.match(/\/\d+_\d+_(\d+)_(\d+)\/\d+\.\w+$/); + if (!match) return 'landscape'; + const [, width, height] = match.map(Number); + if (width == null || height == null) return 'landscape'; + if (height > width) return 'portrait'; + if (width > height) return 'landscape'; + return 'square'; +}; + const GridItem = ({ area, layoutType, @@ -110,6 +129,8 @@ export const StandardLayoutArticleGrid = ({ format.design === ArticleDesign.Video || format.design === ArticleDesign.Audio; const isShowcase = format.display === ArticleDisplay.Showcase; + const isImmersive = format.display === ArticleDisplay.Immersive; + const isFeature = format.design === ArticleDesign.Feature; const footballMatchUrl = article.matchType === 'FootballMatchType' @@ -119,11 +140,36 @@ export const StandardLayoutArticleGrid = ({ const isFootballMatchReport = format.design === ArticleDesign.MatchReport && !!footballMatchUrl; - const layoutType: LayoutType = isMedia - ? 'media' - : isShowcase - ? 'showcase' - : 'standard'; + const firstMainMediaElement = article.mainMediaElements[0]; + const mainMediaUrl: string | undefined = + firstMainMediaElement?._type === + 'model.dotcomrendering.pageElements.ImageBlockElement' + ? firstMainMediaElement.media.allImages[0]?.url + : undefined; + + const mainMediaOrientation = + mainMediaUrl != null ? getImageOrientation(mainMediaUrl) : 'landscape'; + + const layoutType = getLayoutType({ + isImmersive, + isFeature, + orientation: mainMediaOrientation, + isMedia, + isShowcase, + }); + const contentLayoutName = `${ArticleDisplay[format.display]}Layout`; + + const isImmersivePortrait = + layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature'; + const isImmersiveLandscape = + layoutType === 'immersiveLandscapeDefault' || + layoutType === 'immersiveLandscapeFeature'; + + const ageWarning = getAgeWarning( + article.tags, + article.webPublicationDateDeprecated, + ); return (
- + - + - + - - {format.design !== ArticleDesign.Audio && ( -
- {isWeb && - format.theme === ArticleSpecial.Labs && - format.design !== ArticleDesign.Video ? ( - - ) : ( - - )} -
- )} + + {format.design !== ArticleDesign.Audio && + layoutType !== 'immersivePortraitDefault' && ( +
+ {isWeb && + format.theme === ArticleSpecial.Labs && + format.design !== ArticleDesign.Video ? ( + + ) : ( + + )} +
+ )} {isApps ? ( <> @@ -263,11 +378,11 @@ export const StandardLayoutArticleGrid = ({ secondaryDateline={ article.webPublicationSecondaryDateDisplay } + webPublicationDate={article.webPublicationDate} isCommentable={article.isCommentable} discussionApiUrl={article.config.discussionApiUrl} shortUrlId={article.config.shortUrlId} mainMediaElements={article.mainMediaElements} - webPublicationDate={article.webPublicationDate} /> {!!article.affiliateLinksDisclaimer && ( diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 2cf5fb6843f..636af9fac97 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -2,7 +2,14 @@ import { css, type SerializedStyles } from '@emotion/react'; import { from, until } from '@guardian/source/foundations'; import { grid } from '../../grid'; -export type LayoutType = 'standard' | 'showcase' | 'media'; +export type LayoutType = + | 'standard' + | 'showcase' + | 'media' + | 'immersiveLandscapeDefault' + | 'immersiveLandscapeFeature' + | 'immersivePortraitDefault' + | 'immersivePortraitFeature'; export type Area = | 'title' @@ -13,13 +20,14 @@ export type Area = | 'body' | 'right-column'; -type Breakpoint = 'mobile' | 'tablet' | 'desktop' | 'leftCol'; +type Breakpoint = 'mobile' | 'tablet' | 'desktop' | 'leftCol' | 'wide'; const breakpointQueries: Record = { mobile: until.tablet, tablet: from.tablet, desktop: from.desktop, leftCol: from.leftCol, + wide: from.wide, }; // Raw CSS overrides per area per breakpoint. Entries are only needed when an area @@ -149,10 +157,160 @@ const mediaCss: LayoutCssMap = { }, }; +const immersivePortraitDefaultCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: `grid-row: 1; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 1; ${grid.between('left-column-start', 9)};`, + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: `grid-row: 2; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 2; ${grid.between('left-column-start', 9)};`, + wide: `grid-row: 2; ${grid.between('left-column-start', 10)};`, + }, + media: { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: `grid-row: 1 / span 4; ${grid.between(8, 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between(9, 'right-column-end')};`, + wide: `grid-row: 1 / span 3; ${grid.between(10, 'right-column-end')};`, + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + desktop: `grid-row: 3; ${grid.between('centre-column-start', 7)};`, + leftCol: `grid-row: 3; ${grid.between('centre-column-start', 8)};`, + wide: `grid-row: 3; ${grid.between('centre-column-start', 9)};`, + }, + meta: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: `grid-row: 4; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 3; ${grid.column.left};`, + }, + body: { + mobile: 'grid-row: 6;', + }, + 'right-column': { + desktop: `grid-row: 5; ${grid.column.right};`, + leftCol: `grid-row: 4; ${grid.column.right};`, + }, +}; + +const immersivePortraitFeatureCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: `grid-row: 1; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 1; ${grid.between('left-column-start', 9)};`, + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: `grid-row: 2; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 2; ${grid.between('left-column-start', 9)};`, + wide: `grid-row: 2; ${grid.between('left-column-start', 10)};`, + }, + media: { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: `grid-row: 1 / span 4; ${grid.between(8, 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between(9, 'right-column-end')};`, + wide: `grid-row: 1 / span 3; ${grid.between(10, 'right-column-end')};`, + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + desktop: `grid-row: 3; ${grid.between('centre-column-start', 7)};`, + leftCol: `grid-row: 3; ${grid.between('centre-column-start', 8)};`, + wide: `grid-row: 3; ${grid.between('centre-column-start', 9)};`, + }, + meta: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: `grid-row: 4; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 3; ${grid.column.left};`, + }, + body: { + mobile: 'grid-row: 6;', + }, + 'right-column': { + desktop: `grid-row: 5; ${grid.column.right};`, + leftCol: `grid-row: 4; ${grid.column.right};`, + }, +}; + +const immersiveLandscapeDefaultCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: 'grid-row: 2;', + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: 'grid-row: 3 / span 2;', + }, + media: { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: `grid-row: 1 / span 3; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + desktop: 'grid-row: 5;', + }, + meta: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: `grid-row: 6; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 6 / span 2; ${grid.column.left};`, + }, + body: { + mobile: 'grid-row: 6;', + }, + 'right-column': { + desktop: `grid-row: 5 / span 3; ${grid.column.right};`, + }, +}; + +const immersiveLandscapeFeatureCss: LayoutCssMap = { + title: { + desktop: 'grid-row: 2;', + }, + headline: { + desktop: 'grid-row: 3 / span 2;', + }, + media: { + mobile: `${grid.column.all}`, + desktop: `grid-row: 1 / span 3; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, + }, + standfirst: { + desktop: 'grid-row: 5;', + }, + meta: { + desktop: `grid-row: 6; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 6 / span 2; ${grid.column.left};`, + }, + 'right-column': { + desktop: `grid-row: 5 / span 3; ${grid.column.right};`, + }, +}; + const layoutCssMaps: Record = { standard: standardCss, showcase: showcaseCss, media: mediaCss, + immersiveLandscapeDefault: immersiveLandscapeDefaultCss, + immersiveLandscapeFeature: immersiveLandscapeFeatureCss, + immersivePortraitDefault: immersivePortraitDefaultCss, + immersivePortraitFeature: immersivePortraitFeatureCss, }; /** @@ -192,3 +350,38 @@ export const gridItemCss = ( ${breakpointCss} `; }; + +/** + * Determines which {@link LayoutType} to render. Immersive layouts are + * split by orientation (portrait vs. landscape/square) and by whether the + * format is a Feature, since each combination has a distinct grid + * arrangement. Non-immersive formats fall back to media/showcase/standard. + */ +export const getLayoutType = ({ + isImmersive, + isFeature, + orientation, + isMedia, + isShowcase, +}: { + isImmersive: boolean; + isFeature: boolean; + orientation: 'portrait' | 'landscape' | 'square'; + isMedia: boolean; + isShowcase: boolean; +}): LayoutType => { + if (isImmersive) { + if (orientation === 'portrait') { + return isFeature + ? 'immersivePortraitFeature' + : 'immersivePortraitDefault'; + } + // Square images are treated the same as landscape for immersive layouts. + return isFeature + ? 'immersiveLandscapeFeature' + : 'immersiveLandscapeDefault'; + } + if (isMedia) return 'media'; + if (isShowcase) return 'showcase'; + return 'standard'; +}; From d693896512b066a30e8d26a72a6871c9dc4ada90 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 26 May 2026 16:50:39 +0100 Subject: [PATCH 31/80] Rough implementation of portrait/landscape variants --- .../src/components/ArticleHeadline.tsx | 151 +-- .../src/components/ArticleTitle.tsx | 10 +- .../src/components/Standfirst.tsx | 1 - dotcom-rendering/src/layouts/DecideLayout.tsx | 5 +- .../src/layouts/ImmersiveLayout.tsx | 1022 ----------------- dotcom-rendering/src/paletteDeclarations.ts | 15 +- 6 files changed, 62 insertions(+), 1142 deletions(-) delete mode 100644 dotcom-rendering/src/layouts/ImmersiveLayout.tsx diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 3b0c141fc86..8beda81e2eb 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -27,7 +27,6 @@ import { ArticleSpecial, Pillar, } from '../lib/articleFormat'; -import { getZIndex } from '../lib/getZIndex'; import { palette as themePalette } from '../palette'; import type { StarRating as Rating } from '../types/content'; import type { TagType } from '../types/tag'; @@ -45,6 +44,7 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; + isInverted?: boolean; }; const topPadding = css` @@ -214,30 +214,20 @@ const invertedStyles = css` box-decoration-break: clone; `; -const immersiveStyles = css` - min-height: 112px; - padding-bottom: ${space[6]}px; - padding-left: ${space[1]}px; - - ${from.mobileLandscape} { - padding-left: ${space[3]}px; - } - - ${from.tablet} { - padding-left: ${space[1]}px; - } - - margin-right: ${space[5]}px; -`; - const darkBackground = css` background-color: ${themePalette('--headline-background')}; `; const invertedText = css` - white-space: pre-wrap; - padding-bottom: ${space[1]}px; - padding-right: ${space[1]}px; + ${from.desktop} { + color: white; + background-color: black; + white-space: pre-wrap; + padding-bottom: ${space[1]}px; + padding-right: ${space[1]}px; + margin-left: -10px; + padding-left: 10px; + } `; const maxWidth = css` @@ -255,35 +245,6 @@ const invertedWrapper = css` margin-left: 6px; `; -const immersiveWrapper = css` - /* - Make sure we vertically align the headline font with the body font - */ - margin-left: 6px; - - ${from.tablet} { - margin-left: 16px; - } - - ${from.leftCol} { - margin-left: 25px; - } - - /* - We need this grow to ensure the headline fills the main content column - */ - flex-grow: 1; - /* - This z-index is what ensures the headline text shows above the pseudo black - box that extends the black background to the right - */ - z-index: ${getZIndex('articleHeadline')}; - - ${until.mobileLandscape} { - margin-right: 40px; - } -`; - // Due to MainMedia using position: relative, this seems to effect the rendering order // To mitigate we use z-index // TODO: find a cleaner solution @@ -292,59 +253,58 @@ const zIndex = css` `; const ageWarningMargins = (format: ArticleFormat) => { - if (format.design === ArticleDesign.Gallery) { + if ( + format.design === ArticleDesign.Gallery || + format.display === ArticleDisplay.Immersive + ) { return ''; } - return format.display === ArticleDisplay.Immersive - ? css` - margin-left: 0px; - margin-bottom: 0px; - - ${from.tablet} { - margin-left: 10px; - } + return css` + margin-top: 12px; + margin-left: -10px; + margin-bottom: 6px; - ${from.leftCol} { - margin-left: 20px; - } - ` - : css` - margin-top: 12px; - margin-left: -10px; - margin-bottom: 6px; - - ${from.tablet} { - margin-left: -20px; - } + ${from.tablet} { + margin-left: -20px; + } - ${from.leftCol} { - margin-left: -10px; - margin-top: 0; - } - `; + ${from.leftCol} { + margin-left: -10px; + margin-top: 0; + } + `; }; -const backgroundStyles = css` - background-color: ${themePalette('--age-warning-wrapper-background')}; -`; - const WithAgeWarning = ({ tags, webPublicationDateDeprecated, format, children, + snapToInverted = false, }: { tags: TagType[]; webPublicationDateDeprecated: string; format: ArticleFormat; children: React.ReactNode; + snapToInverted?: boolean; }) => { const age = getAgeWarning(tags, webPublicationDateDeprecated); if (age) { return ( <> -
+
{children} @@ -439,6 +399,7 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, + isInverted = false, }: Props) => { switch (format.display) { case ArticleDisplay.Immersive: { @@ -465,12 +426,13 @@ export const ArticleHeadline = ({ format.theme === ArticleSpecial.Labs ? labsFont : headlineFont(format), - invertedText, - css` - color: ${themePalette( - '--headline-colour', - )}; - `, + isInverted + ? [invertedText, darkBackground] + : css` + color: ${themePalette( + '--headline-colour', + )}; + `, ]} > {headlineString} @@ -496,16 +458,17 @@ export const ArticleHeadline = ({ webPublicationDateDeprecated } format={format} + snapToInverted={true} >

diff --git a/dotcom-rendering/src/components/ArticleTitle.tsx b/dotcom-rendering/src/components/ArticleTitle.tsx index de44e1eb319..2cf4d717f3a 100644 --- a/dotcom-rendering/src/components/ArticleTitle.tsx +++ b/dotcom-rendering/src/components/ArticleTitle.tsx @@ -28,17 +28,11 @@ const sectionStyles = css` `; const immersiveMargins = css` - max-width: 400px; + max-width: 500px; min-width: 200px; margin-bottom: 4px; - /* - Make sure we vertically align the title font with the body font - */ ${from.tablet} { - margin-left: 16px; - } - ${from.leftCol} { - margin-left: 25px; + margin-left: -4px; } `; diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index 0567d1dbc99..e95212f3302 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -268,7 +268,6 @@ const standfirstStyles = ({ display, design, theme }: ArticleFormat) => { `; default: return css` - max-width: 280px; ${from.tablet} { max-width: 460px; } diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index 77803138313..3d828bdc45b 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -9,7 +9,6 @@ import { GalleryLayout } from './GalleryLayout'; import { HostedArticleLayout } from './HostedArticleLayout'; import { HostedGalleryLayout } from './HostedGalleryLayout'; import { HostedVideoLayout } from './HostedVideoLayout'; -import { ImmersiveLayout } from './ImmersiveLayout'; import { InteractiveLayout } from './InteractiveLayout'; import { LiveLayout } from './LiveLayout'; import { NewsletterSignupLayout } from './NewsletterSignupLayout'; @@ -55,7 +54,7 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { } default: { return ( - { } default: { return ( - ( -
- {children} -
-); - -const maxWidth = css` - ${from.desktop} { - max-width: 620px; - } -`; - -const linesMargin = css` - ${from.leftCol} { - margin-top: ${space[5]}px; - } -`; - -const stretchLines = css` - ${until.phablet} { - margin-left: -20px; - margin-right: -20px; - } - ${until.mobileLandscape} { - margin-left: -10px; - margin-right: -10px; - } -`; - -interface CommonProps { - article: ArticleDeprecated; - format: ArticleFormat; - serverTime?: number; -} - -interface WebProps extends CommonProps { - NAV: NavType; - renderingTarget: 'Web'; -} - -interface AppProps extends CommonProps { - renderingTarget: 'Apps'; -} - -const Box = ({ children }: { children: React.ReactNode }) => ( -
- {children} -
-); - -export const ImmersiveLayout = (props: WebProps | AppProps) => { - const { article, format, renderingTarget, serverTime } = props; - - const { - config: { isPaidContent, host, hasSurveyAd }, - editionId, - } = article; - const isWeb = renderingTarget === 'Web'; - const isApps = renderingTarget === 'Apps'; - - const showBodyEndSlot = - isWeb && - (parse(article.slotMachineFlags ?? '').showBodyEnd || - article.config.switches.slotBodyEnd); - - // TODO: - // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render - // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. - - const showComments = article.isCommentable && !isPaidContent; - - const mainMedia = article.mainMediaElements[0]; - - const captionText = decideMainMediaCaption(mainMedia); - - const HEADLINE_OFFSET = mainMedia ? 120 : 0; - - const { branding } = article.commercialProperties[article.editionId]; - - const contributionsServiceUrl = getContributionsServiceUrl(article); - - const isLabs = format.theme === ArticleSpecial.Labs; - - /** - We need change the height values depending on whether the labs header is there or not to keep - the headlines appearing at a consistent height between labs and non labs immersive articles. - */ - - const labsHeaderHeight = LABS_HEADER_HEIGHT; - const combinedHeight = (minHeaderHeightPx + labsHeaderHeight).toString(); - - const navAndLabsHeaderHeight = isLabs - ? `${combinedHeight}px` - : `${minHeaderHeightPx}px`; - - const hasMainMediaStyles = css` - height: calc(80vh - ${navAndLabsHeaderHeight}); - /** - 80vh is normally enough but don't let the content shrink vertically too - much just in case - */ - min-height: calc(25rem - ${navAndLabsHeaderHeight}); - ${from.desktop} { - height: calc(100vh - ${navAndLabsHeaderHeight}); - min-height: calc(31.25rem - ${navAndLabsHeaderHeight}); - } - ${from.wide} { - min-height: calc(50rem - ${navAndLabsHeaderHeight}); - } - `; - const LeftColCaption = () => ( -
- -
- ); - - const renderAds = canRenderAds(article); - - return ( - <> - {isWeb && ( - tag.id)} - sectionId={article.config.section} - contentType={article.contentType} - /> - )} - - - - {format.theme === ArticleSpecial.Labs && ( - -
- -
-
- )} - -
-
- -
- {mainMedia && ( - <> -
-
} - > - -
- -
- -
-
-
- - )} -
- - {isWeb && renderAds && hasSurveyAd && ( - - )} - -
- {isApps && renderAds && ( - - - - )} -
- - {/* Above leftCol, the Caption is controlled by Section ^^ */} - - - - - - - {format.design === ArticleDesign.PhotoEssay ? ( - <> - ) : ( - - )} - - - <> - {!mainMedia && ( -
- -
- )} - -
- - <> - {!mainMedia && ( -
- -
- )} - -
- - - - - {!!article.byline && ( - - )} - {/* Only show Listen to Article button on App landscape views */} - {isApps && ( - -
- - - -
-
- )} -
- - {format.design === ArticleDesign.PhotoEssay && - !isLabs ? ( - <> - ) : ( -
-
- {format.theme === - ArticleSpecial.Labs ? ( - - ) : ( - - )} -
-
- )} -
- {isApps ? ( - <> - - - - - - {!!article.affiliateLinksDisclaimer && ( - - )} - - - ) : ( - <> - - {!!article.affiliateLinksDisclaimer && ( - - )} - - )} -
-
- - - - {showBodyEndSlot && ( - - - - )} - - - - - -
- - <> - {mainMedia && isWeb && renderAds && ( -
- { - - } -
- )} - -
-
-
-
-
- {!isLabs && isWeb && renderAds && ( -
- -
- )} - - {article.storyPackage && ( -
- - - -
- )} - - - - - - {showComments && ( -
- -
- )} - {!isPaidContent && ( -
- - - - - -
- )} - {!isLabs && isWeb && renderAds && ( -
- -
- )} -
- - {isWeb && props.NAV.subNavSections && ( -
- - - -
- )} - - {isWeb && ( - <> -
-
-
- - - - - - - {renderAds && ( - - )} - - )} - {isApps && ( -
- - - -
- )} - - ); -}; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 3fb76dea59d..b0bf5c22d02 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -88,7 +88,7 @@ const textblockTextDark: PaletteFunction = () => 'inherit'; const headlineTextLight: PaletteFunction = ({ design, display, theme }) => { switch (display) { case ArticleDisplay.Immersive: - return sourcePalette.neutral[97]; + return sourcePalette.neutral[7]; default: { switch (design) { case ArticleDesign.Editorial: @@ -195,19 +195,8 @@ const headlineMatchTextLight: PaletteFunction = (format) => const headlineMatchTextDark: PaletteFunction = (format) => seriesTitleMatchTextDark(format); -const headlineBackgroundLight: PaletteFunction = ({ - display, - design, - theme, -}) => { +const headlineBackgroundLight: PaletteFunction = ({ display, design }) => { switch (display) { - case ArticleDisplay.Immersive: - switch (theme) { - case ArticleSpecial.SpecialReport: - return sourcePalette.specialReport[300]; - default: - return sourcePalette.neutral[7]; - } case ArticleDisplay.Showcase: case ArticleDisplay.NumberedList: case ArticleDisplay.Standard: From 249ea42d66c6441380d5ad4b6ba767bb76e2009e Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 11 Jun 2026 15:41:01 +0100 Subject: [PATCH 32/80] Feature specific styling --- .../src/components/ArticleHeadline.tsx | 5 +- dotcom-rendering/src/components/Figure.tsx | 53 ++++++++++++++++++- dotcom-rendering/src/components/MainMedia.tsx | 1 + dotcom-rendering/src/grid.ts | 2 + 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 8beda81e2eb..96917e0fa74 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -44,7 +44,6 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; - isInverted?: boolean; }; const topPadding = css` @@ -399,8 +398,10 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, - isInverted = false, }: Props) => { + const isInverted = + format.design === ArticleDesign.Standard && + format.display === ArticleDisplay.Immersive; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index ccb113fb4b1..ab99567f7a6 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -1,6 +1,18 @@ import { css } from '@emotion/react'; -import { breakpoints, from, space, until } from '@guardian/source/foundations'; -import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; +import { + breakpoints, + from, + palette as sourcePalette, + space, + until, +} from '@guardian/source/foundations'; +import { + ArticleDesign, + ArticleDisplay, + type ArticleFormat, +} from '../lib/articleFormat'; +import { transparentColour } from '../lib/transparentColour'; +import { palette } from '../palette'; import type { FEElement, RoleType } from '../types/content'; type Props = { @@ -14,6 +26,32 @@ type Props = { isTimeline?: boolean; }; +const overlayMaskGradientStyles = (angle: string) => css` + mask-image: linear-gradient( + ${angle}, + rgb(0, 0, 0) 0%, + rgba(0, 0, 0, 0.9619) 12.5%, + rgba(0, 0, 0, 0.8536) 25%, + rgba(0, 0, 0, 0.6913) 37.5%, + rgba(0, 0, 0, 0.5) 50%, + rgba(0, 0, 0, 0.3087) 62.5%, + rgba(0, 0, 0, 0.1464) 75%, + rgba(0, 0, 0, 0.0381) 87.5%, + transparent 100% + ); +`; + +const blurStyles = css` + position: absolute; + inset: 0; + background-color: ${palette('--article-background')}; + backdrop-filter: blur(12px) brightness(0.5); + @supports not (backdrop-filter: blur(12px)) { + background-color: ${transparentColour(sourcePalette.neutral[10], 0.7)}; + } + ${overlayMaskGradientStyles('0deg')}; +`; + const roleCss = { inline: css` margin-top: ${space[3]}px; @@ -273,6 +311,17 @@ export const Figure = ({ return (
{children} + {format.display === ArticleDisplay.Immersive && ( +
+ )}
); } diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx index 871c4606715..7b980173c99 100644 --- a/dotcom-rendering/src/components/MainMedia.tsx +++ b/dotcom-rendering/src/components/MainMedia.tsx @@ -12,6 +12,7 @@ import type { Switches } from '../types/config'; import type { FEElement } from '../types/content'; const mainMedia = css` + position: relative; height: 100%; ${until.tablet} { diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index a02e5c3f861..5e7a0111455 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -159,6 +159,7 @@ const outerRules = (color?: string): string => ` &::before { grid-column: centre-column-start; transform: translateX(-${columnGap}); + z-index: 10; ${fromBreakpoint.leftCol} { grid-column: left-column-start; @@ -169,6 +170,7 @@ const outerRules = (color?: string): string => ` &::after { grid-column: right-column-end; transform: translateX(-1px); + z-index: 10; ${betweenBreakpoint.tablet.and.desktop} { grid-column: centre-column-end; From 4db971e25945107510350576f5978949e1e7d09c Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 30 Jun 2026 16:47:05 +0100 Subject: [PATCH 33/80] Distinguish between feature and non-feature immersives --- .../src/components/ArticleHeadline.tsx | 4 ++-- dotcom-rendering/src/components/Figure.tsx | 23 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 96917e0fa74..f7951d9f145 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -400,8 +400,8 @@ export const ArticleHeadline = ({ starRating, }: Props) => { const isInverted = - format.design === ArticleDesign.Standard && - format.display === ArticleDisplay.Immersive; + format.display === ArticleDisplay.Immersive && + format.design === ArticleDesign.Standard; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index ab99567f7a6..7534acd1cd1 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -311,17 +311,18 @@ export const Figure = ({ return (
{children} - {format.display === ArticleDisplay.Immersive && ( -
- )} + {format.display === ArticleDisplay.Immersive && + format.design === ArticleDesign.Feature && ( +
+ )}
); } From 2d1b937585c9d2a4932387c65912c7e5362e1176 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 30 Jun 2026 18:21:43 +0100 Subject: [PATCH 34/80] Tie main media styling to article arrangement --- dotcom-rendering/src/components/Figure.tsx | 32 +++++++++---------- dotcom-rendering/src/components/MainMedia.tsx | 4 +++ dotcom-rendering/src/lib/renderElement.tsx | 4 +++ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index 7534acd1cd1..83a444ac608 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -6,11 +6,8 @@ import { space, until, } from '@guardian/source/foundations'; -import { - ArticleDesign, - ArticleDisplay, - type ArticleFormat, -} from '../lib/articleFormat'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; +import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; import { transparentColour } from '../lib/transparentColour'; import { palette } from '../palette'; import type { FEElement, RoleType } from '../types/content'; @@ -24,6 +21,7 @@ type Props = { className?: string; type?: FEElement['_type']; isTimeline?: boolean; + articleArrangement?: LayoutType; }; const overlayMaskGradientStyles = (angle: string) => css` @@ -301,6 +299,7 @@ export const Figure = ({ className = '', type, isTimeline = false, + articleArrangement = 'standard', }: Props) => { if (isMainMedia && !isTimeline) { // Don't add in-body styles for main media elements @@ -311,18 +310,17 @@ export const Figure = ({ return (
{children} - {format.display === ArticleDisplay.Immersive && - format.design === ArticleDesign.Feature && ( -
- )} + {articleArrangement === 'immersiveLandscapeFeature' && ( +
+ )}
); } diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx index 7b980173c99..3a2d30e23f5 100644 --- a/dotcom-rendering/src/components/MainMedia.tsx +++ b/dotcom-rendering/src/components/MainMedia.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/react'; import { breakpoints, space, until } from '@guardian/source/foundations'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { ArticleDesign, ArticleDisplay, @@ -96,6 +97,7 @@ type Props = { shouldHideAds: boolean; contentType?: string; contentLayout?: string; + articleArrangement?: LayoutType; }; export const MainMedia = ({ @@ -113,6 +115,7 @@ export const MainMedia = ({ shouldHideAds, contentType, contentLayout, + articleArrangement, }: Props) => { return (
@@ -135,6 +138,7 @@ export const MainMedia = ({ shouldHideAds={shouldHideAds} contentType={contentType} contentLayout={contentLayout} + articleArrangement={articleArrangement} /> ))}
diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index 03043d1bef0..fed8b1339dc 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -69,6 +69,7 @@ import { } from '../components/WitnessBlockComponent'; import { YoutubeBlockComponent } from '../components/YoutubeBlockComponent.island'; import { YoutubeEmbedBlockComponent } from '../components/YoutubeEmbedBlockComponent'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { interactiveLegacyFigureClasses, isInteractive, @@ -107,6 +108,7 @@ type Props = { shouldHideAds: boolean; contentType?: string; contentLayout?: string; + articleArrangement?: LayoutType; idApiUrl?: string; }; @@ -1050,6 +1052,7 @@ export const RenderArticleElement = ({ shouldHideAds, contentType, contentLayout, + articleArrangement, idApiUrl, }: Props) => { const withUpdatedRole = updateRole(element, format); @@ -1102,6 +1105,7 @@ export const RenderArticleElement = ({ type={element._type} format={format} isTimeline={isTimeline} + articleArrangement={articleArrangement} > {el} From 3107ac93efd53c2e05eaf9ec538b48045f554e4f Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 7 Jul 2026 16:15:55 +0100 Subject: [PATCH 35/80] Rebase tidying --- dotcom-rendering/src/components/ArticleHeadline.tsx | 5 ++--- .../src/layouts/StandardLayoutArticleGrid.tsx | 10 +++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index f7951d9f145..bcfd9d920b0 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -44,6 +44,7 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; + isInverted?: boolean; }; const topPadding = css` @@ -398,10 +399,8 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, + isInverted, }: Props) => { - const isInverted = - format.display === ArticleDisplay.Immersive && - format.design === ArticleDesign.Standard; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 21a5f4cdb17..3fb83d96ad9 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -234,6 +234,7 @@ export const StandardLayoutArticleGrid = ({ shouldHideAds={article.shouldHideAds} contentType={article.contentType} contentLayout={contentLayoutName} + articleArrangement={layoutType} /> @@ -390,7 +392,13 @@ export const StandardLayoutArticleGrid = ({ )} - + {/* Only show Listen to Article button on App landscape views */} {isApps && ( From ebf1fbc6a17e928df1a85f630fd0d52beda6f6af Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 16 Jul 2026 14:10:48 +0100 Subject: [PATCH 36/80] Rich review tweaks Slim nav, display bylines in the right place, grid adjustments --- .../src/components/DecideLines.tsx | 8 +++- .../src/components/Standfirst.tsx | 2 - .../src/layouts/StandardLayout.tsx | 4 +- .../src/layouts/StandardLayoutArticleGrid.tsx | 44 +++++++++++++++---- .../src/layouts/lib/articleArrangements.ts | 9 ++-- dotcom-rendering/src/lib/articleMeta.ts | 1 - 6 files changed, 50 insertions(+), 18 deletions(-) diff --git a/dotcom-rendering/src/components/DecideLines.tsx b/dotcom-rendering/src/components/DecideLines.tsx index 5d356323691..ef90047f62b 100644 --- a/dotcom-rendering/src/components/DecideLines.tsx +++ b/dotcom-rendering/src/components/DecideLines.tsx @@ -3,6 +3,7 @@ import { palette } from '@guardian/source/foundations'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import { ArticleDesign, + ArticleDisplay, type ArticleFormat, Pillar, } from '../lib/articleFormat'; @@ -41,7 +42,12 @@ const DottedLines = ({ ); export const DecideLines = ({ format, color }: Props) => { - const count = format.design === ArticleDesign.Comment ? 8 : 4; + const count = + format.design === ArticleDesign.Comment + ? 8 + : format.display === ArticleDisplay.Immersive + ? 1 + : 4; switch (format.theme) { case Pillar.Sport: diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index e95212f3302..c517894fcaa 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -213,7 +213,6 @@ const decidePadding = ({ display, design }: ArticleFormat) => { case ArticleDisplay.Immersive: return css` padding-bottom: 0; - padding-top: ${space[2]}px; ${from.tablet} { padding-bottom: 0; @@ -238,7 +237,6 @@ const decidePadding = ({ display, design }: ArticleFormat) => { case ArticleDisplay.Immersive: return css` padding-bottom: 0; - padding-top: ${space[2]}px; ${from.tablet} { padding-bottom: 0; diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 8a855b830a5..95318a5727e 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -116,7 +116,9 @@ export const StandardLayout = (props: WebProps | AppProps) => { idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} showSubNav={!isLabs && !isWorldCup2026} - showSlimNav={false} + showSlimNav={ + format.display === ArticleDisplay.Immersive + } hasPageSkinContentSelfConstrain={true} pageId={article.pageId} tagIds={article.tags.map((tag) => tag.id)} diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 3fb83d96ad9..d4c157a848a 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -186,7 +186,10 @@ export const StandardLayoutArticleGrid = ({ isImmersivePortrait ? 4 : isImmersiveLandscape - ? 3 + ? layoutType === + 'immersiveLandscapeFeature' + ? 3 + : 4 : 1, )} } @@ -208,7 +211,7 @@ export const StandardLayoutArticleGrid = ({ - + { case ArticleDisplay.NumberedList: return true; case ArticleDisplay.Immersive: - return false; case ArticleDisplay.Showcase: case ArticleDisplay.Standard: { switch (format.design) { From 2100f70f89d32bd34a1a062c0526edb6326d56ac Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 21 Jul 2026 16:40:23 +0100 Subject: [PATCH 37/80] Caption grid area and meta section --- .../src/components/ArticleMeta.web.tsx | 23 +++++++ dotcom-rendering/src/components/Caption.tsx | 32 ++-------- .../src/layouts/StandardLayoutArticleGrid.tsx | 36 +++++++++-- .../src/layouts/lib/articleArrangements.ts | 60 +++++++++++++------ dotcom-rendering/src/lib/articleMeta.test.ts | 8 --- 5 files changed, 100 insertions(+), 59 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index 5abd00eede4..812aad98dc5 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/react'; import { isUndefined } from '@guardian/libs'; import { between, from, space, until } from '@guardian/source/foundations'; +import { Hide } from '@guardian/source/react-components'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import type { CSSProperties } from 'react'; import type { FEArticle } from '../frontend/feArticle'; @@ -20,6 +21,7 @@ import { } from '../lib/articleMeta'; import { getAudioData } from '../lib/audio-data'; import { getSoleContributor } from '../lib/byline'; +import { decideMainMediaCaption } from '../lib/decide-caption'; import { palette as themePalette } from '../palette'; import { hasPreferredSourceButton } from '../preferredSource'; import type { Branding as BrandingType } from '../types/branding'; @@ -27,6 +29,7 @@ import type { FEElement } from '../types/content'; import type { TagType } from '../types/tag'; import { Avatar } from './Avatar'; import { Branding } from './Branding.island'; +import { Caption } from './Caption'; import { CommentCount } from './CommentCount.island'; import { useConfig } from './ConfigContext'; import { Contributor } from './Contributor'; @@ -479,6 +482,26 @@ export const ArticleMeta = ({

{showPreferredSource ? : null} + {format.display === ArticleDisplay.Immersive && + mainMediaElements?.[0] && ( + +
+ +
+
+ )} ); diff --git a/dotcom-rendering/src/components/Caption.tsx b/dotcom-rendering/src/components/Caption.tsx index 536aea4105e..b708a9c0129 100644 --- a/dotcom-rendering/src/components/Caption.tsx +++ b/dotcom-rendering/src/components/Caption.tsx @@ -34,7 +34,6 @@ type Props = { }; type IconProps = { - format: ArticleFormat; isMainMedia?: boolean; }; @@ -161,12 +160,6 @@ const bigLeftMargin = css` } `; -const hideIconBelowLeftCol = css` - ${until.leftCol} { - display: none; - } -`; - const pictureRatio = (13 / 18) * 100; const videoRatio = (23 / 36) * 100; @@ -251,30 +244,17 @@ const galleryStyles = css` } `; -const CameraIcon = ({ format, isMainMedia }: IconProps) => { +const CameraIcon = ({ isMainMedia }: IconProps) => { return ( - + ); }; -const VideoIcon = ({ format, isMainMedia }: IconProps) => { +const VideoIcon = ({ isMainMedia }: IconProps) => { return ( - + ); @@ -325,9 +305,9 @@ export const Caption = ({ data-spacefinder-role="inline" > {mediaType === 'YoutubeVideo' || mediaType === 'SelfHostedVideo' ? ( - + ) : ( - + )} {!!captionText && (
+ {isImmersive && ( + + + + + + )} { ...standardFormat, display: ArticleDisplay.NumberedList, }; - const immersive = { - ...standardFormat, - display: ArticleDisplay.Immersive, - }; it('should return true if Standard display and Standard design', () => { expect(shouldShowContributor(standardFormat)).toBe(true); @@ -47,8 +43,4 @@ describe('shouldShowContributor', () => { it('should return true if Numbered list display', () => { expect(shouldShowContributor(numberedList)).toBe(true); }); - - it('should return false if Immersive display', () => { - expect(shouldShowContributor(immersive)).toBe(false); - }); }); From b31f2dfb5958c6710a6ce41054eb29969588e941 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 29 Jul 2026 15:36:52 +0100 Subject: [PATCH 38/80] Update immersive ad count test --- dotcom-rendering/playwright/tests/commercial.e2e.spec.ts | 2 +- dotcom-rendering/playwright/tests/lightbox.e2e.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts b/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts index 61c96351056..db823202659 100644 --- a/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts +++ b/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts @@ -19,7 +19,7 @@ test.describe('Commercial E2E tests', () => { 'merchandising', ]; - const totalSlotsExpected = 15; // All slots, even if unfilled ie. `display: none` + const totalSlotsExpected = 16; // All slots, even if unfilled ie. `display: none` const inlineSlots = totalSlotsExpected - fixedSlots.length; // We are excluding survey slot as they can be switched off diff --git a/dotcom-rendering/playwright/tests/lightbox.e2e.spec.ts b/dotcom-rendering/playwright/tests/lightbox.e2e.spec.ts index 5b902439b82..f5e3becad75 100644 --- a/dotcom-rendering/playwright/tests/lightbox.e2e.spec.ts +++ b/dotcom-rendering/playwright/tests/lightbox.e2e.spec.ts @@ -109,7 +109,7 @@ test.describe('Lightbox', () => { // Open lightbox by clicking on the fourth image within the article body // Lightbox should open at the fifth image as the first image is the main media image - await page.locator('article img').nth(3).click({ force: true }); + await page.locator('button.open-lightbox').nth(4).click(); await expectToBeVisible(page, '#gu-lightbox'); From a6a17d700966e7545e6531013c17904c3d4ae2a0 Mon Sep 17 00:00:00 2001 From: Alessia Amitrano Date: Wed, 29 Jul 2026 17:20:32 +0100 Subject: [PATCH 39/80] add changes lost during merge --- .../src/layouts/StandardLayoutArticleGrid.tsx | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 1855ea52306..4f52aca1c2d 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -229,11 +229,11 @@ export const StandardLayoutArticleGrid = ({ ` : undefined, // Force portrait aspect ratio for local dev purposes - isImmersivePortrait - ? css` - aspect-ratio: 4 / 5; - ` - : undefined, + // isImmersivePortrait + // ? css` + // aspect-ratio: 4 / 5; + // ` + // : undefined, ]} > - {layoutType !== 'immersivePortraitDefault' && ( -
- {isWeb && - format.theme === ArticleSpecial.Labs && - format.design !== ArticleDesign.Video ? ( - - ) : ( - - )} -
- )} + {layoutType !== 'immersivePortraitDefault' && + layoutType !== 'immersivePortraitFeature' && + layoutType !== 'immersiveLandscapeDefault' && + layoutType !== 'immersiveLandscapeFeature' && ( +
+ {isWeb && + format.theme === ArticleSpecial.Labs && + format.design !== ArticleDesign.Video ? ( + + ) : ( + + )} +
+ )} {isApps ? ( <> Date: Wed, 29 Jul 2026 17:33:26 +0100 Subject: [PATCH 40/80] remove duplicate identifier --- dotcom-rendering/src/components/ArticleHeadline.tsx | 1 - dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index a5b77f30544..babb0ff536b 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -402,7 +402,6 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, - isInverted, }: Props) => { const isInverted = format.display === ArticleDisplay.Immersive && diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index dcb2e230f08..17dcaeb422d 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -331,7 +331,6 @@ export const StandardLayoutArticleGrid = ({ article.webPublicationDateDeprecated } starRating={article.starRating} - isInverted={layoutType === 'immersiveLandscapeDefault'} />
Date: Tue, 19 May 2026 15:25:40 +0100 Subject: [PATCH 41/80] Rough first pass --- .../src/layouts/StandardLayoutArticleGrid.tsx | 167 ++++++++++++--- .../src/layouts/lib/articleArrangements.ts | 197 +++++++++++++++++- 2 files changed, 336 insertions(+), 28 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 84fb7928e6d..21a5f4cdb17 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -22,6 +22,7 @@ import { SlotBodyEnd } from '../components/SlotBodyEnd.island'; import { Standfirst } from '../components/Standfirst'; import { SubMeta } from '../components/SubMeta'; import { grid } from '../grid'; +import { getAgeWarning } from '../lib/age-warning'; import { ArticleDesign, ArticleDisplay, @@ -36,6 +37,7 @@ import type { ArticleDeprecated } from '../types/article'; import type { RenderingTarget } from '../types/renderingTarget'; import { type Area, + getLayoutType, gridItemCss, type LayoutType, } from './lib/articleArrangements'; @@ -59,6 +61,23 @@ interface GridItemProps { children: React.ReactNode; } +/** + * Works out the orientation of an image from its Guardian media URL, which + * encodes the crop dimensions in the path (e.g. `/1000_600_800_480/`). + * Falls back to 'landscape' if the URL doesn't match the expected pattern. + */ +const getImageOrientation = ( + url: string, +): 'portrait' | 'landscape' | 'square' => { + const match = url.match(/\/\d+_\d+_(\d+)_(\d+)\/\d+\.\w+$/); + if (!match) return 'landscape'; + const [, width, height] = match.map(Number); + if (width == null || height == null) return 'landscape'; + if (height > width) return 'portrait'; + if (width > height) return 'landscape'; + return 'square'; +}; + const GridItem = ({ area, layoutType, @@ -110,6 +129,8 @@ export const StandardLayoutArticleGrid = ({ format.design === ArticleDesign.Video || format.design === ArticleDesign.Audio; const isShowcase = format.display === ArticleDisplay.Showcase; + const isImmersive = format.display === ArticleDisplay.Immersive; + const isFeature = format.design === ArticleDesign.Feature; const footballMatchUrl = article.matchType === 'FootballMatchType' @@ -119,11 +140,36 @@ export const StandardLayoutArticleGrid = ({ const isFootballMatchReport = format.design === ArticleDesign.MatchReport && !!footballMatchUrl; - const layoutType: LayoutType = isMedia - ? 'media' - : isShowcase - ? 'showcase' - : 'standard'; + const firstMainMediaElement = article.mainMediaElements[0]; + const mainMediaUrl: string | undefined = + firstMainMediaElement?._type === + 'model.dotcomrendering.pageElements.ImageBlockElement' + ? firstMainMediaElement.media.allImages[0]?.url + : undefined; + + const mainMediaOrientation = + mainMediaUrl != null ? getImageOrientation(mainMediaUrl) : 'landscape'; + + const layoutType = getLayoutType({ + isImmersive, + isFeature, + orientation: mainMediaOrientation, + isMedia, + isShowcase, + }); + const contentLayoutName = `${ArticleDisplay[format.display]}Layout`; + + const isImmersivePortrait = + layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature'; + const isImmersiveLandscape = + layoutType === 'immersiveLandscapeDefault' || + layoutType === 'immersiveLandscapeFeature'; + + const ageWarning = getAgeWarning( + article.tags, + article.webPublicationDateDeprecated, + ); return (
- + - + - + - - {format.design !== ArticleDesign.Audio && ( -
- {isWeb && - format.theme === ArticleSpecial.Labs && - format.design !== ArticleDesign.Video ? ( - - ) : ( - - )} -
- )} + + {format.design !== ArticleDesign.Audio && + layoutType !== 'immersivePortraitDefault' && ( +
+ {isWeb && + format.theme === ArticleSpecial.Labs && + format.design !== ArticleDesign.Video ? ( + + ) : ( + + )} +
+ )} {isApps ? ( <> @@ -263,11 +378,11 @@ export const StandardLayoutArticleGrid = ({ secondaryDateline={ article.webPublicationSecondaryDateDisplay } + webPublicationDate={article.webPublicationDate} isCommentable={article.isCommentable} discussionApiUrl={article.config.discussionApiUrl} shortUrlId={article.config.shortUrlId} mainMediaElements={article.mainMediaElements} - webPublicationDate={article.webPublicationDate} /> {!!article.affiliateLinksDisclaimer && ( diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 2cf5fb6843f..636af9fac97 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -2,7 +2,14 @@ import { css, type SerializedStyles } from '@emotion/react'; import { from, until } from '@guardian/source/foundations'; import { grid } from '../../grid'; -export type LayoutType = 'standard' | 'showcase' | 'media'; +export type LayoutType = + | 'standard' + | 'showcase' + | 'media' + | 'immersiveLandscapeDefault' + | 'immersiveLandscapeFeature' + | 'immersivePortraitDefault' + | 'immersivePortraitFeature'; export type Area = | 'title' @@ -13,13 +20,14 @@ export type Area = | 'body' | 'right-column'; -type Breakpoint = 'mobile' | 'tablet' | 'desktop' | 'leftCol'; +type Breakpoint = 'mobile' | 'tablet' | 'desktop' | 'leftCol' | 'wide'; const breakpointQueries: Record = { mobile: until.tablet, tablet: from.tablet, desktop: from.desktop, leftCol: from.leftCol, + wide: from.wide, }; // Raw CSS overrides per area per breakpoint. Entries are only needed when an area @@ -149,10 +157,160 @@ const mediaCss: LayoutCssMap = { }, }; +const immersivePortraitDefaultCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: `grid-row: 1; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 1; ${grid.between('left-column-start', 9)};`, + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: `grid-row: 2; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 2; ${grid.between('left-column-start', 9)};`, + wide: `grid-row: 2; ${grid.between('left-column-start', 10)};`, + }, + media: { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: `grid-row: 1 / span 4; ${grid.between(8, 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between(9, 'right-column-end')};`, + wide: `grid-row: 1 / span 3; ${grid.between(10, 'right-column-end')};`, + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + desktop: `grid-row: 3; ${grid.between('centre-column-start', 7)};`, + leftCol: `grid-row: 3; ${grid.between('centre-column-start', 8)};`, + wide: `grid-row: 3; ${grid.between('centre-column-start', 9)};`, + }, + meta: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: `grid-row: 4; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 3; ${grid.column.left};`, + }, + body: { + mobile: 'grid-row: 6;', + }, + 'right-column': { + desktop: `grid-row: 5; ${grid.column.right};`, + leftCol: `grid-row: 4; ${grid.column.right};`, + }, +}; + +const immersivePortraitFeatureCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: `grid-row: 1; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 1; ${grid.between('left-column-start', 9)};`, + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: `grid-row: 2; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 2; ${grid.between('left-column-start', 9)};`, + wide: `grid-row: 2; ${grid.between('left-column-start', 10)};`, + }, + media: { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: `grid-row: 1 / span 4; ${grid.between(8, 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between(9, 'right-column-end')};`, + wide: `grid-row: 1 / span 3; ${grid.between(10, 'right-column-end')};`, + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + desktop: `grid-row: 3; ${grid.between('centre-column-start', 7)};`, + leftCol: `grid-row: 3; ${grid.between('centre-column-start', 8)};`, + wide: `grid-row: 3; ${grid.between('centre-column-start', 9)};`, + }, + meta: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: `grid-row: 4; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 3; ${grid.column.left};`, + }, + body: { + mobile: 'grid-row: 6;', + }, + 'right-column': { + desktop: `grid-row: 5; ${grid.column.right};`, + leftCol: `grid-row: 4; ${grid.column.right};`, + }, +}; + +const immersiveLandscapeDefaultCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + desktop: 'grid-row: 2;', + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: 'grid-row: 3 / span 2;', + }, + media: { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: `grid-row: 1 / span 3; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + desktop: 'grid-row: 5;', + }, + meta: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: `grid-row: 6; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 6 / span 2; ${grid.column.left};`, + }, + body: { + mobile: 'grid-row: 6;', + }, + 'right-column': { + desktop: `grid-row: 5 / span 3; ${grid.column.right};`, + }, +}; + +const immersiveLandscapeFeatureCss: LayoutCssMap = { + title: { + desktop: 'grid-row: 2;', + }, + headline: { + desktop: 'grid-row: 3 / span 2;', + }, + media: { + mobile: `${grid.column.all}`, + desktop: `grid-row: 1 / span 3; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 1 / span 3; ${grid.between('left-column-start', 'right-column-end')};`, + }, + standfirst: { + desktop: 'grid-row: 5;', + }, + meta: { + desktop: `grid-row: 6; ${grid.between('centre-column-start', 8)};`, + leftCol: `grid-row: 6 / span 2; ${grid.column.left};`, + }, + 'right-column': { + desktop: `grid-row: 5 / span 3; ${grid.column.right};`, + }, +}; + const layoutCssMaps: Record = { standard: standardCss, showcase: showcaseCss, media: mediaCss, + immersiveLandscapeDefault: immersiveLandscapeDefaultCss, + immersiveLandscapeFeature: immersiveLandscapeFeatureCss, + immersivePortraitDefault: immersivePortraitDefaultCss, + immersivePortraitFeature: immersivePortraitFeatureCss, }; /** @@ -192,3 +350,38 @@ export const gridItemCss = ( ${breakpointCss} `; }; + +/** + * Determines which {@link LayoutType} to render. Immersive layouts are + * split by orientation (portrait vs. landscape/square) and by whether the + * format is a Feature, since each combination has a distinct grid + * arrangement. Non-immersive formats fall back to media/showcase/standard. + */ +export const getLayoutType = ({ + isImmersive, + isFeature, + orientation, + isMedia, + isShowcase, +}: { + isImmersive: boolean; + isFeature: boolean; + orientation: 'portrait' | 'landscape' | 'square'; + isMedia: boolean; + isShowcase: boolean; +}): LayoutType => { + if (isImmersive) { + if (orientation === 'portrait') { + return isFeature + ? 'immersivePortraitFeature' + : 'immersivePortraitDefault'; + } + // Square images are treated the same as landscape for immersive layouts. + return isFeature + ? 'immersiveLandscapeFeature' + : 'immersiveLandscapeDefault'; + } + if (isMedia) return 'media'; + if (isShowcase) return 'showcase'; + return 'standard'; +}; From b73ce4a3d44bd6e7176449f865db3a612ff1dbcd Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 26 May 2026 16:50:39 +0100 Subject: [PATCH 42/80] Rough implementation of portrait/landscape variants --- .../src/components/ArticleHeadline.tsx | 151 +-- .../src/components/ArticleTitle.tsx | 10 +- .../src/components/Standfirst.tsx | 1 - dotcom-rendering/src/layouts/DecideLayout.tsx | 5 +- .../src/layouts/ImmersiveLayout.tsx | 1022 ----------------- dotcom-rendering/src/paletteDeclarations.ts | 15 +- 6 files changed, 62 insertions(+), 1142 deletions(-) delete mode 100644 dotcom-rendering/src/layouts/ImmersiveLayout.tsx diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 3b0c141fc86..8beda81e2eb 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -27,7 +27,6 @@ import { ArticleSpecial, Pillar, } from '../lib/articleFormat'; -import { getZIndex } from '../lib/getZIndex'; import { palette as themePalette } from '../palette'; import type { StarRating as Rating } from '../types/content'; import type { TagType } from '../types/tag'; @@ -45,6 +44,7 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; + isInverted?: boolean; }; const topPadding = css` @@ -214,30 +214,20 @@ const invertedStyles = css` box-decoration-break: clone; `; -const immersiveStyles = css` - min-height: 112px; - padding-bottom: ${space[6]}px; - padding-left: ${space[1]}px; - - ${from.mobileLandscape} { - padding-left: ${space[3]}px; - } - - ${from.tablet} { - padding-left: ${space[1]}px; - } - - margin-right: ${space[5]}px; -`; - const darkBackground = css` background-color: ${themePalette('--headline-background')}; `; const invertedText = css` - white-space: pre-wrap; - padding-bottom: ${space[1]}px; - padding-right: ${space[1]}px; + ${from.desktop} { + color: white; + background-color: black; + white-space: pre-wrap; + padding-bottom: ${space[1]}px; + padding-right: ${space[1]}px; + margin-left: -10px; + padding-left: 10px; + } `; const maxWidth = css` @@ -255,35 +245,6 @@ const invertedWrapper = css` margin-left: 6px; `; -const immersiveWrapper = css` - /* - Make sure we vertically align the headline font with the body font - */ - margin-left: 6px; - - ${from.tablet} { - margin-left: 16px; - } - - ${from.leftCol} { - margin-left: 25px; - } - - /* - We need this grow to ensure the headline fills the main content column - */ - flex-grow: 1; - /* - This z-index is what ensures the headline text shows above the pseudo black - box that extends the black background to the right - */ - z-index: ${getZIndex('articleHeadline')}; - - ${until.mobileLandscape} { - margin-right: 40px; - } -`; - // Due to MainMedia using position: relative, this seems to effect the rendering order // To mitigate we use z-index // TODO: find a cleaner solution @@ -292,59 +253,58 @@ const zIndex = css` `; const ageWarningMargins = (format: ArticleFormat) => { - if (format.design === ArticleDesign.Gallery) { + if ( + format.design === ArticleDesign.Gallery || + format.display === ArticleDisplay.Immersive + ) { return ''; } - return format.display === ArticleDisplay.Immersive - ? css` - margin-left: 0px; - margin-bottom: 0px; - - ${from.tablet} { - margin-left: 10px; - } + return css` + margin-top: 12px; + margin-left: -10px; + margin-bottom: 6px; - ${from.leftCol} { - margin-left: 20px; - } - ` - : css` - margin-top: 12px; - margin-left: -10px; - margin-bottom: 6px; - - ${from.tablet} { - margin-left: -20px; - } + ${from.tablet} { + margin-left: -20px; + } - ${from.leftCol} { - margin-left: -10px; - margin-top: 0; - } - `; + ${from.leftCol} { + margin-left: -10px; + margin-top: 0; + } + `; }; -const backgroundStyles = css` - background-color: ${themePalette('--age-warning-wrapper-background')}; -`; - const WithAgeWarning = ({ tags, webPublicationDateDeprecated, format, children, + snapToInverted = false, }: { tags: TagType[]; webPublicationDateDeprecated: string; format: ArticleFormat; children: React.ReactNode; + snapToInverted?: boolean; }) => { const age = getAgeWarning(tags, webPublicationDateDeprecated); if (age) { return ( <> -
+
{children} @@ -439,6 +399,7 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, + isInverted = false, }: Props) => { switch (format.display) { case ArticleDisplay.Immersive: { @@ -465,12 +426,13 @@ export const ArticleHeadline = ({ format.theme === ArticleSpecial.Labs ? labsFont : headlineFont(format), - invertedText, - css` - color: ${themePalette( - '--headline-colour', - )}; - `, + isInverted + ? [invertedText, darkBackground] + : css` + color: ${themePalette( + '--headline-colour', + )}; + `, ]} > {headlineString} @@ -496,16 +458,17 @@ export const ArticleHeadline = ({ webPublicationDateDeprecated } format={format} + snapToInverted={true} >

diff --git a/dotcom-rendering/src/components/ArticleTitle.tsx b/dotcom-rendering/src/components/ArticleTitle.tsx index de44e1eb319..2cf4d717f3a 100644 --- a/dotcom-rendering/src/components/ArticleTitle.tsx +++ b/dotcom-rendering/src/components/ArticleTitle.tsx @@ -28,17 +28,11 @@ const sectionStyles = css` `; const immersiveMargins = css` - max-width: 400px; + max-width: 500px; min-width: 200px; margin-bottom: 4px; - /* - Make sure we vertically align the title font with the body font - */ ${from.tablet} { - margin-left: 16px; - } - ${from.leftCol} { - margin-left: 25px; + margin-left: -4px; } `; diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index 0567d1dbc99..e95212f3302 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -268,7 +268,6 @@ const standfirstStyles = ({ display, design, theme }: ArticleFormat) => { `; default: return css` - max-width: 280px; ${from.tablet} { max-width: 460px; } diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index 77803138313..3d828bdc45b 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -9,7 +9,6 @@ import { GalleryLayout } from './GalleryLayout'; import { HostedArticleLayout } from './HostedArticleLayout'; import { HostedGalleryLayout } from './HostedGalleryLayout'; import { HostedVideoLayout } from './HostedVideoLayout'; -import { ImmersiveLayout } from './ImmersiveLayout'; import { InteractiveLayout } from './InteractiveLayout'; import { LiveLayout } from './LiveLayout'; import { NewsletterSignupLayout } from './NewsletterSignupLayout'; @@ -55,7 +54,7 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { } default: { return ( - { } default: { return ( - ( -
- {children} -
-); - -const maxWidth = css` - ${from.desktop} { - max-width: 620px; - } -`; - -const linesMargin = css` - ${from.leftCol} { - margin-top: ${space[5]}px; - } -`; - -const stretchLines = css` - ${until.phablet} { - margin-left: -20px; - margin-right: -20px; - } - ${until.mobileLandscape} { - margin-left: -10px; - margin-right: -10px; - } -`; - -interface CommonProps { - article: ArticleDeprecated; - format: ArticleFormat; - serverTime?: number; -} - -interface WebProps extends CommonProps { - NAV: NavType; - renderingTarget: 'Web'; -} - -interface AppProps extends CommonProps { - renderingTarget: 'Apps'; -} - -const Box = ({ children }: { children: React.ReactNode }) => ( -
- {children} -
-); - -export const ImmersiveLayout = (props: WebProps | AppProps) => { - const { article, format, renderingTarget, serverTime } = props; - - const { - config: { isPaidContent, host, hasSurveyAd }, - editionId, - } = article; - const isWeb = renderingTarget === 'Web'; - const isApps = renderingTarget === 'Apps'; - - const showBodyEndSlot = - isWeb && - (parse(article.slotMachineFlags ?? '').showBodyEnd || - article.config.switches.slotBodyEnd); - - // TODO: - // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render - // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. - - const showComments = article.isCommentable && !isPaidContent; - - const mainMedia = article.mainMediaElements[0]; - - const captionText = decideMainMediaCaption(mainMedia); - - const HEADLINE_OFFSET = mainMedia ? 120 : 0; - - const { branding } = article.commercialProperties[article.editionId]; - - const contributionsServiceUrl = getContributionsServiceUrl(article); - - const isLabs = format.theme === ArticleSpecial.Labs; - - /** - We need change the height values depending on whether the labs header is there or not to keep - the headlines appearing at a consistent height between labs and non labs immersive articles. - */ - - const labsHeaderHeight = LABS_HEADER_HEIGHT; - const combinedHeight = (minHeaderHeightPx + labsHeaderHeight).toString(); - - const navAndLabsHeaderHeight = isLabs - ? `${combinedHeight}px` - : `${minHeaderHeightPx}px`; - - const hasMainMediaStyles = css` - height: calc(80vh - ${navAndLabsHeaderHeight}); - /** - 80vh is normally enough but don't let the content shrink vertically too - much just in case - */ - min-height: calc(25rem - ${navAndLabsHeaderHeight}); - ${from.desktop} { - height: calc(100vh - ${navAndLabsHeaderHeight}); - min-height: calc(31.25rem - ${navAndLabsHeaderHeight}); - } - ${from.wide} { - min-height: calc(50rem - ${navAndLabsHeaderHeight}); - } - `; - const LeftColCaption = () => ( -
- -
- ); - - const renderAds = canRenderAds(article); - - return ( - <> - {isWeb && ( - tag.id)} - sectionId={article.config.section} - contentType={article.contentType} - /> - )} - - - - {format.theme === ArticleSpecial.Labs && ( - -
- -
-
- )} - -
-
- -
- {mainMedia && ( - <> -
-
} - > - -
- -
- -
-
-
- - )} -
- - {isWeb && renderAds && hasSurveyAd && ( - - )} - -
- {isApps && renderAds && ( - - - - )} -
- - {/* Above leftCol, the Caption is controlled by Section ^^ */} - - - - - - - {format.design === ArticleDesign.PhotoEssay ? ( - <> - ) : ( - - )} - - - <> - {!mainMedia && ( -
- -
- )} - -
- - <> - {!mainMedia && ( -
- -
- )} - -
- - - - - {!!article.byline && ( - - )} - {/* Only show Listen to Article button on App landscape views */} - {isApps && ( - -
- - - -
-
- )} -
- - {format.design === ArticleDesign.PhotoEssay && - !isLabs ? ( - <> - ) : ( -
-
- {format.theme === - ArticleSpecial.Labs ? ( - - ) : ( - - )} -
-
- )} -
- {isApps ? ( - <> - - - - - - {!!article.affiliateLinksDisclaimer && ( - - )} - - - ) : ( - <> - - {!!article.affiliateLinksDisclaimer && ( - - )} - - )} -
-
- - - - {showBodyEndSlot && ( - - - - )} - - - - - -
- - <> - {mainMedia && isWeb && renderAds && ( -
- { - - } -
- )} - -
-
-
-
-
- {!isLabs && isWeb && renderAds && ( -
- -
- )} - - {article.storyPackage && ( -
- - - -
- )} - - - - - - {showComments && ( -
- -
- )} - {!isPaidContent && ( -
- - - - - -
- )} - {!isLabs && isWeb && renderAds && ( -
- -
- )} -
- - {isWeb && props.NAV.subNavSections && ( -
- - - -
- )} - - {isWeb && ( - <> -
-
-
- - - - - - - {renderAds && ( - - )} - - )} - {isApps && ( -
- - - -
- )} - - ); -}; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 3fb76dea59d..b0bf5c22d02 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -88,7 +88,7 @@ const textblockTextDark: PaletteFunction = () => 'inherit'; const headlineTextLight: PaletteFunction = ({ design, display, theme }) => { switch (display) { case ArticleDisplay.Immersive: - return sourcePalette.neutral[97]; + return sourcePalette.neutral[7]; default: { switch (design) { case ArticleDesign.Editorial: @@ -195,19 +195,8 @@ const headlineMatchTextLight: PaletteFunction = (format) => const headlineMatchTextDark: PaletteFunction = (format) => seriesTitleMatchTextDark(format); -const headlineBackgroundLight: PaletteFunction = ({ - display, - design, - theme, -}) => { +const headlineBackgroundLight: PaletteFunction = ({ display, design }) => { switch (display) { - case ArticleDisplay.Immersive: - switch (theme) { - case ArticleSpecial.SpecialReport: - return sourcePalette.specialReport[300]; - default: - return sourcePalette.neutral[7]; - } case ArticleDisplay.Showcase: case ArticleDisplay.NumberedList: case ArticleDisplay.Standard: From a3163957e9d1116e65c6dcee3a3e9353f2a183be Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 11 Jun 2026 15:41:01 +0100 Subject: [PATCH 43/80] Feature specific styling --- .../src/components/ArticleHeadline.tsx | 5 +- dotcom-rendering/src/components/Figure.tsx | 53 ++++++++++++++++++- dotcom-rendering/src/components/MainMedia.tsx | 1 + dotcom-rendering/src/grid.ts | 2 + 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 8beda81e2eb..96917e0fa74 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -44,7 +44,6 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; - isInverted?: boolean; }; const topPadding = css` @@ -399,8 +398,10 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, - isInverted = false, }: Props) => { + const isInverted = + format.design === ArticleDesign.Standard && + format.display === ArticleDisplay.Immersive; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index ccb113fb4b1..ab99567f7a6 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -1,6 +1,18 @@ import { css } from '@emotion/react'; -import { breakpoints, from, space, until } from '@guardian/source/foundations'; -import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; +import { + breakpoints, + from, + palette as sourcePalette, + space, + until, +} from '@guardian/source/foundations'; +import { + ArticleDesign, + ArticleDisplay, + type ArticleFormat, +} from '../lib/articleFormat'; +import { transparentColour } from '../lib/transparentColour'; +import { palette } from '../palette'; import type { FEElement, RoleType } from '../types/content'; type Props = { @@ -14,6 +26,32 @@ type Props = { isTimeline?: boolean; }; +const overlayMaskGradientStyles = (angle: string) => css` + mask-image: linear-gradient( + ${angle}, + rgb(0, 0, 0) 0%, + rgba(0, 0, 0, 0.9619) 12.5%, + rgba(0, 0, 0, 0.8536) 25%, + rgba(0, 0, 0, 0.6913) 37.5%, + rgba(0, 0, 0, 0.5) 50%, + rgba(0, 0, 0, 0.3087) 62.5%, + rgba(0, 0, 0, 0.1464) 75%, + rgba(0, 0, 0, 0.0381) 87.5%, + transparent 100% + ); +`; + +const blurStyles = css` + position: absolute; + inset: 0; + background-color: ${palette('--article-background')}; + backdrop-filter: blur(12px) brightness(0.5); + @supports not (backdrop-filter: blur(12px)) { + background-color: ${transparentColour(sourcePalette.neutral[10], 0.7)}; + } + ${overlayMaskGradientStyles('0deg')}; +`; + const roleCss = { inline: css` margin-top: ${space[3]}px; @@ -273,6 +311,17 @@ export const Figure = ({ return (
{children} + {format.display === ArticleDisplay.Immersive && ( +
+ )}
); } diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx index 871c4606715..7b980173c99 100644 --- a/dotcom-rendering/src/components/MainMedia.tsx +++ b/dotcom-rendering/src/components/MainMedia.tsx @@ -12,6 +12,7 @@ import type { Switches } from '../types/config'; import type { FEElement } from '../types/content'; const mainMedia = css` + position: relative; height: 100%; ${until.tablet} { diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index a02e5c3f861..5e7a0111455 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -159,6 +159,7 @@ const outerRules = (color?: string): string => ` &::before { grid-column: centre-column-start; transform: translateX(-${columnGap}); + z-index: 10; ${fromBreakpoint.leftCol} { grid-column: left-column-start; @@ -169,6 +170,7 @@ const outerRules = (color?: string): string => ` &::after { grid-column: right-column-end; transform: translateX(-1px); + z-index: 10; ${betweenBreakpoint.tablet.and.desktop} { grid-column: centre-column-end; From b950785ad2e6adab9d7ca1a0b51c33bc366b8a19 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 30 Jun 2026 16:47:05 +0100 Subject: [PATCH 44/80] Distinguish between feature and non-feature immersives --- .../src/components/ArticleHeadline.tsx | 4 ++-- dotcom-rendering/src/components/Figure.tsx | 23 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 96917e0fa74..f7951d9f145 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -400,8 +400,8 @@ export const ArticleHeadline = ({ starRating, }: Props) => { const isInverted = - format.design === ArticleDesign.Standard && - format.display === ArticleDisplay.Immersive; + format.display === ArticleDisplay.Immersive && + format.design === ArticleDesign.Standard; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index ab99567f7a6..7534acd1cd1 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -311,17 +311,18 @@ export const Figure = ({ return (
{children} - {format.display === ArticleDisplay.Immersive && ( -
- )} + {format.display === ArticleDisplay.Immersive && + format.design === ArticleDesign.Feature && ( +
+ )}
); } From 691fcea3a663a4c80ea5adc65c7f3e0d5f5f8b66 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 30 Jun 2026 18:21:43 +0100 Subject: [PATCH 45/80] Tie main media styling to article arrangement --- dotcom-rendering/src/components/Figure.tsx | 32 +++++++++---------- dotcom-rendering/src/components/MainMedia.tsx | 4 +++ dotcom-rendering/src/lib/renderElement.tsx | 4 +++ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/dotcom-rendering/src/components/Figure.tsx b/dotcom-rendering/src/components/Figure.tsx index 7534acd1cd1..83a444ac608 100644 --- a/dotcom-rendering/src/components/Figure.tsx +++ b/dotcom-rendering/src/components/Figure.tsx @@ -6,11 +6,8 @@ import { space, until, } from '@guardian/source/foundations'; -import { - ArticleDesign, - ArticleDisplay, - type ArticleFormat, -} from '../lib/articleFormat'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; +import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; import { transparentColour } from '../lib/transparentColour'; import { palette } from '../palette'; import type { FEElement, RoleType } from '../types/content'; @@ -24,6 +21,7 @@ type Props = { className?: string; type?: FEElement['_type']; isTimeline?: boolean; + articleArrangement?: LayoutType; }; const overlayMaskGradientStyles = (angle: string) => css` @@ -301,6 +299,7 @@ export const Figure = ({ className = '', type, isTimeline = false, + articleArrangement = 'standard', }: Props) => { if (isMainMedia && !isTimeline) { // Don't add in-body styles for main media elements @@ -311,18 +310,17 @@ export const Figure = ({ return (
{children} - {format.display === ArticleDisplay.Immersive && - format.design === ArticleDesign.Feature && ( -
- )} + {articleArrangement === 'immersiveLandscapeFeature' && ( +
+ )}
); } diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx index 7b980173c99..3a2d30e23f5 100644 --- a/dotcom-rendering/src/components/MainMedia.tsx +++ b/dotcom-rendering/src/components/MainMedia.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/react'; import { breakpoints, space, until } from '@guardian/source/foundations'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { ArticleDesign, ArticleDisplay, @@ -96,6 +97,7 @@ type Props = { shouldHideAds: boolean; contentType?: string; contentLayout?: string; + articleArrangement?: LayoutType; }; export const MainMedia = ({ @@ -113,6 +115,7 @@ export const MainMedia = ({ shouldHideAds, contentType, contentLayout, + articleArrangement, }: Props) => { return (
@@ -135,6 +138,7 @@ export const MainMedia = ({ shouldHideAds={shouldHideAds} contentType={contentType} contentLayout={contentLayout} + articleArrangement={articleArrangement} /> ))}
diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index 03043d1bef0..fed8b1339dc 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -69,6 +69,7 @@ import { } from '../components/WitnessBlockComponent'; import { YoutubeBlockComponent } from '../components/YoutubeBlockComponent.island'; import { YoutubeEmbedBlockComponent } from '../components/YoutubeEmbedBlockComponent'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { interactiveLegacyFigureClasses, isInteractive, @@ -107,6 +108,7 @@ type Props = { shouldHideAds: boolean; contentType?: string; contentLayout?: string; + articleArrangement?: LayoutType; idApiUrl?: string; }; @@ -1050,6 +1052,7 @@ export const RenderArticleElement = ({ shouldHideAds, contentType, contentLayout, + articleArrangement, idApiUrl, }: Props) => { const withUpdatedRole = updateRole(element, format); @@ -1102,6 +1105,7 @@ export const RenderArticleElement = ({ type={element._type} format={format} isTimeline={isTimeline} + articleArrangement={articleArrangement} > {el} From 0904753659a50548d5b8bc1cff6df23918397d4b Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 7 Jul 2026 16:15:55 +0100 Subject: [PATCH 46/80] Rebase tidying --- dotcom-rendering/src/components/ArticleHeadline.tsx | 5 ++--- .../src/layouts/StandardLayoutArticleGrid.tsx | 10 +++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index f7951d9f145..bcfd9d920b0 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -44,6 +44,7 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; + isInverted?: boolean; }; const topPadding = css` @@ -398,10 +399,8 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, + isInverted, }: Props) => { - const isInverted = - format.display === ArticleDisplay.Immersive && - format.design === ArticleDesign.Standard; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 21a5f4cdb17..3fb83d96ad9 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -234,6 +234,7 @@ export const StandardLayoutArticleGrid = ({ shouldHideAds={article.shouldHideAds} contentType={article.contentType} contentLayout={contentLayoutName} + articleArrangement={layoutType} /> @@ -390,7 +392,13 @@ export const StandardLayoutArticleGrid = ({ )} - + {/* Only show Listen to Article button on App landscape views */} {isApps && ( From 6041b44fdddd078c36da995e32fe822d5e850b94 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 16 Jul 2026 14:10:48 +0100 Subject: [PATCH 47/80] Rich review tweaks Slim nav, display bylines in the right place, grid adjustments --- .../src/components/DecideLines.tsx | 8 +++- .../src/components/Standfirst.tsx | 2 - .../src/layouts/StandardLayout.tsx | 4 +- .../src/layouts/StandardLayoutArticleGrid.tsx | 44 +++++++++++++++---- .../src/layouts/lib/articleArrangements.ts | 9 ++-- dotcom-rendering/src/lib/articleMeta.ts | 1 - 6 files changed, 50 insertions(+), 18 deletions(-) diff --git a/dotcom-rendering/src/components/DecideLines.tsx b/dotcom-rendering/src/components/DecideLines.tsx index 5d356323691..ef90047f62b 100644 --- a/dotcom-rendering/src/components/DecideLines.tsx +++ b/dotcom-rendering/src/components/DecideLines.tsx @@ -3,6 +3,7 @@ import { palette } from '@guardian/source/foundations'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import { ArticleDesign, + ArticleDisplay, type ArticleFormat, Pillar, } from '../lib/articleFormat'; @@ -41,7 +42,12 @@ const DottedLines = ({ ); export const DecideLines = ({ format, color }: Props) => { - const count = format.design === ArticleDesign.Comment ? 8 : 4; + const count = + format.design === ArticleDesign.Comment + ? 8 + : format.display === ArticleDisplay.Immersive + ? 1 + : 4; switch (format.theme) { case Pillar.Sport: diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index e95212f3302..c517894fcaa 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -213,7 +213,6 @@ const decidePadding = ({ display, design }: ArticleFormat) => { case ArticleDisplay.Immersive: return css` padding-bottom: 0; - padding-top: ${space[2]}px; ${from.tablet} { padding-bottom: 0; @@ -238,7 +237,6 @@ const decidePadding = ({ display, design }: ArticleFormat) => { case ArticleDisplay.Immersive: return css` padding-bottom: 0; - padding-top: ${space[2]}px; ${from.tablet} { padding-bottom: 0; diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 8a855b830a5..95318a5727e 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -116,7 +116,9 @@ export const StandardLayout = (props: WebProps | AppProps) => { idApiUrl={article.config.idApiUrl} contributionsServiceUrl={contributionsServiceUrl} showSubNav={!isLabs && !isWorldCup2026} - showSlimNav={false} + showSlimNav={ + format.display === ArticleDisplay.Immersive + } hasPageSkinContentSelfConstrain={true} pageId={article.pageId} tagIds={article.tags.map((tag) => tag.id)} diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 3fb83d96ad9..d4c157a848a 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -186,7 +186,10 @@ export const StandardLayoutArticleGrid = ({ isImmersivePortrait ? 4 : isImmersiveLandscape - ? 3 + ? layoutType === + 'immersiveLandscapeFeature' + ? 3 + : 4 : 1, )} } @@ -208,7 +211,7 @@ export const StandardLayoutArticleGrid = ({ - + { case ArticleDisplay.NumberedList: return true; case ArticleDisplay.Immersive: - return false; case ArticleDisplay.Showcase: case ArticleDisplay.Standard: { switch (format.design) { From 8f76f07a664fe46741bb3d2d5c9565302a68418b Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Tue, 21 Jul 2026 16:40:23 +0100 Subject: [PATCH 48/80] Caption grid area and meta section --- .../src/components/ArticleMeta.web.tsx | 23 +++++++ dotcom-rendering/src/components/Caption.tsx | 32 ++-------- .../src/layouts/StandardLayoutArticleGrid.tsx | 36 +++++++++-- .../src/layouts/lib/articleArrangements.ts | 60 +++++++++++++------ dotcom-rendering/src/lib/articleMeta.test.ts | 8 --- 5 files changed, 100 insertions(+), 59 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index 5abd00eede4..812aad98dc5 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/react'; import { isUndefined } from '@guardian/libs'; import { between, from, space, until } from '@guardian/source/foundations'; +import { Hide } from '@guardian/source/react-components'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import type { CSSProperties } from 'react'; import type { FEArticle } from '../frontend/feArticle'; @@ -20,6 +21,7 @@ import { } from '../lib/articleMeta'; import { getAudioData } from '../lib/audio-data'; import { getSoleContributor } from '../lib/byline'; +import { decideMainMediaCaption } from '../lib/decide-caption'; import { palette as themePalette } from '../palette'; import { hasPreferredSourceButton } from '../preferredSource'; import type { Branding as BrandingType } from '../types/branding'; @@ -27,6 +29,7 @@ import type { FEElement } from '../types/content'; import type { TagType } from '../types/tag'; import { Avatar } from './Avatar'; import { Branding } from './Branding.island'; +import { Caption } from './Caption'; import { CommentCount } from './CommentCount.island'; import { useConfig } from './ConfigContext'; import { Contributor } from './Contributor'; @@ -479,6 +482,26 @@ export const ArticleMeta = ({

{showPreferredSource ? : null} + {format.display === ArticleDisplay.Immersive && + mainMediaElements?.[0] && ( + +
+ +
+
+ )} ); diff --git a/dotcom-rendering/src/components/Caption.tsx b/dotcom-rendering/src/components/Caption.tsx index 536aea4105e..b708a9c0129 100644 --- a/dotcom-rendering/src/components/Caption.tsx +++ b/dotcom-rendering/src/components/Caption.tsx @@ -34,7 +34,6 @@ type Props = { }; type IconProps = { - format: ArticleFormat; isMainMedia?: boolean; }; @@ -161,12 +160,6 @@ const bigLeftMargin = css` } `; -const hideIconBelowLeftCol = css` - ${until.leftCol} { - display: none; - } -`; - const pictureRatio = (13 / 18) * 100; const videoRatio = (23 / 36) * 100; @@ -251,30 +244,17 @@ const galleryStyles = css` } `; -const CameraIcon = ({ format, isMainMedia }: IconProps) => { +const CameraIcon = ({ isMainMedia }: IconProps) => { return ( - + ); }; -const VideoIcon = ({ format, isMainMedia }: IconProps) => { +const VideoIcon = ({ isMainMedia }: IconProps) => { return ( - + ); @@ -325,9 +305,9 @@ export const Caption = ({ data-spacefinder-role="inline" > {mediaType === 'YoutubeVideo' || mediaType === 'SelfHostedVideo' ? ( - + ) : ( - + )} {!!captionText && (
+ {isImmersive && ( + + + + + + )} { ...standardFormat, display: ArticleDisplay.NumberedList, }; - const immersive = { - ...standardFormat, - display: ArticleDisplay.Immersive, - }; it('should return true if Standard display and Standard design', () => { expect(shouldShowContributor(standardFormat)).toBe(true); @@ -47,8 +43,4 @@ describe('shouldShowContributor', () => { it('should return true if Numbered list display', () => { expect(shouldShowContributor(numberedList)).toBe(true); }); - - it('should return false if Immersive display', () => { - expect(shouldShowContributor(immersive)).toBe(false); - }); }); From 4f66342352c701537a8c641a9015bd2b4a518788 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 29 Jul 2026 15:36:52 +0100 Subject: [PATCH 49/80] Update immersive ad count test --- dotcom-rendering/playwright/tests/commercial.e2e.spec.ts | 2 +- dotcom-rendering/playwright/tests/lightbox.e2e.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts b/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts index 61c96351056..db823202659 100644 --- a/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts +++ b/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts @@ -19,7 +19,7 @@ test.describe('Commercial E2E tests', () => { 'merchandising', ]; - const totalSlotsExpected = 15; // All slots, even if unfilled ie. `display: none` + const totalSlotsExpected = 16; // All slots, even if unfilled ie. `display: none` const inlineSlots = totalSlotsExpected - fixedSlots.length; // We are excluding survey slot as they can be switched off diff --git a/dotcom-rendering/playwright/tests/lightbox.e2e.spec.ts b/dotcom-rendering/playwright/tests/lightbox.e2e.spec.ts index 5b902439b82..f5e3becad75 100644 --- a/dotcom-rendering/playwright/tests/lightbox.e2e.spec.ts +++ b/dotcom-rendering/playwright/tests/lightbox.e2e.spec.ts @@ -109,7 +109,7 @@ test.describe('Lightbox', () => { // Open lightbox by clicking on the fourth image within the article body // Lightbox should open at the fifth image as the first image is the main media image - await page.locator('article img').nth(3).click({ force: true }); + await page.locator('button.open-lightbox').nth(4).click(); await expectToBeVisible(page, '#gu-lightbox'); From 457487da53bbaa4d9147f8289c8199ef0142ad17 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 30 Jul 2026 11:05:46 +0100 Subject: [PATCH 50/80] Update ArticleHeadline.tsx --- .../src/components/ArticleHeadline.tsx | 7 +------ .../src/components/ArticleMeta.apps.tsx | 2 +- .../src/components/ArticleMeta.web.tsx | 4 ++-- dotcom-rendering/src/layouts/StandardLayout.tsx | 1 - .../src/layouts/StandardLayoutArticleGrid.tsx | 3 --- dotcom-rendering/src/lib/articleMeta.test.ts | 2 +- dotcom-rendering/src/lib/articleMeta.ts | 14 +------------- 7 files changed, 6 insertions(+), 27 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index a5b77f30544..0d32869f847 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -46,7 +46,6 @@ type Props = { hasAvatar?: boolean; isMatch?: boolean; starRating?: Rating; - isInverted?: boolean; }; const topPadding = css` @@ -402,12 +401,8 @@ export const ArticleHeadline = ({ hasAvatar, isMatch, starRating, - isInverted, }: Props) => { - const isInverted = - format.display === ArticleDisplay.Immersive && - format.design === ArticleDesign.Standard && - layoutType === 'immersiveLandscapeDefault'; + const isInverted = layoutType === 'immersiveLandscapeDefault'; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { diff --git a/dotcom-rendering/src/components/ArticleMeta.apps.tsx b/dotcom-rendering/src/components/ArticleMeta.apps.tsx index 0eae4a9da8c..5813b6f7485 100644 --- a/dotcom-rendering/src/components/ArticleMeta.apps.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.apps.tsx @@ -331,7 +331,7 @@ export const ArticleMetaApps = ({ )} - {shouldShowContributor(format, layoutType) && !!byline && ( + {shouldShowContributor(format) && !!byline && ( )} - {shouldShowContributor(format, layoutType) && ( + {shouldShowContributor(format) && ( )} - {shouldShowContributor(format, layoutType) && ( + {shouldShowContributor(format) && ( {
diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 22034173704..78311f4c928 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -98,12 +98,10 @@ const GridItem = ({ export const StandardLayoutArticleGrid = ({ article, - contentLayoutName, format, renderingTarget, }: { article: ArticleDeprecated; - contentLayoutName: string; format: ArticleFormat; renderingTarget: RenderingTarget; }) => { @@ -330,7 +328,6 @@ export const StandardLayoutArticleGrid = ({ article.webPublicationDateDeprecated } starRating={article.starRating} - isInverted={layoutType === 'immersiveLandscapeDefault'} /> { }); it('should return false if Immersive display', () => { - expect(shouldShowContributor(immersive)).toBe(false); + expect(shouldShowContributor(immersive)).toBe(true); }); it('should return true if Immersive display and immersivePortraitDefault layout', () => { diff --git a/dotcom-rendering/src/lib/articleMeta.ts b/dotcom-rendering/src/lib/articleMeta.ts index 6f95ffe66be..f457626b366 100644 --- a/dotcom-rendering/src/lib/articleMeta.ts +++ b/dotcom-rendering/src/lib/articleMeta.ts @@ -39,23 +39,11 @@ export const shouldShowAvatar = ( } }; -export const shouldShowContributor = ( - format: ArticleFormat, - layoutType?: LayoutType, -): boolean => { +export const shouldShowContributor = (format: ArticleFormat): boolean => { switch (format.display) { case ArticleDisplay.NumberedList: return true; case ArticleDisplay.Immersive: - switch (layoutType) { - case 'immersivePortraitDefault': - case 'immersivePortraitFeature': - case 'immersiveLandscapeDefault': - case 'immersiveLandscapeFeature': - return true; - default: - return false; - } case ArticleDisplay.Showcase: case ArticleDisplay.Standard: { switch (format.design) { From 6c161193fdaf7cde5ca731130971c0d65e4bd193 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 30 Jul 2026 11:25:23 +0100 Subject: [PATCH 51/80] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- dotcom-rendering/src/layouts/StandardLayout.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index ae67aa67326..b3536cefd1f 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -380,7 +380,9 @@ export const StandardLayout = (props: WebProps | AppProps) => { /> - + {renderAds && ( + + )} )} From 8e1bd1a9061009c3175fb3fc94d22faf7e03a997 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 30 Jul 2026 11:33:45 +0100 Subject: [PATCH 52/80] Simplify `shouldShowAvatar` switch --- .../src/components/ArticleMeta.apps.tsx | 5 +---- .../src/components/ArticleMeta.web.tsx | 2 +- .../src/layouts/StandardLayoutArticleGrid.tsx | 1 - dotcom-rendering/src/lib/articleMeta.ts | 15 +-------------- 4 files changed, 3 insertions(+), 20 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.apps.tsx b/dotcom-rendering/src/components/ArticleMeta.apps.tsx index 5813b6f7485..de5333c68e5 100644 --- a/dotcom-rendering/src/components/ArticleMeta.apps.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.apps.tsx @@ -4,7 +4,6 @@ import { from, space, until } from '@guardian/source/foundations'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import type { ReactNode } from 'react'; import { grid } from '../grid'; -import type { LayoutType } from '../layouts/lib/articleArrangements'; import { interactiveLegacyClasses } from '../layouts/lib/interactiveLegacyStyling'; import { ArticleDesign, @@ -37,7 +36,6 @@ import { PodcastMeta } from './PodcastMeta'; type Props = { format: ArticleFormat; - layoutType?: LayoutType; byline?: string; tags: TagType[]; primaryDateline: string; @@ -244,7 +242,6 @@ const galleryMetaContainer = css` export const ArticleMetaApps = ({ branding, format, - layoutType, byline, tags, primaryDateline, @@ -258,7 +255,7 @@ export const ArticleMetaApps = ({ }: Props) => { const soleContributor = getSoleContributor(tags, byline); const authorName = soleContributor?.title ?? 'Author Image'; - const avatarUrl = shouldShowAvatar(format, layoutType) + const avatarUrl = shouldShowAvatar(format) ? soleContributor?.bylineLargeImageUrl : undefined; diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index 4cc582cf48a..71013659d5a 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -315,7 +315,7 @@ export const ArticleMeta = ({ const soleContributor = getSoleContributor(tags, byline); const authorName = soleContributor?.title ?? 'Author Image'; - const avatarUrl = shouldShowAvatar(format, layoutType) + const avatarUrl = shouldShowAvatar(format) ? soleContributor?.bylineLargeImageUrl : undefined; const isInteractive = format.design === ArticleDesign.Interactive; diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 78311f4c928..73ded291f8f 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -398,7 +398,6 @@ export const StandardLayoutArticleGrid = ({ { +export const shouldShowAvatar = (format: ArticleFormat): boolean => { switch (format.display) { case ArticleDisplay.Immersive: - switch (layoutType) { - case 'immersivePortraitDefault': - case 'immersivePortraitFeature': - case 'immersiveLandscapeDefault': - case 'immersiveLandscapeFeature': - return true; - default: - return false; - } case ArticleDisplay.Showcase: case ArticleDisplay.NumberedList: case ArticleDisplay.Standard: { From 265d0f26e45773c1f95bba5f2842be56ec3854b0 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:55:26 +0100 Subject: [PATCH 53/80] Remove leftover debug crop and duplicate CSS from immersive grid layout --- .../src/layouts/StandardLayoutArticleGrid.tsx | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 73ded291f8f..cfd5abacd98 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -221,26 +221,12 @@ export const StandardLayoutArticleGrid = ({ )} } `, - isImmersivePortrait && - css` - ${from.desktop} { - grid-template-rows: 0.25fr 1fr auto; - } - `, - isImmersiveLandscape && - css` - ${from.desktop} { - grid-template-rows: auto auto ${ageWarning - ? '130px' - : '90px'} auto auto auto auto auto; - } - `, ]} > Date: Thu, 30 Jul 2026 15:22:10 +0100 Subject: [PATCH 54/80] Fix immersive content overlapping the Labs header --- .../src/layouts/StandardLayoutArticleGrid.tsx | 12 +++++++----- dotcom-rendering/src/lib/getZIndex.test.ts | 6 ++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index cfd5abacd98..383bb52c193 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -32,6 +32,7 @@ import { } from '../lib/articleFormat'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideMainMediaCaption } from '../lib/decide-caption'; +import { getZIndex } from '../lib/getZIndex'; import { safeParseURL } from '../lib/parse'; import { parse } from '../lib/slot-machine-flags'; import { palette as themePalette } from '../palette'; @@ -260,7 +261,7 @@ export const StandardLayoutArticleGrid = ({ layoutType={layoutType} element="aside" css={css` - z-index: 100; + z-index: ${getZIndex('articleHeadline')}; `} > {/* Only show Listen to Article button on App landscape views */} diff --git a/dotcom-rendering/src/lib/getZIndex.test.ts b/dotcom-rendering/src/lib/getZIndex.test.ts index 19f82962d88..78bb441772c 100644 --- a/dotcom-rendering/src/lib/getZIndex.test.ts +++ b/dotcom-rendering/src/lib/getZIndex.test.ts @@ -14,6 +14,12 @@ describe('getZIndex', () => { expect(getZIndex('tableOfContents')).toBeGreaterThan( getZIndex('articleHeadline'), ); + expect(getZIndex('subNavBanner')).toBeGreaterThan( + getZIndex('articleHeadline'), + ); + expect(getZIndex('subNavBanner')).toBeGreaterThan( + getZIndex('bodyArea'), + ); expect(getZIndex('card-nested-link')).toBeGreaterThan( getZIndex('card-link'), ); From 996fd99b8e55b1fc04fd905118159936c6ef95de Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:42:53 +0100 Subject: [PATCH 55/80] Preserve immersive main media aspect ratios --- .../src/components/ImageBlockComponent.tsx | 4 +++ .../src/components/ImageComponent.tsx | 9 ++++-- .../src/layouts/StandardLayoutArticleGrid.tsx | 29 +++++++++++-------- dotcom-rendering/src/lib/renderElement.tsx | 3 ++ 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/dotcom-rendering/src/components/ImageBlockComponent.tsx b/dotcom-rendering/src/components/ImageBlockComponent.tsx index 2171767212a..fab73cf3ccd 100644 --- a/dotcom-rendering/src/components/ImageBlockComponent.tsx +++ b/dotcom-rendering/src/components/ImageBlockComponent.tsx @@ -1,3 +1,4 @@ +import type { LayoutType } from '../layouts/lib/articleArrangements'; import type { ArticleFormat } from '../lib/articleFormat'; import type { ImageBlockElement } from '../types/content'; import { ImageComponent } from './ImageComponent'; @@ -10,6 +11,7 @@ type Props = { isMainMedia?: boolean; isAvatar?: boolean; isTimeline?: boolean; + articleArrangement?: LayoutType; }; export const ImageBlockComponent = ({ @@ -20,6 +22,7 @@ export const ImageBlockComponent = ({ isMainMedia, isAvatar, isTimeline = false, + articleArrangement, }: Props) => { const { role } = element; return ( @@ -32,6 +35,7 @@ export const ImageBlockComponent = ({ title={title} isAvatar={isAvatar} isTimeline={isTimeline} + articleArrangement={articleArrangement} /> ); }; diff --git a/dotcom-rendering/src/components/ImageComponent.tsx b/dotcom-rendering/src/components/ImageComponent.tsx index acf1d4678ab..d7ad02686a9 100644 --- a/dotcom-rendering/src/components/ImageComponent.tsx +++ b/dotcom-rendering/src/components/ImageComponent.tsx @@ -9,6 +9,7 @@ import { palette as srcPalette, until, } from '@guardian/source/foundations'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { ArticleDesign, ArticleDisplay, @@ -34,6 +35,7 @@ type Props = { title?: string; isAvatar?: boolean; isTimeline?: boolean; + articleArrangement?: LayoutType; }; const timelineBulletStyles = css` @@ -244,6 +246,7 @@ export const ImageComponent = ({ title, isAvatar, isTimeline = false, + articleArrangement, }: Props) => { const { renderingTarget } = useConfig(); // Its possible the tools wont send us any images urls @@ -270,6 +273,8 @@ export const ImageComponent = ({ } const isWeb = renderingTarget === 'Web'; + const isGridImmersive = + articleArrangement?.startsWith('immersive') ?? false; /** * We use height and width for two things. @@ -303,10 +308,10 @@ export const ImageComponent = ({ always be used if display === 'immersive' */ height: 100%; width: 100%; - min-height: 25rem; + min-height: ${isGridImmersive ? 0 : '25rem'}; ${from.desktop} { - min-height: 31.25rem; + min-height: ${isGridImmersive ? 0 : '31.25rem'}; } img { diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 383bb52c193..068d60c4219 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -150,6 +150,11 @@ export const StandardLayoutArticleGrid = ({ 'model.dotcomrendering.pageElements.ImageBlockElement' ? mainMedia.media.allImages[0]?.url : undefined; + const mainMediaAspectRatio = + mainMedia?._type === + 'model.dotcomrendering.pageElements.ImageBlockElement' + ? mainMedia.media.allImages[0]?.fields.aspectRatio + : undefined; const mainMediaOrientation = mainMediaUrl != null ? getImageOrientation(mainMediaUrl) : 'landscape'; @@ -207,18 +212,13 @@ export const StandardLayoutArticleGrid = ({ isImmersiveLandscape && css` ${from.desktop} { - grid-template-rows: auto auto ${ageWarning + grid-template-rows: auto auto ${ageWarning != null ? '130px' : '90px'} auto auto auto auto auto; ${grid.centreRule( - isImmersivePortrait - ? 4 - : isImmersiveLandscape - ? layoutType === - 'immersiveLandscapeFeature' - ? 3 - : 4 - : 1, + layoutType === 'immersiveLandscapeFeature' + ? 3 + : 4, )} } `, @@ -228,11 +228,16 @@ export const StandardLayoutArticleGrid = ({ area="media" layoutType={layoutType} css={ - isImmersiveLandscape + isImmersive ? css` + align-self: start; + ${mainMediaAspectRatio != null && + `aspect-ratio: ${mainMediaAspectRatio.replace(':', ' / ')};`} + ${from.desktop} { - margin-left: -20px; - margin-right: -20px; + ${isImmersiveLandscape && + `margin-left: -20px; + margin-right: -20px;`} } ` : undefined diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx index fed8b1339dc..8414852af5f 100644 --- a/dotcom-rendering/src/lib/renderElement.tsx +++ b/dotcom-rendering/src/lib/renderElement.tsx @@ -180,6 +180,7 @@ export const renderElement = ({ shouldHideAds, contentType, contentLayout, + articleArrangement, idApiUrl, }: Props) => { const isBlog = @@ -411,6 +412,7 @@ export const renderElement = ({ title={element.title} isAvatar={element.isAvatar} isTimeline={isTimeline} + articleArrangement={articleArrangement} /> ); case 'model.dotcomrendering.pageElements.InstagramBlockElement': @@ -1081,6 +1083,7 @@ export const RenderArticleElement = ({ shouldHideAds, contentType, contentLayout, + articleArrangement, idApiUrl, }); From e96161e550653cbf3095c99b275e16fc842b05c3 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:03:54 +0100 Subject: [PATCH 56/80] Route only Labs immersive articles through the new grid --- .../src/components/ArticleHeadline.tsx | 142 ++- .../src/components/ArticleTitle.tsx | 16 +- .../src/layouts/DecideLayout.stories.tsx | 82 +- dotcom-rendering/src/layouts/DecideLayout.tsx | 26 +- .../src/layouts/ImmersiveLayout.tsx | 1022 +++++++++++++++++ dotcom-rendering/src/paletteDeclarations.ts | 19 +- 6 files changed, 1276 insertions(+), 31 deletions(-) create mode 100644 dotcom-rendering/src/layouts/ImmersiveLayout.tsx diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 0d32869f847..5d2ef46d2d3 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -28,6 +28,7 @@ import { ArticleSpecial, Pillar, } from '../lib/articleFormat'; +import { getZIndex } from '../lib/getZIndex'; import { palette as themePalette } from '../palette'; import type { StarRating as Rating } from '../types/content'; import type { TagType } from '../types/tag'; @@ -215,6 +216,56 @@ const invertedStyles = css` box-decoration-break: clone; `; +const legacyInvertedStyles = css` + white-space: pre-wrap; + padding-right: ${space[1]}px; + padding-bottom: ${space[1]}px; + box-shadow: -6px 0 0 ${themePalette('--headline-background')}; + /* Box decoration is required to push the box shadow out on Firefox */ + box-decoration-break: clone; +`; + +const legacyImmersiveStyles = css` + min-height: 112px; + padding-bottom: ${space[6]}px; + padding-left: ${space[1]}px; + + ${from.mobileLandscape} { + padding-left: ${space[3]}px; + } + + ${from.tablet} { + padding-left: ${space[1]}px; + } + + margin-right: ${space[5]}px; +`; + +const legacyImmersiveWrapper = css` + margin-left: 6px; + + ${from.tablet} { + margin-left: 16px; + } + + ${from.leftCol} { + margin-left: 25px; + } + + flex-grow: 1; + z-index: ${getZIndex('articleHeadline')}; + + ${until.mobileLandscape} { + margin-right: 40px; + } +`; + +const legacyInvertedText = css` + white-space: pre-wrap; + padding-bottom: ${space[1]}px; + padding-right: ${space[1]}px; +`; + const darkBackground = css` background-color: ${themePalette('--headline-background')}; `; @@ -253,13 +304,29 @@ const zIndex = css` z-index: 1; `; -const ageWarningMargins = (format: ArticleFormat) => { - if ( - format.design === ArticleDesign.Gallery || - format.display === ArticleDisplay.Immersive - ) { +const ageWarningMargins = ( + format: ArticleFormat, + isLegacyImmersive: boolean, +) => { + if (format.design === ArticleDesign.Gallery) { return ''; } + if (format.display === ArticleDisplay.Immersive) { + return isLegacyImmersive + ? css` + margin-left: 0; + margin-bottom: 0; + + ${from.tablet} { + margin-left: 10px; + } + + ${from.leftCol} { + margin-left: 20px; + } + ` + : ''; + } return css` margin-top: 12px; margin-left: -10px; @@ -282,12 +349,14 @@ const WithAgeWarning = ({ format, children, snapToInverted = false, + isLegacyImmersive = false, }: { tags: TagType[]; webPublicationDateDeprecated: string; format: ArticleFormat; children: React.ReactNode; snapToInverted?: boolean; + isLegacyImmersive?: boolean; }) => { const age = getAgeWarning(tags, webPublicationDateDeprecated); @@ -296,8 +365,14 @@ const WithAgeWarning = ({ <>
{ const isInverted = layoutType === 'immersiveLandscapeDefault'; + const isLegacyImmersive = layoutType == null; switch (format.display) { case ArticleDisplay.Immersive: { switch (format.design) { @@ -428,13 +504,22 @@ export const ArticleHeadline = ({ format.theme === ArticleSpecial.Labs ? labsFont : headlineFont(format), - isInverted - ? [invertedText, darkBackground] - : css` - color: ${themePalette( - '--headline-colour', - )}; - `, + isLegacyImmersive + ? [ + legacyInvertedText, + css` + color: ${themePalette( + '--headline-colour', + )}; + `, + ] + : isInverted + ? [invertedText, darkBackground] + : css` + color: ${themePalette( + '--headline-colour', + )}; + `, ]} > {headlineString} @@ -461,16 +546,27 @@ export const ArticleHeadline = ({ } format={format} snapToInverted={true} + isLegacyImmersive={isLegacyImmersive} >

diff --git a/dotcom-rendering/src/components/ArticleTitle.tsx b/dotcom-rendering/src/components/ArticleTitle.tsx index e78a65319ea..0137f523936 100644 --- a/dotcom-rendering/src/components/ArticleTitle.tsx +++ b/dotcom-rendering/src/components/ArticleTitle.tsx @@ -38,6 +38,18 @@ const immersiveMargins = css` } `; +const legacyImmersiveMargins = css` + max-width: 400px; + min-width: 200px; + margin-bottom: 4px; + ${from.tablet} { + margin-left: 16px; + } + ${from.leftCol} { + margin-left: 25px; + } +`; + const galleryStyles = css` ${grid.column.all} @@ -71,7 +83,9 @@ export const ArticleTitle = ({
diff --git a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx index e65424200ac..39a46821af7 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx @@ -229,14 +229,88 @@ export const AppsPictureShowcaseOpinionDark: Story = { * * Example: https://www.chromatic.com/test?appId=63e251470cfbe61776b0ef19&id=675aaa4f3aa384bd64bde3a1 */ +const photoEssayImmersiveLabsArticle = enhanceArticleType( + PhotoEssayImmersiveLabsFixture, + 'Web', +); + +const portraitMainMedia = PhotoEssayImmersiveLabsFixture.blocks + .flatMap((block) => block.elements) + .find( + (element) => + element._type === + 'model.dotcomrendering.pageElements.ImageBlockElement' && + element.media.allImages[0]?.fields.aspectRatio === '4:5', + ); + +if (portraitMainMedia == null) { + throw new Error('The Labs fixture must contain a portrait image'); +} + +const photoEssayImmersiveLabsPortraitArticle = enhanceArticleType( + { + ...PhotoEssayImmersiveLabsFixture, + mainMediaElements: [{ ...portraitMainMedia, role: 'immersive' }], + }, + 'Web', +); + +const labsImmersiveArticle = ({ + orientation, + design, +}: { + orientation: 'portrait' | 'landscape'; + design: ArticleDesign.PhotoEssay | ArticleDesign.Feature; +}): Article => ({ + ...(orientation === 'portrait' + ? photoEssayImmersiveLabsPortraitArticle + : photoEssayImmersiveLabsArticle), + design, +}); + +const immersiveLabsParameters = { + ...webParameters, + chromatic: { disableSnapshot: true }, +}; + export const WebPhotoEssayImmersiveLabsLight: Story = { args: { - article: enhanceArticleType(PhotoEssayImmersiveLabsFixture, 'Web'), + article: labsImmersiveArticle({ + orientation: 'landscape', + design: ArticleDesign.PhotoEssay, + }), }, - parameters: { - ...webParameters, - chromatic: { disableSnapshot: true }, + parameters: immersiveLabsParameters, +}; + +export const WebPhotoEssayImmersiveLabsPortraitLight: Story = { + args: { + article: labsImmersiveArticle({ + orientation: 'portrait', + design: ArticleDesign.PhotoEssay, + }), + }, + parameters: immersiveLabsParameters, +}; + +export const WebFeatureImmersiveLabsLandscapeLight: Story = { + args: { + article: labsImmersiveArticle({ + orientation: 'landscape', + design: ArticleDesign.Feature, + }), + }, + parameters: immersiveLabsParameters, +}; + +export const WebFeatureImmersiveLabsPortraitLight: Story = { + args: { + article: labsImmersiveArticle({ + orientation: 'portrait', + design: ArticleDesign.Feature, + }), }, + parameters: immersiveLabsParameters, }; const standardStandardLabsWebFixture: Article = { diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index 3d828bdc45b..2a763cf1e56 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -1,4 +1,8 @@ -import { ArticleDesign, ArticleDisplay } from '../lib/articleFormat'; +import { + ArticleDesign, + ArticleDisplay, + ArticleSpecial, +} from '../lib/articleFormat'; import type { NavType } from '../model/extract-nav'; import type { Article } from '../types/article'; import type { RenderingTarget } from '../types/renderingTarget'; @@ -9,6 +13,7 @@ import { GalleryLayout } from './GalleryLayout'; import { HostedArticleLayout } from './HostedArticleLayout'; import { HostedGalleryLayout } from './HostedGalleryLayout'; import { HostedVideoLayout } from './HostedVideoLayout'; +import { ImmersiveLayout } from './ImmersiveLayout'; import { InteractiveLayout } from './InteractiveLayout'; import { LiveLayout } from './LiveLayout'; import { NewsletterSignupLayout } from './NewsletterSignupLayout'; @@ -53,13 +58,20 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { ); } default: { - return ( + return article.theme === ArticleSpecial.Labs ? ( + ) : ( + ); } } @@ -229,7 +241,7 @@ const DecideLayoutWeb = ({ article, NAV, renderingTarget }: WebProps) => { ); } default: { - return ( + return article.theme === ArticleSpecial.Labs ? ( { renderingTarget={renderingTarget} serverTime={serverTime} /> + ) : ( + ); } } diff --git a/dotcom-rendering/src/layouts/ImmersiveLayout.tsx b/dotcom-rendering/src/layouts/ImmersiveLayout.tsx new file mode 100644 index 00000000000..03779fc4c3a --- /dev/null +++ b/dotcom-rendering/src/layouts/ImmersiveLayout.tsx @@ -0,0 +1,1022 @@ +import { css } from '@emotion/react'; +import { + from, + palette as sourcePalette, + space, + until, +} from '@guardian/source/foundations'; +import { StraightLines } from '@guardian/source-development-kitchen/react-components'; +import { AdPortals } from '../components/AdPortals.island'; +import { AdSlot, MobileStickyContainer } from '../components/AdSlot.web'; +import { AffiliateDisclaimer } from '../components/AffiliateDisclaimer'; +import { AppsFooter } from '../components/AppsFooter.island'; +import { ArticleBody } from '../components/ArticleBody'; +import { ArticleContainer } from '../components/ArticleContainer'; +import { ArticleHeadline } from '../components/ArticleHeadline'; +import { ArticleMetaApps } from '../components/ArticleMeta.apps'; +import { ArticleMeta } from '../components/ArticleMeta.web'; +import { ArticleTitle } from '../components/ArticleTitle'; +import { Border } from '../components/Border'; +import { Caption } from '../components/Caption'; +import { Carousel } from '../components/Carousel.island'; +import { DecideLines } from '../components/DecideLines'; +import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland'; +import { DiscussionLayout } from '../components/DiscussionLayout'; +import { Footer } from '../components/Footer'; +import { GridItem } from '../components/GridItem'; +import { GuardianLabsLines } from '../components/GuardianLabsLines'; +import { HeadlineByline } from '../components/HeadlineByline'; +import { Hide } from '../components/Hide'; +import { Island } from '../components/Island'; +import { LabsHeader } from '../components/LabsHeader'; +import { ListenToArticle } from '../components/ListenToArticle.island'; +import { MainMedia } from '../components/MainMedia'; +import { Masthead } from '../components/Masthead/Masthead'; +import { minHeaderHeightPx } from '../components/Masthead/Titlepiece/constants'; +import { MostViewedFooterData } from '../components/MostViewedFooterData.island'; +import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; +import { OnwardsUpper } from '../components/OnwardsUpper.island'; +import { RightColumn } from '../components/RightColumn'; +import { Section } from '../components/Section'; +import { SlotBodyEnd } from '../components/SlotBodyEnd.island'; +import { Standfirst } from '../components/Standfirst'; +import { StickyBottomBanner } from '../components/StickyBottomBanner.island'; +import { SubMeta } from '../components/SubMeta'; +import { SubNav } from '../components/SubNav.island'; +import { + ArticleDesign, + type ArticleFormat, + ArticleSpecial, +} from '../lib/articleFormat'; +import { canRenderAds } from '../lib/canRenderAds'; +import { getContributionsServiceUrl } from '../lib/contributions'; +import { decideMainMediaCaption } from '../lib/decide-caption'; +import { decideStoryPackageTrails } from '../lib/decideTrail'; +import { getZIndex } from '../lib/getZIndex'; +import { LABS_HEADER_HEIGHT } from '../lib/labs-constants'; +import { parse } from '../lib/slot-machine-flags'; +import type { NavType } from '../model/extract-nav'; +import { palette as themePalette } from '../palette'; +import type { ArticleDeprecated } from '../types/article'; +import { BannerWrapper, Stuck } from './lib/stickiness'; + +const ImmersiveGrid = ({ children }: { children: React.ReactNode }) => ( +
+ {children} +
+); + +const maxWidth = css` + ${from.desktop} { + max-width: 620px; + } +`; + +const linesMargin = css` + ${from.leftCol} { + margin-top: ${space[5]}px; + } +`; + +const stretchLines = css` + ${until.phablet} { + margin-left: -20px; + margin-right: -20px; + } + ${until.mobileLandscape} { + margin-left: -10px; + margin-right: -10px; + } +`; + +interface CommonProps { + article: ArticleDeprecated; + format: ArticleFormat; + serverTime?: number; +} + +interface WebProps extends CommonProps { + NAV: NavType; + renderingTarget: 'Web'; +} + +interface AppProps extends CommonProps { + renderingTarget: 'Apps'; +} + +const Box = ({ children }: { children: React.ReactNode }) => ( +
+ {children} +
+); + +export const ImmersiveLayout = (props: WebProps | AppProps) => { + const { article, format, renderingTarget, serverTime } = props; + + const { + config: { isPaidContent, host, hasSurveyAd }, + editionId, + } = article; + const isWeb = renderingTarget === 'Web'; + const isApps = renderingTarget === 'Apps'; + + const showBodyEndSlot = + isWeb && + (parse(article.slotMachineFlags ?? '').showBodyEnd || + article.config.switches.slotBodyEnd); + + // TODO: + // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render + // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. + + const showComments = article.isCommentable && !isPaidContent; + + const mainMedia = article.mainMediaElements[0]; + + const captionText = decideMainMediaCaption(mainMedia); + + const HEADLINE_OFFSET = mainMedia ? 120 : 0; + + const { branding } = article.commercialProperties[article.editionId]; + + const contributionsServiceUrl = getContributionsServiceUrl(article); + + const isLabs = format.theme === ArticleSpecial.Labs; + + /** + We need change the height values depending on whether the labs header is there or not to keep + the headlines appearing at a consistent height between labs and non labs immersive articles. + */ + + const labsHeaderHeight = LABS_HEADER_HEIGHT; + const combinedHeight = (minHeaderHeightPx + labsHeaderHeight).toString(); + + const navAndLabsHeaderHeight = isLabs + ? `${combinedHeight}px` + : `${minHeaderHeightPx}px`; + + const hasMainMediaStyles = css` + height: calc(80vh - ${navAndLabsHeaderHeight}); + /** + 80vh is normally enough but don't let the content shrink vertically too + much just in case + */ + min-height: calc(25rem - ${navAndLabsHeaderHeight}); + ${from.desktop} { + height: calc(100vh - ${navAndLabsHeaderHeight}); + min-height: calc(31.25rem - ${navAndLabsHeaderHeight}); + } + ${from.wide} { + min-height: calc(50rem - ${navAndLabsHeaderHeight}); + } + `; + const LeftColCaption = () => ( +
+ +
+ ); + + const renderAds = canRenderAds(article); + + return ( + <> + {isWeb && ( + tag.id)} + sectionId={article.config.section} + contentType={article.contentType} + /> + )} + + + + {format.theme === ArticleSpecial.Labs && ( + +
+ +
+
+ )} + +
+
+ +
+ {mainMedia && ( + <> +
+
} + > + +
+ +
+ +
+
+
+ + )} +
+ + {isWeb && renderAds && hasSurveyAd && ( + + )} + +
+ {isApps && renderAds && ( + + + + )} +
+ + {/* Above leftCol, the Caption is controlled by Section ^^ */} + + + + + + + {format.design === ArticleDesign.PhotoEssay ? ( + <> + ) : ( + + )} + + + <> + {!mainMedia && ( +
+ +
+ )} + +
+ + <> + {!mainMedia && ( +
+ +
+ )} + +
+ + + + + {!!article.byline && ( + + )} + {/* Only show Listen to Article button on App landscape views */} + {isApps && ( + +
+ + + +
+
+ )} +
+ + {format.design === ArticleDesign.PhotoEssay && + !isLabs ? ( + <> + ) : ( +
+
+ {format.theme === + ArticleSpecial.Labs ? ( + + ) : ( + + )} +
+
+ )} +
+ {isApps ? ( + <> + + + + + + {!!article.affiliateLinksDisclaimer && ( + + )} + + + ) : ( + <> + + {!!article.affiliateLinksDisclaimer && ( + + )} + + )} +
+
+ + + + {showBodyEndSlot && ( + + + + )} + + + + + +
+ + <> + {mainMedia && isWeb && renderAds && ( +
+ { + + } +
+ )} + +
+
+
+
+
+ {!isLabs && isWeb && renderAds && ( +
+ +
+ )} + + {article.storyPackage && ( +
+ + + +
+ )} + + + + + + {showComments && ( +
+ +
+ )} + {!isPaidContent && ( +
+ + + + + +
+ )} + {!isLabs && isWeb && renderAds && ( +
+ +
+ )} +
+ + {isWeb && props.NAV.subNavSections && ( +
+ + + +
+ )} + + {isWeb && ( + <> +
+
+
+ + + + + + + {renderAds && ( + + )} + + )} + {isApps && ( +
+ + + +
+ )} + + ); +}; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index b0bf5c22d02..6efcfbf0033 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -88,7 +88,9 @@ const textblockTextDark: PaletteFunction = () => 'inherit'; const headlineTextLight: PaletteFunction = ({ design, display, theme }) => { switch (display) { case ArticleDisplay.Immersive: - return sourcePalette.neutral[7]; + return theme === ArticleSpecial.Labs + ? sourcePalette.neutral[7] + : sourcePalette.neutral[97]; default: { switch (design) { case ArticleDesign.Editorial: @@ -195,8 +197,21 @@ const headlineMatchTextLight: PaletteFunction = (format) => const headlineMatchTextDark: PaletteFunction = (format) => seriesTitleMatchTextDark(format); -const headlineBackgroundLight: PaletteFunction = ({ display, design }) => { +const headlineBackgroundLight: PaletteFunction = ({ + display, + design, + theme, +}) => { switch (display) { + case ArticleDisplay.Immersive: + switch (theme) { + case ArticleSpecial.Labs: + return 'transparent'; + case ArticleSpecial.SpecialReport: + return sourcePalette.specialReport[300]; + default: + return sourcePalette.neutral[7]; + } case ArticleDisplay.Showcase: case ArticleDisplay.NumberedList: case ArticleDisplay.Standard: From 12c9356a6525cefeff7236c907d87e8ce44e9c92 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:11:03 +0100 Subject: [PATCH 57/80] Update DecideLayout.stories.tsx --- dotcom-rendering/src/layouts/DecideLayout.stories.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx index 39a46821af7..db337c329ed 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx @@ -27,6 +27,7 @@ import { import { getCurrentPillar } from '../lib/layoutHelpers'; import { extractNAV } from '../model/extract-nav'; import { type Article, enhanceArticleType } from '../types/article'; +import type { ImageBlockElement } from '../types/content'; import { DecideLayout, type Props as DecideLayoutProps } from './DecideLayout'; export type HydratedLayoutDecoratorArgs = { @@ -237,7 +238,7 @@ const photoEssayImmersiveLabsArticle = enhanceArticleType( const portraitMainMedia = PhotoEssayImmersiveLabsFixture.blocks .flatMap((block) => block.elements) .find( - (element) => + (element): element is ImageBlockElement => element._type === 'model.dotcomrendering.pageElements.ImageBlockElement' && element.media.allImages[0]?.fields.aspectRatio === '4:5', @@ -250,7 +251,7 @@ if (portraitMainMedia == null) { const photoEssayImmersiveLabsPortraitArticle = enhanceArticleType( { ...PhotoEssayImmersiveLabsFixture, - mainMediaElements: [{ ...portraitMainMedia, role: 'immersive' }], + mainMediaElements: [portraitMainMedia], }, 'Web', ); From 09f4a738d6dc57b3ad51da698be7fef5fde74d8f Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:42:20 +0100 Subject: [PATCH 58/80] Update playwright assertion --- dotcom-rendering/playwright/tests/commercial.e2e.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts b/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts index db823202659..61c96351056 100644 --- a/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts +++ b/dotcom-rendering/playwright/tests/commercial.e2e.spec.ts @@ -19,7 +19,7 @@ test.describe('Commercial E2E tests', () => { 'merchandising', ]; - const totalSlotsExpected = 16; // All slots, even if unfilled ie. `display: none` + const totalSlotsExpected = 15; // All slots, even if unfilled ie. `display: none` const inlineSlots = totalSlotsExpected - fixedSlots.length; // We are excluding survey slot as they can be switched off From 077c34815ced822b4fef7f5e21958fec21caf548 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:10:43 +0100 Subject: [PATCH 59/80] Fixes and reverts --- .../src/components/ArticleMeta.web.tsx | 39 +++++++++---------- .../src/components/Standfirst.tsx | 19 ++++++++- .../src/layouts/StandardLayoutArticleGrid.tsx | 8 +++- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index 71013659d5a..85b586f2c21 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -566,26 +566,25 @@ export const ArticleMeta = ({

{showPreferredSource ? : null} - {format.display === ArticleDisplay.Immersive && - mainMediaElements?.[0] && ( - -
- -
-
- )} + {isPortraitOrLandscapeImmersive && mainMediaElements?.[0] && ( + +
+ +
+
+ )}
); diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index c517894fcaa..da5fb9130b1 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -15,6 +15,7 @@ import { } from '@guardian/source/foundations'; import sanitise from 'sanitize-html'; import { grid } from '../../src/grid'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { interactiveLegacyClasses } from '../layouts/lib/interactiveLegacyStyling'; import { ArticleDesign, @@ -27,6 +28,7 @@ import { palette } from '../palette'; type Props = { format: ArticleFormat; standfirst: string; + layoutType?: LayoutType; }; const nestedStyles = (format: ArticleFormat) => { @@ -213,6 +215,7 @@ const decidePadding = ({ display, design }: ArticleFormat) => { case ArticleDisplay.Immersive: return css` padding-bottom: 0; + padding-top: ${space[2]}px; ${from.tablet} { padding-bottom: 0; @@ -237,6 +240,7 @@ const decidePadding = ({ display, design }: ArticleFormat) => { case ArticleDisplay.Immersive: return css` padding-bottom: 0; + padding-top: ${space[2]}px; ${from.tablet} { padding-bottom: 0; @@ -266,6 +270,7 @@ const standfirstStyles = ({ display, design, theme }: ArticleFormat) => { `; default: return css` + max-width: 280px; ${from.tablet} { max-width: 460px; } @@ -387,7 +392,16 @@ const hoverStyles = css` } `; -export const Standfirst = ({ format, standfirst }: Props) => { +const immersiveGridOverrides = css` + padding-top: 0; + max-width: none; + + ${from.tablet} { + max-width: none; + } +`; + +export const Standfirst = ({ format, standfirst, layoutType }: Props) => { if (standfirst.trim() === '') { return null; } @@ -400,6 +414,9 @@ export const Standfirst = ({ format, standfirst }: Props) => { decideFont(format), decidePadding(format), hoverStyles, + layoutType?.startsWith('immersive') + ? immersiveGridOverrides + : undefined, ]} className={ format.design === ArticleDesign.Interactive diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index b629d40bb89..30eabcd7800 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -199,7 +199,7 @@ export const StandardLayoutArticleGrid = ({ 'immersiveLandscapeFeature' ? 3 : 4 - : 1, + : 3, )} } `, @@ -329,7 +329,11 @@ export const StandardLayoutArticleGrid = ({ `, ]} > - +
{isImmersive && ( Date: Mon, 3 Aug 2026 15:53:17 +0100 Subject: [PATCH 60/80] Fix legacy styles --- .../src/components/ArticleMeta.apps.tsx | 21 +++++--- .../src/components/ArticleMeta.web.tsx | 10 ++-- dotcom-rendering/src/components/Caption.tsx | 53 ++++++++++++++++--- .../src/components/DecideLines.tsx | 8 +-- dotcom-rendering/src/grid.ts | 2 - .../src/layouts/StandardLayoutArticleGrid.tsx | 10 ++++ dotcom-rendering/src/lib/articleMeta.test.ts | 35 ++---------- dotcom-rendering/src/lib/articleMeta.ts | 18 ++++++- 8 files changed, 97 insertions(+), 60 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.apps.tsx b/dotcom-rendering/src/components/ArticleMeta.apps.tsx index de5333c68e5..9137b14e906 100644 --- a/dotcom-rendering/src/components/ArticleMeta.apps.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.apps.tsx @@ -4,6 +4,7 @@ import { from, space, until } from '@guardian/source/foundations'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import type { ReactNode } from 'react'; import { grid } from '../grid'; +import type { LayoutType } from '../layouts/lib/articleArrangements'; import { interactiveLegacyClasses } from '../layouts/lib/interactiveLegacyStyling'; import { ArticleDesign, @@ -36,6 +37,7 @@ import { PodcastMeta } from './PodcastMeta'; type Props = { format: ArticleFormat; + layoutType?: LayoutType; byline?: string; tags: TagType[]; primaryDateline: string; @@ -242,6 +244,7 @@ const galleryMetaContainer = css` export const ArticleMetaApps = ({ branding, format, + layoutType, byline, tags, primaryDateline, @@ -253,9 +256,10 @@ export const ArticleMetaApps = ({ headline, mainMediaElements, }: Props) => { + const isImmersiveGrid = layoutType?.startsWith('immersive') ?? false; const soleContributor = getSoleContributor(tags, byline); const authorName = soleContributor?.title ?? 'Author Image'; - const avatarUrl = shouldShowAvatar(format) + const avatarUrl = shouldShowAvatar(format, isImmersiveGrid) ? soleContributor?.bylineLargeImageUrl : undefined; @@ -328,13 +332,14 @@ export const ArticleMetaApps = ({ )} - {shouldShowContributor(format) && !!byline && ( - - )} + {shouldShowContributor(format, isImmersiveGrid) && + !!byline && ( + + )} {shouldShowFollowButtons( isComment || isAnalysis || isImmersive, ) && diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index 85b586f2c21..da0f1925e55 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -312,10 +312,11 @@ export const ArticleMeta = ({ renderingTarget, format, ); + const isImmersiveGrid = layoutType?.startsWith('immersive') ?? false; const soleContributor = getSoleContributor(tags, byline); const authorName = soleContributor?.title ?? 'Author Image'; - const avatarUrl = shouldShowAvatar(format) + const avatarUrl = shouldShowAvatar(format, isImmersiveGrid) ? soleContributor?.bylineLargeImageUrl : undefined; const isInteractive = format.design === ArticleDesign.Interactive; @@ -399,7 +400,7 @@ export const ArticleMeta = ({ /> )} - {shouldShowContributor(format) && ( + {shouldShowContributor(format, isImmersiveGrid) && ( )} - {shouldShowContributor(format) && ( + {shouldShowContributor( + format, + isImmersiveGrid, + ) && ( css` @@ -160,6 +163,12 @@ const bigLeftMargin = css` } `; +const hideIconBelowLeftCol = css` + ${until.leftCol} { + display: none; + } +`; + const pictureRatio = (13 / 18) * 100; const videoRatio = (23 / 36) * 100; @@ -244,17 +253,40 @@ const galleryStyles = css` } `; -const CameraIcon = ({ isMainMedia }: IconProps) => { +const CameraIcon = ({ + format, + isMainMedia, + showIconBelowLeftCol, +}: IconProps) => { return ( - + ); }; -const VideoIcon = ({ isMainMedia }: IconProps) => { +const VideoIcon = ({ + format, + isMainMedia, + showIconBelowLeftCol, +}: IconProps) => { return ( - + ); @@ -272,6 +304,7 @@ export const Caption = ({ mediaType = 'Gallery', isMainMedia = false, isImmersive = false, + showIconBelowLeftCol = false, }: Props) => { // Sometimes captions come thorough as a single blank space, so we trim here to ignore those const noCaption = !captionText?.trim(); @@ -305,9 +338,17 @@ export const Caption = ({ data-spacefinder-role="inline" > {mediaType === 'YoutubeVideo' || mediaType === 'SelfHostedVideo' ? ( - + ) : ( - + )} {!!captionText && ( { - const count = - format.design === ArticleDesign.Comment - ? 8 - : format.display === ArticleDisplay.Immersive - ? 1 - : 4; + const count = format.design === ArticleDesign.Comment ? 8 : 4; switch (format.theme) { case Pillar.Sport: diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index 5e7a0111455..a02e5c3f861 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -159,7 +159,6 @@ const outerRules = (color?: string): string => ` &::before { grid-column: centre-column-start; transform: translateX(-${columnGap}); - z-index: 10; ${fromBreakpoint.leftCol} { grid-column: left-column-start; @@ -170,7 +169,6 @@ const outerRules = (color?: string): string => ` &::after { grid-column: right-column-end; transform: translateX(-1px); - z-index: 10; ${betweenBreakpoint.tablet.and.desktop} { grid-column: centre-column-end; diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 30eabcd7800..d46a2d8a385 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -188,6 +188,14 @@ export const StandardLayoutArticleGrid = ({ `, grid.container, grid.outerRules(), + isLabs && + isImmersive && + css` + &::before, + &::after { + z-index: 10; + } + `, !isLabs && css` ${from.leftCol} { @@ -350,6 +358,7 @@ export const StandardLayoutArticleGrid = ({ shouldLimitWidth={false} isLeftCol={true} isMainMedia={true} + showIconBelowLeftCol={true} /> @@ -390,6 +399,7 @@ export const StandardLayoutArticleGrid = ({ { display: ArticleDisplay.Immersive, }; - const immersivePortraitDefault = { - ...immersive, - layoutType: 'immersivePortraitDefault', - }; - const immersivePortraitFeature = { - ...immersive, - layoutType: 'immersivePortraitFeature', - }; - const immersiveLandscapeDefault = { - ...immersive, - layoutType: 'immersiveLandscapeDefault', - }; - const immersiveLandscapeFeature = { - ...immersive, - layoutType: 'immersiveLandscapeFeature', - }; - it('should return true if Standard display and Standard design', () => { expect(shouldShowContributor(standardFormat)).toBe(true); }); @@ -67,22 +50,10 @@ describe('shouldShowContributor', () => { }); it('should return false if Immersive display', () => { - expect(shouldShowContributor(immersive)).toBe(true); - }); - - it('should return true if Immersive display and immersivePortraitDefault layout', () => { - expect(shouldShowContributor(immersivePortraitDefault)).toBe(true); - }); - - it('should return true if Immersive display and immersivePortraitFeature layout', () => { - expect(shouldShowContributor(immersivePortraitFeature)).toBe(true); - }); - - it('should return true if Immersive display and immersiveLandscapeDefault layout', () => { - expect(shouldShowContributor(immersiveLandscapeDefault)).toBe(true); + expect(shouldShowContributor(immersive)).toBe(false); }); - it('should return true if Immersive display and immersiveLandscapeFeature layout', () => { - expect(shouldShowContributor(immersiveLandscapeFeature)).toBe(true); + it('should return true if Immersive display uses the new grid', () => { + expect(shouldShowContributor(immersive, true)).toBe(true); }); }); diff --git a/dotcom-rendering/src/lib/articleMeta.ts b/dotcom-rendering/src/lib/articleMeta.ts index 23b51b8a264..54c8494ffab 100644 --- a/dotcom-rendering/src/lib/articleMeta.ts +++ b/dotcom-rendering/src/lib/articleMeta.ts @@ -5,7 +5,14 @@ import { type ArticleFormat, } from './articleFormat'; -export const shouldShowAvatar = (format: ArticleFormat): boolean => { +export const shouldShowAvatar = ( + format: ArticleFormat, + isImmersiveGrid = false, +): boolean => { + if (format.display === ArticleDisplay.Immersive && !isImmersiveGrid) { + return false; + } + switch (format.display) { case ArticleDisplay.Immersive: case ArticleDisplay.Showcase: @@ -26,7 +33,14 @@ export const shouldShowAvatar = (format: ArticleFormat): boolean => { } }; -export const shouldShowContributor = (format: ArticleFormat): boolean => { +export const shouldShowContributor = ( + format: ArticleFormat, + isImmersiveGrid = false, +): boolean => { + if (format.display === ArticleDisplay.Immersive && !isImmersiveGrid) { + return false; + } + switch (format.display) { case ArticleDisplay.NumberedList: return true; From 1e3c43e3fa280f12854d1d954ed29346fd29763c Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:13:27 +0100 Subject: [PATCH 61/80] Fix grid index --- .../src/components/ArticleHeadline.tsx | 2 +- .../src/layouts/StandardLayoutArticleGrid.tsx | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 5d2ef46d2d3..7549d4b4cd6 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -545,7 +545,7 @@ export const ArticleHeadline = ({ webPublicationDateDeprecated } format={format} - snapToInverted={true} + snapToInverted={isInverted} isLegacyImmersive={isLegacyImmersive} >

Date: Mon, 3 Aug 2026 16:30:54 +0100 Subject: [PATCH 62/80] Use isImmersive --- dotcom-rendering/src/components/ArticleMeta.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index da0f1925e55..58d2a510d28 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -570,7 +570,7 @@ export const ArticleMeta = ({ {showPreferredSource ? : null} - {isPortraitOrLandscapeImmersive && mainMediaElements?.[0] && ( + {isImmersive && mainMediaElements?.[0] && (
Date: Tue, 4 Aug 2026 15:17:45 +0100 Subject: [PATCH 63/80] Add immersive labs dark mode stories and new labs colours --- .../src/components/LabsHeader.tsx | 49 ++++++++++++---- .../src/layouts/DecideLayout.stories.tsx | 57 +++++++++++++++++-- .../src/layouts/StandardLayout.tsx | 8 ++- dotcom-rendering/src/paletteDeclarations.ts | 56 ++++++++++++++---- 4 files changed, 142 insertions(+), 28 deletions(-) diff --git a/dotcom-rendering/src/components/LabsHeader.tsx b/dotcom-rendering/src/components/LabsHeader.tsx index 77076f444b8..f443ef136c9 100644 --- a/dotcom-rendering/src/components/LabsHeader.tsx +++ b/dotcom-rendering/src/components/LabsHeader.tsx @@ -17,7 +17,13 @@ import { LABS_HEADER_HEIGHT } from '../lib/labs-constants'; import LabsLogo from '../static/logos/the-guardian-labs.svg'; import { Details } from './Details'; -const FlexWrapper = ({ children }: { children: React.ReactNode }) => ( +const FlexWrapper = ({ + children, + textColour, +}: { + children: React.ReactNode; + textColour: string; +}) => (
( display: flex; justify-content: space-between; - color: ${palette.neutral[7]}; + color: ${textColour}; `} > {children} @@ -90,11 +96,17 @@ const Title = () => (
); -const About = () => ( +const About = ({ + backgroundColour, + textColour, +}: { + backgroundColour: string; + textColour: string; +}) => (
( ${from.mobileLandscape} { padding: ${space[3]}px 20px; } - - > a { - color: black; - } `} >

@@ -125,6 +133,7 @@ const About = () => ( iconSide="right" size="xsmall" priority="subdued" + theme={{ textSubdued: textColour }} icon={} href="https://www.theguardian.com/info/2016/jan/25/content-funding" > @@ -138,13 +147,28 @@ const Logo = ({ editionId }: { editionId: EditionId }) => ( href={`https://www.theguardian.com/guardian-labs${getLabsUrlSuffix( editionId, )}`} + cssOverrides={css` + display: flex; + color: inherit; + svg { + fill: currentColor; + } + `} > ); -export const LabsHeader = ({ editionId }: { editionId: EditionId }) => ( - +export const LabsHeader = ({ + editionId, + textColour = palette.neutral[7], + backgroundColour = palette.labs[400], +}: { + editionId: EditionId; + textColour?: string; + backgroundColour?: string; +}) => ( + @@ -166,7 +190,10 @@ export const LabsHeader = ({ editionId }: { editionId: EditionId }) => ( } `} > - <About /> + <About + backgroundColour={backgroundColour} + textColour={textColour} + /> </Details> </HeaderSection> </Left> diff --git a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx index db337c329ed..d3eb850a2ff 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx @@ -51,11 +51,7 @@ const HydratedLayout: Decorator< display: article.display, theme: article.theme, }; - const colourScheme = - (isObject(context.parameters.config) && - context.parameters.config.renderingTarget === 'Apps' - ? context.args.colourScheme - : 'light') ?? 'light'; + const colourScheme = context.args.colourScheme ?? 'light'; const paletteDecorator = colourSchemeDecorator( colourScheme, )<DecideLayoutProps>([format]); @@ -119,6 +115,13 @@ const webParameters = { }, }; +const webDarkParameters = { + config: { + renderingTarget: 'Web', + darkModeAvailable: true, + }, +}; + export const WebStandardStandardNewsLight = { args: { article: enhanceArticleType(StandardStandardNewsFixture, 'Web'), @@ -284,6 +287,17 @@ export const WebPhotoEssayImmersiveLabsLight: Story = { parameters: immersiveLabsParameters, }; +export const WebPhotoEssayImmersiveLabsDark: Story = { + args: { + article: WebPhotoEssayImmersiveLabsLight.args?.article, + colourScheme: 'dark', + }, + parameters: { + ...immersiveLabsParameters, + ...webDarkParameters, + }, +}; + export const WebPhotoEssayImmersiveLabsPortraitLight: Story = { args: { article: labsImmersiveArticle({ @@ -294,6 +308,17 @@ export const WebPhotoEssayImmersiveLabsPortraitLight: Story = { parameters: immersiveLabsParameters, }; +export const WebPhotoEssayImmersiveLabsPortraitDark: Story = { + args: { + article: WebPhotoEssayImmersiveLabsPortraitLight.args?.article, + colourScheme: 'dark', + }, + parameters: { + ...immersiveLabsParameters, + ...webDarkParameters, + }, +}; + export const WebFeatureImmersiveLabsLandscapeLight: Story = { args: { article: labsImmersiveArticle({ @@ -304,6 +329,17 @@ export const WebFeatureImmersiveLabsLandscapeLight: Story = { parameters: immersiveLabsParameters, }; +export const WebFeatureImmersiveLabsLandscapeDark: Story = { + args: { + article: WebFeatureImmersiveLabsLandscapeLight.args?.article, + colourScheme: 'dark', + }, + parameters: { + ...immersiveLabsParameters, + ...webDarkParameters, + }, +}; + export const WebFeatureImmersiveLabsPortraitLight: Story = { args: { article: labsImmersiveArticle({ @@ -314,6 +350,17 @@ export const WebFeatureImmersiveLabsPortraitLight: Story = { parameters: immersiveLabsParameters, }; +export const WebFeatureImmersiveLabsPortraitDark: Story = { + args: { + article: WebFeatureImmersiveLabsPortraitLight.args?.article, + colourScheme: 'dark', + }, + parameters: { + ...immersiveLabsParameters, + ...webDarkParameters, + }, +}; + const standardStandardLabsWebFixture: Article = { ...WebStandardStandardNewsLight.args.article, theme: ArticleSpecial.Labs, diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index b3536cefd1f..764314e7381 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -135,12 +135,16 @@ export const StandardLayout = (props: WebProps | AppProps) => { <Section fullWidth={true} showTopBorder={false} - backgroundColour={sourcePalette.labs[400]} + backgroundColour={sourcePalette.labs[100]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" element="aside" > - <LabsHeader editionId={editionId} /> + <LabsHeader + editionId={editionId} + textColour={sourcePalette.neutral[100]} + backgroundColour={sourcePalette.labs[100]} + /> </Section> </Stuck> )} diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 6efcfbf0033..b332910defa 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -1286,6 +1286,10 @@ const followIconBackgroundLight: PaletteFunction = ({ design, theme }) => { } }; const followIconBackgroundDark: PaletteFunction = ({ theme, design }) => { + if (theme === ArticleSpecial.Labs) { + return sourcePalette.neutral[7]; + } + switch (design) { case ArticleDesign.DeadBlog: return sourcePalette.neutral[7]; @@ -1315,6 +1319,10 @@ const followIconBackgroundDark: PaletteFunction = ({ theme, design }) => { }; const followIconFillLight: PaletteFunction = ({ design, theme }) => { + if (theme === ArticleSpecial.Labs) { + return sourcePalette.labs[200]; + } + switch (design) { case ArticleDesign.Gallery: case ArticleDesign.HostedGallery: @@ -1424,6 +1432,10 @@ const followIconFillLight: PaletteFunction = ({ design, theme }) => { } }; const followIconFillDark: PaletteFunction = ({ theme, design }) => { + if (theme === ArticleSpecial.Labs) { + return sourcePalette.labs[500]; + } + switch (design) { case ArticleDesign.LiveBlog: return sourcePalette.neutral[93]; @@ -2474,6 +2486,10 @@ const standfirstLinkTextDark: PaletteFunction = ({ design, theme }) => { }; const standfirstTextLight: PaletteFunction = (format) => { + if (format.theme === ArticleSpecial.Labs) { + return sourcePalette.labs[100]; + } + switch (format.design) { case ArticleDesign.LiveBlog: return sourcePalette.neutral[100]; @@ -2501,6 +2517,10 @@ const standfirstTextLight: PaletteFunction = (format) => { }; const standfirstTextDark: PaletteFunction = ({ design, display, theme }) => { + if (theme === ArticleSpecial.Labs) { + return sourcePalette.labs[500]; + } + switch (design) { case ArticleDesign.LiveBlog: case ArticleDesign.DeadBlog: @@ -3927,6 +3947,10 @@ const shareButtonCopiedLight: PaletteFunction = ({ design }) => { const shareButtonCopiedDark: PaletteFunction = () => sourcePalette.neutral[86]; const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { + if (theme === ArticleSpecial.Labs) { + return sourcePalette.labs[200]; + } + switch (design) { case ArticleDesign.Gallery: case ArticleDesign.HostedGallery: @@ -3994,6 +4018,10 @@ const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { }; const shareButtonDark: PaletteFunction = ({ design, theme }) => { + if (theme === ArticleSpecial.Labs) { + return sourcePalette.labs[500]; + } + switch (design) { case ArticleDesign.Gallery: case ArticleDesign.HostedArticle: @@ -4975,6 +5003,13 @@ const seriesTitleBackgroundLight: PaletteFunction = ({ if (theme === ArticleSpecial.SpecialReport) { return sourcePalette.brandAlt[400]; } + if ( + theme === ArticleSpecial.Labs && + design !== ArticleDesign.Gallery && + design !== ArticleDesign.HostedGallery + ) { + return sourcePalette.labs[200]; + } switch (display) { case ArticleDisplay.Immersive: switch (theme) { @@ -4984,8 +5019,6 @@ const seriesTitleBackgroundLight: PaletteFunction = ({ case Pillar.Culture: case Pillar.Lifestyle: return pillarPalette(theme, 400); - case ArticleSpecial.Labs: - return sourcePalette.labs[300]; case ArticleSpecial.SpecialReportAlt: return sourcePalette.specialReportAlt[300]; } @@ -5040,6 +5073,9 @@ const seriesTitleBackgroundDark: PaletteFunction = ({ if (theme === ArticleSpecial.SpecialReport) { return sourcePalette.brandAlt[400]; } + if (theme === ArticleSpecial.Labs) { + return sourcePalette.labs[500]; + } switch (display) { case ArticleDisplay.Immersive: switch (theme) { @@ -5049,8 +5085,6 @@ const seriesTitleBackgroundDark: PaletteFunction = ({ case Pillar.Culture: case Pillar.Lifestyle: return pillarPalette(theme, 400); - case ArticleSpecial.Labs: - return sourcePalette.labs[300]; case ArticleSpecial.SpecialReportAlt: return sourcePalette.specialReportAlt[300]; } @@ -5090,7 +5124,7 @@ const sectionTitleBackgroundLight: PaletteFunction = ({ theme, display }) => { }; const seriesTitleTextLight: PaletteFunction = ({ theme, display, design }) => { if (theme === ArticleSpecial.Labs && design !== ArticleDesign.LiveBlog) { - return sourcePalette.neutral[7]; + return sourcePalette.neutral[100]; } if ( theme === ArticleSpecial.SpecialReportAlt && @@ -5197,6 +5231,13 @@ const seriesTitleTextLight: PaletteFunction = ({ theme, display, design }) => { } }; const seriesTitleTextDark: PaletteFunction = ({ design, theme, display }) => { + if ( + theme === ArticleSpecial.Labs && + design !== ArticleDesign.Gallery && + design !== ArticleDesign.HostedGallery + ) { + return sourcePalette.neutral[7]; + } if (display === ArticleDisplay.Immersive) return sourcePalette.neutral[100]; switch (design) { case ArticleDesign.Gallery: @@ -5213,7 +5254,6 @@ const seriesTitleTextDark: PaletteFunction = ({ design, theme, display }) => { case ArticleSpecial.SpecialReportAlt: return sourcePalette.specialReportAlt[700]; case ArticleSpecial.SpecialReport: - case ArticleSpecial.Labs: return sourcePalette.news[500]; } case ArticleDesign.Standard: @@ -5233,8 +5273,6 @@ const seriesTitleTextDark: PaletteFunction = ({ design, theme, display }) => { case Pillar.Culture: case Pillar.Lifestyle: return pillarPalette(theme, 500); - case ArticleSpecial.Labs: - return sourcePalette.labs[400]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[500]; case ArticleSpecial.SpecialReportAlt: @@ -5249,8 +5287,6 @@ const seriesTitleTextDark: PaletteFunction = ({ design, theme, display }) => { case Pillar.Culture: case Pillar.Lifestyle: return pillarPalette(theme, 500); - case ArticleSpecial.Labs: - return sourcePalette.labs[400]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[500]; case ArticleSpecial.SpecialReportAlt: From 3e1ac3549d8e0f5a36688532f6a4ebc250015554 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:25:43 +0100 Subject: [PATCH 64/80] Update paletteDeclarations.ts --- dotcom-rendering/src/paletteDeclarations.ts | 53 +++++---------------- 1 file changed, 11 insertions(+), 42 deletions(-) diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index b332910defa..e7183fe4293 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -1369,8 +1369,6 @@ const followIconFillLight: PaletteFunction = ({ design, theme }) => { return sourcePalette.culture[400]; case Pillar.Lifestyle: return sourcePalette.lifestyle[400]; - case ArticleSpecial.Labs: - return sourcePalette.labs[300]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[300]; case ArticleSpecial.SpecialReportAlt: @@ -1401,8 +1399,6 @@ const followIconFillLight: PaletteFunction = ({ design, theme }) => { return sourcePalette.culture[400]; case Pillar.Lifestyle: return sourcePalette.lifestyle[400]; - case ArticleSpecial.Labs: - return sourcePalette.labs[300]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[300]; case ArticleSpecial.SpecialReportAlt: @@ -1441,8 +1437,6 @@ const followIconFillDark: PaletteFunction = ({ theme, design }) => { return sourcePalette.neutral[93]; case ArticleDesign.Standard: switch (theme) { - case ArticleSpecial.Labs: - return sourcePalette.labs[300]; case Pillar.Opinion: return sourcePalette.opinion[500]; case Pillar.Sport: @@ -1480,8 +1474,6 @@ const followIconFillDark: PaletteFunction = ({ theme, design }) => { return sourcePalette.culture[500]; case Pillar.Lifestyle: return sourcePalette.lifestyle[500]; - case ArticleSpecial.Labs: - return sourcePalette.labs[400]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[700]; case ArticleSpecial.SpecialReportAlt: @@ -1500,8 +1492,6 @@ const followIconFillDark: PaletteFunction = ({ theme, design }) => { return sourcePalette.culture[500]; case Pillar.Lifestyle: return sourcePalette.lifestyle[500]; - case ArticleSpecial.Labs: - return sourcePalette.labs[300]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[700]; case ArticleSpecial.SpecialReportAlt: @@ -2496,12 +2486,7 @@ const standfirstTextLight: PaletteFunction = (format) => { case ArticleDesign.Picture: case ArticleDesign.Video: case ArticleDesign.Audio: - switch (format.theme) { - case ArticleSpecial.Labs: - return sourcePalette.neutral[0]; - default: - return sourcePalette.neutral[86]; - } + return sourcePalette.neutral[86]; case ArticleDesign.Gallery: case ArticleDesign.HostedGallery: return sourcePalette.neutral[86]; @@ -2531,12 +2516,7 @@ const standfirstTextDark: PaletteFunction = ({ design, display, theme }) => { case ArticleDesign.Picture: case ArticleDesign.Video: case ArticleDesign.Audio: - switch (theme) { - case ArticleSpecial.Labs: - return sourcePalette.neutral[7]; - default: - return sourcePalette.neutral[86]; - } + return sourcePalette.neutral[86]; case ArticleDesign.Standard: case ArticleDesign.Review: case ArticleDesign.Explainer: @@ -3960,20 +3940,13 @@ const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { case ArticleDesign.Picture: case ArticleDesign.HostedArticle: case ArticleDesign.HostedVideo: - switch (theme) { - case ArticleSpecial.Labs: - return sourcePalette.neutral[7]; - default: - return sourcePalette.neutral[86]; - } + return sourcePalette.neutral[86]; case ArticleDesign.DeadBlog: switch (theme) { case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[300]; case ArticleSpecial.SpecialReportAlt: return sourcePalette.news[400]; - case ArticleSpecial.Labs: - return sourcePalette.neutral[7]; default: return pillarPalette(theme, 400); } @@ -3981,8 +3954,6 @@ const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { switch (theme) { case Pillar.News: return sourcePalette.news[300]; - case ArticleSpecial.Labs: - return sourcePalette.neutral[7]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[300]; case ArticleSpecial.SpecialReportAlt: @@ -3992,8 +3963,6 @@ const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { } default: switch (theme) { - case ArticleSpecial.Labs: - return sourcePalette.neutral[7]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[300]; default: @@ -4031,16 +4000,9 @@ const shareButtonDark: PaletteFunction = ({ design, theme }) => { case ArticleDesign.Audio: case ArticleDesign.Video: case ArticleDesign.Picture: - switch (theme) { - case ArticleSpecial.Labs: - return sourcePalette.neutral[7]; - default: - return sourcePalette.neutral[86]; - } + return sourcePalette.neutral[86]; default: switch (theme) { - case ArticleSpecial.Labs: - return sourcePalette.labs[400]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[500]; case ArticleSpecial.SpecialReportAlt: @@ -5019,6 +4981,8 @@ const seriesTitleBackgroundLight: PaletteFunction = ({ case Pillar.Culture: case Pillar.Lifestyle: return pillarPalette(theme, 400); + case ArticleSpecial.Labs: + return sourcePalette.labs[200]; case ArticleSpecial.SpecialReportAlt: return sourcePalette.specialReportAlt[300]; } @@ -5254,6 +5218,7 @@ const seriesTitleTextDark: PaletteFunction = ({ design, theme, display }) => { case ArticleSpecial.SpecialReportAlt: return sourcePalette.specialReportAlt[700]; case ArticleSpecial.SpecialReport: + case ArticleSpecial.Labs: return sourcePalette.news[500]; } case ArticleDesign.Standard: @@ -5277,6 +5242,8 @@ const seriesTitleTextDark: PaletteFunction = ({ design, theme, display }) => { return sourcePalette.specialReport[500]; case ArticleSpecial.SpecialReportAlt: return sourcePalette.specialReportAlt[700]; + case ArticleSpecial.Labs: + return sourcePalette.labs[400]; } case ArticleDesign.Comment: case ArticleDesign.Editorial: @@ -5291,6 +5258,8 @@ const seriesTitleTextDark: PaletteFunction = ({ design, theme, display }) => { return sourcePalette.specialReport[500]; case ArticleSpecial.SpecialReportAlt: return sourcePalette.specialReportAlt[300]; + case ArticleSpecial.Labs: + return sourcePalette.labs[400]; } case ArticleDesign.Picture: case ArticleDesign.Video: From 2b8b34405a8f81e978b49d29abbb7d547e2acabc Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:32:34 +0100 Subject: [PATCH 65/80] Enable snapshots for immersive labs stories --- dotcom-rendering/src/layouts/DecideLayout.stories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx index d3eb850a2ff..a235ea2febe 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx @@ -274,7 +274,6 @@ const labsImmersiveArticle = ({ const immersiveLabsParameters = { ...webParameters, - chromatic: { disableSnapshot: true }, }; export const WebPhotoEssayImmersiveLabsLight: Story = { From e1b007964f632d00e91507cfb1092606845faf1c Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:19:55 +0100 Subject: [PATCH 66/80] Fix section title colours and colours for other design types --- .../src/components/SeriesSectionLink.tsx | 20 +- dotcom-rendering/src/paletteDeclarations.ts | 178 ++++++++++++++++-- 2 files changed, 168 insertions(+), 30 deletions(-) diff --git a/dotcom-rendering/src/components/SeriesSectionLink.tsx b/dotcom-rendering/src/components/SeriesSectionLink.tsx index c10fe3cdfd5..780ffce63aa 100644 --- a/dotcom-rendering/src/components/SeriesSectionLink.tsx +++ b/dotcom-rendering/src/components/SeriesSectionLink.tsx @@ -225,6 +225,11 @@ export const SeriesSectionLink = ({ ? themePalette('--series-title-match-text') : themePalette('--series-title-text'); + /** Used by the separate 'article section' link, kept distinct from the series tag's colour */ + const sectionTitleColour = isMatch + ? themePalette('--series-title-match-text') + : themePalette('--article-section-link-text'); + if ( format.display === ArticleDisplay.Immersive && (format.design === ArticleDesign.Comment || @@ -274,9 +279,7 @@ export const SeriesSectionLink = ({ displayBlock, breakWord, css` - color: ${themePalette( - '--series-title-text', - )}; + color: ${sectionTitleColour}; background-color: ${themePalette( '--section-title-background', )}; @@ -311,7 +314,7 @@ export const SeriesSectionLink = ({ fontStyles(format), breakWord, css` - color: ${titleColour}; + color: ${sectionTitleColour}; background-color: ${themePalette( '--section-title-background', )}; @@ -392,7 +395,7 @@ export const SeriesSectionLink = ({ css={[ sectionLabelLink, css` - color: ${titleColour}; + color: ${sectionTitleColour}; background-color: ${themePalette( '--section-title-background', )}; @@ -473,7 +476,7 @@ export const SeriesSectionLink = ({ displayBlock, breakWord, css` - color: ${titleColour}; + color: ${sectionTitleColour}; background-color: ${themePalette( '--section-title-background', )}; @@ -515,10 +518,7 @@ export const SeriesSectionLink = ({ css={[ sectionLabelLink, css` - color: ${titleColour}; - background-color: ${themePalette( - '--section-title-background', - )}; + color: ${sectionTitleColour}; `, marginRight, fontStyles(format), diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index e7183fe4293..c3c6e92a95f 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -49,6 +49,31 @@ const pillarPalette = ( } }; +/** + * Design groups that keep their pre-existing colours rather than the + * Labs theme-wide overrides used elsewhere in this file. + */ +const labsGalleryDesigns: ArticleDesign[] = [ + ArticleDesign.Gallery, + ArticleDesign.HostedGallery, +]; +const labsMediaDesigns: ArticleDesign[] = [ + ArticleDesign.Video, + ArticleDesign.Audio, + ArticleDesign.Picture, +]; +const labsHostedDesigns: ArticleDesign[] = [ + ArticleDesign.HostedArticle, + ArticleDesign.HostedVideo, + ArticleDesign.HostedGallery, +]; + +/** Whether a Labs theme-wide colour override should be skipped for this design */ +const keepsOriginalLabsColour = ( + design: ArticleDesign, + excludedDesigns: readonly ArticleDesign[], +): boolean => excludedDesigns.includes(design); + const textblockBulletLight: PaletteFunction = ({ theme, design }) => { switch (theme) { case Pillar.News: { @@ -1286,7 +1311,10 @@ const followIconBackgroundLight: PaletteFunction = ({ design, theme }) => { } }; const followIconBackgroundDark: PaletteFunction = ({ theme, design }) => { - if (theme === ArticleSpecial.Labs) { + if ( + theme === ArticleSpecial.Labs && + !keepsOriginalLabsColour(design, [ArticleDesign.LiveBlog]) + ) { return sourcePalette.neutral[7]; } @@ -1319,7 +1347,10 @@ const followIconBackgroundDark: PaletteFunction = ({ theme, design }) => { }; const followIconFillLight: PaletteFunction = ({ design, theme }) => { - if (theme === ArticleSpecial.Labs) { + if ( + theme === ArticleSpecial.Labs && + !keepsOriginalLabsColour(design, [ArticleDesign.LiveBlog]) + ) { return sourcePalette.labs[200]; } @@ -1428,7 +1459,10 @@ const followIconFillLight: PaletteFunction = ({ design, theme }) => { } }; const followIconFillDark: PaletteFunction = ({ theme, design }) => { - if (theme === ArticleSpecial.Labs) { + if ( + theme === ArticleSpecial.Labs && + !keepsOriginalLabsColour(design, [ArticleDesign.LiveBlog]) + ) { return sourcePalette.labs[500]; } @@ -2476,7 +2510,13 @@ const standfirstLinkTextDark: PaletteFunction = ({ design, theme }) => { }; const standfirstTextLight: PaletteFunction = (format) => { - if (format.theme === ArticleSpecial.Labs) { + if ( + format.theme === ArticleSpecial.Labs && + !keepsOriginalLabsColour(format.design, [ + ArticleDesign.LiveBlog, + ...labsHostedDesigns, + ]) + ) { return sourcePalette.labs[100]; } @@ -2502,7 +2542,13 @@ const standfirstTextLight: PaletteFunction = (format) => { }; const standfirstTextDark: PaletteFunction = ({ design, display, theme }) => { - if (theme === ArticleSpecial.Labs) { + if ( + theme === ArticleSpecial.Labs && + !keepsOriginalLabsColour(design, [ + ArticleDesign.LiveBlog, + ...labsHostedDesigns, + ]) + ) { return sourcePalette.labs[500]; } @@ -3927,7 +3973,16 @@ const shareButtonCopiedLight: PaletteFunction = ({ design }) => { const shareButtonCopiedDark: PaletteFunction = () => sourcePalette.neutral[86]; const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { - if (theme === ArticleSpecial.Labs) { + if ( + theme === ArticleSpecial.Labs && + !keepsOriginalLabsColour(design, [ + ArticleDesign.LiveBlog, + ...labsGalleryDesigns, + ...labsMediaDesigns, + ArticleDesign.HostedArticle, + ArticleDesign.HostedVideo, + ]) + ) { return sourcePalette.labs[200]; } @@ -3940,13 +3995,20 @@ const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { case ArticleDesign.Picture: case ArticleDesign.HostedArticle: case ArticleDesign.HostedVideo: - return sourcePalette.neutral[86]; + switch (theme) { + case ArticleSpecial.Labs: + return sourcePalette.neutral[7]; + default: + return sourcePalette.neutral[86]; + } case ArticleDesign.DeadBlog: switch (theme) { case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[300]; case ArticleSpecial.SpecialReportAlt: return sourcePalette.news[400]; + case ArticleSpecial.Labs: + return sourcePalette.neutral[7]; default: return pillarPalette(theme, 400); } @@ -3958,11 +4020,15 @@ const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { return sourcePalette.specialReport[300]; case ArticleSpecial.SpecialReportAlt: return sourcePalette.specialReportAlt[100]; + case ArticleSpecial.Labs: + return sourcePalette.neutral[7]; default: return pillarPalette(theme, 400); } default: switch (theme) { + case ArticleSpecial.Labs: + return sourcePalette.neutral[7]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[300]; default: @@ -3987,7 +4053,16 @@ const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { }; const shareButtonDark: PaletteFunction = ({ design, theme }) => { - if (theme === ArticleSpecial.Labs) { + if ( + theme === ArticleSpecial.Labs && + !keepsOriginalLabsColour(design, [ + ArticleDesign.LiveBlog, + ...labsGalleryDesigns, + ...labsMediaDesigns, + ArticleDesign.HostedArticle, + ArticleDesign.HostedVideo, + ]) + ) { return sourcePalette.labs[500]; } @@ -4000,9 +4075,16 @@ const shareButtonDark: PaletteFunction = ({ design, theme }) => { case ArticleDesign.Audio: case ArticleDesign.Video: case ArticleDesign.Picture: - return sourcePalette.neutral[86]; + switch (theme) { + case ArticleSpecial.Labs: + return sourcePalette.neutral[7]; + default: + return sourcePalette.neutral[86]; + } default: switch (theme) { + case ArticleSpecial.Labs: + return sourcePalette.labs[400]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[500]; case ArticleSpecial.SpecialReportAlt: @@ -4967,8 +5049,11 @@ const seriesTitleBackgroundLight: PaletteFunction = ({ } if ( theme === ArticleSpecial.Labs && - design !== ArticleDesign.Gallery && - design !== ArticleDesign.HostedGallery + !keepsOriginalLabsColour(design, [ + ArticleDesign.LiveBlog, + ...labsGalleryDesigns, + ...labsMediaDesigns, + ]) ) { return sourcePalette.labs[200]; } @@ -5037,7 +5122,13 @@ const seriesTitleBackgroundDark: PaletteFunction = ({ if (theme === ArticleSpecial.SpecialReport) { return sourcePalette.brandAlt[400]; } - if (theme === ArticleSpecial.Labs) { + if ( + theme === ArticleSpecial.Labs && + !keepsOriginalLabsColour(design, [ + ArticleDesign.LiveBlog, + ...labsMediaDesigns, + ]) + ) { return sourcePalette.labs[500]; } switch (display) { @@ -5049,6 +5140,8 @@ const seriesTitleBackgroundDark: PaletteFunction = ({ case Pillar.Culture: case Pillar.Lifestyle: return pillarPalette(theme, 400); + case ArticleSpecial.Labs: + return sourcePalette.labs[500]; case ArticleSpecial.SpecialReportAlt: return sourcePalette.specialReportAlt[300]; } @@ -5086,10 +5179,11 @@ const sectionTitleBackgroundLight: PaletteFunction = ({ theme, display }) => { return 'transparent'; } }; -const seriesTitleTextLight: PaletteFunction = ({ theme, display, design }) => { - if (theme === ArticleSpecial.Labs && design !== ArticleDesign.LiveBlog) { - return sourcePalette.neutral[100]; - } +const seriesOrSectionTitleTextLight: PaletteFunction = ({ + theme, + display, + design, +}) => { if ( theme === ArticleSpecial.SpecialReportAlt && design !== ArticleDesign.LiveBlog && @@ -5194,14 +5288,35 @@ const seriesTitleTextLight: PaletteFunction = ({ theme, display, design }) => { return sourcePalette.neutral[7]; } }; -const seriesTitleTextDark: PaletteFunction = ({ design, theme, display }) => { + +/** Used by the series tag only; the article section link keeps `articleSectionLinkTextLight` */ +const seriesTitleTextLight: PaletteFunction = (format) => { if ( - theme === ArticleSpecial.Labs && - design !== ArticleDesign.Gallery && - design !== ArticleDesign.HostedGallery + format.theme === ArticleSpecial.Labs && + !keepsOriginalLabsColour(format.design, [ + ArticleDesign.LiveBlog, + ...labsMediaDesigns, + ]) + ) { + return sourcePalette.neutral[100]; + } + return seriesOrSectionTitleTextLight(format); +}; + +const articleSectionLinkTextLight: PaletteFunction = (format) => { + if ( + format.theme === ArticleSpecial.Labs && + format.design !== ArticleDesign.LiveBlog ) { return sourcePalette.neutral[7]; } + return seriesOrSectionTitleTextLight(format); +}; +const seriesOrSectionTitleTextDark: PaletteFunction = ({ + design, + theme, + display, +}) => { if (display === ArticleDisplay.Immersive) return sourcePalette.neutral[100]; switch (design) { case ArticleDesign.Gallery: @@ -5282,6 +5397,25 @@ const seriesTitleTextDark: PaletteFunction = ({ design, theme, display }) => { } } }; + +/** Used by the series tag only; the article section link keeps `articleSectionLinkTextDark` */ +const seriesTitleTextDark: PaletteFunction = (format) => { + if ( + format.theme === ArticleSpecial.Labs && + !keepsOriginalLabsColour(format.design, [ + ArticleDesign.LiveBlog, + ...labsGalleryDesigns, + ...labsMediaDesigns, + ]) + ) { + return sourcePalette.neutral[7]; + } + return seriesOrSectionTitleTextDark(format); +}; + +const articleSectionLinkTextDark: PaletteFunction = + seriesOrSectionTitleTextDark; + const seriesTitleMatchTextLight: PaletteFunction = (format) => { if ( format.design === ArticleDesign.MatchReport || @@ -6634,6 +6768,10 @@ const paletteColours = { light: articleSectionBorderLight, dark: articleSectionBorderDark, }, + '--article-section-link-text': { + light: articleSectionLinkTextLight, + dark: articleSectionLinkTextDark, + }, '--article-section-secondary-title': { light: articleSectionSecondaryTitleLight, dark: articleSectionSecondaryTitleDark, From 51ac52bf1014387103ddff76f507bdac3017e206 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:47:45 +0100 Subject: [PATCH 67/80] Fix series tag portrait position --- dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index fc4fe35ae45..719fdfb8bb0 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -277,6 +277,9 @@ export const StandardLayoutArticleGrid = ({ isImmersive ? css` z-index: ${getZIndex('articleHeadline')}; + ${isImmersivePortrait && + `align-self: end; + margin-bottom: 2px;`} ` : undefined } From aef49152c4eac6f7501be029dd1c613f893a2c7b Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:35:57 +0100 Subject: [PATCH 68/80] Video labs text colour --- dotcom-rendering/src/paletteDeclarations.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index c3c6e92a95f..d473ca1284d 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -2514,6 +2514,7 @@ const standfirstTextLight: PaletteFunction = (format) => { format.theme === ArticleSpecial.Labs && !keepsOriginalLabsColour(format.design, [ ArticleDesign.LiveBlog, + ...labsMediaDesigns, ...labsHostedDesigns, ]) ) { @@ -2526,7 +2527,12 @@ const standfirstTextLight: PaletteFunction = (format) => { case ArticleDesign.Picture: case ArticleDesign.Video: case ArticleDesign.Audio: - return sourcePalette.neutral[86]; + switch (format.theme) { + case ArticleSpecial.Labs: + return sourcePalette.neutral[0]; + default: + return sourcePalette.neutral[86]; + } case ArticleDesign.Gallery: case ArticleDesign.HostedGallery: return sourcePalette.neutral[86]; From 613cb9e611ade56dba9f8ff3fa3329655e9dd52f Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:08:24 +0100 Subject: [PATCH 69/80] Update all labs headers with new styles --- dotcom-rendering/src/components/LabsHeader.stories.tsx | 2 +- dotcom-rendering/src/components/LabsHeader.tsx | 4 ++-- dotcom-rendering/src/layouts/FrontLayout.tsx | 2 +- dotcom-rendering/src/layouts/FullPageInteractiveLayout.tsx | 4 ++-- dotcom-rendering/src/layouts/GalleryLayout.tsx | 2 +- dotcom-rendering/src/layouts/ImmersiveLayout.tsx | 2 +- dotcom-rendering/src/layouts/InteractiveLayout.tsx | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/dotcom-rendering/src/components/LabsHeader.stories.tsx b/dotcom-rendering/src/components/LabsHeader.stories.tsx index 7bda8b0c501..bc75f03f2cc 100644 --- a/dotcom-rendering/src/components/LabsHeader.stories.tsx +++ b/dotcom-rendering/src/components/LabsHeader.stories.tsx @@ -12,7 +12,7 @@ export const Default = () => { <Section fullWidth={true} showTopBorder={false} - backgroundColour={palette.labs[400]} + backgroundColour={palette.labs[100]} borderColour={palette.neutral[60]} > <LabsHeader editionId="UK" /> diff --git a/dotcom-rendering/src/components/LabsHeader.tsx b/dotcom-rendering/src/components/LabsHeader.tsx index f443ef136c9..dd67931354d 100644 --- a/dotcom-rendering/src/components/LabsHeader.tsx +++ b/dotcom-rendering/src/components/LabsHeader.tsx @@ -161,8 +161,8 @@ const Logo = ({ editionId }: { editionId: EditionId }) => ( export const LabsHeader = ({ editionId, - textColour = palette.neutral[7], - backgroundColour = palette.labs[400], + textColour = palette.neutral[100], + backgroundColour = palette.labs[100], }: { editionId: EditionId; textColour?: string; diff --git a/dotcom-rendering/src/layouts/FrontLayout.tsx b/dotcom-rendering/src/layouts/FrontLayout.tsx index d2507a918bf..0b7e0072158 100644 --- a/dotcom-rendering/src/layouts/FrontLayout.tsx +++ b/dotcom-rendering/src/layouts/FrontLayout.tsx @@ -246,7 +246,7 @@ export const FrontLayout = ({ front, NAV }: Props) => { <Section fullWidth={true} showTopBorder={false} - backgroundColour={sourcePalette.labs[400]} + backgroundColour={sourcePalette.labs[100]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" > diff --git a/dotcom-rendering/src/layouts/FullPageInteractiveLayout.tsx b/dotcom-rendering/src/layouts/FullPageInteractiveLayout.tsx index a799f339ce1..3998e36f578 100644 --- a/dotcom-rendering/src/layouts/FullPageInteractiveLayout.tsx +++ b/dotcom-rendering/src/layouts/FullPageInteractiveLayout.tsx @@ -234,7 +234,7 @@ export const FullPageInteractiveLayout = (props: WebProps | AppsProps) => { fullWidth={true} showTopBorder={false} padSides={true} - backgroundColour={sourcePalette.labs[400]} + backgroundColour={sourcePalette.labs[100]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" > @@ -257,7 +257,7 @@ export const FullPageInteractiveLayout = (props: WebProps | AppsProps) => { fullWidth={true} showTopBorder={false} padSides={true} - backgroundColour={sourcePalette.labs[400]} + backgroundColour={sourcePalette.labs[100]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" > diff --git a/dotcom-rendering/src/layouts/GalleryLayout.tsx b/dotcom-rendering/src/layouts/GalleryLayout.tsx index c00dccf3a5e..fb8cd0351d2 100644 --- a/dotcom-rendering/src/layouts/GalleryLayout.tsx +++ b/dotcom-rendering/src/layouts/GalleryLayout.tsx @@ -436,7 +436,7 @@ const GalleryLabsHeader = (props: { <Section fullWidth={true} showTopBorder={false} - backgroundColour={sourcePalette.labs[400]} + backgroundColour={sourcePalette.labs[100]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" element="aside" diff --git a/dotcom-rendering/src/layouts/ImmersiveLayout.tsx b/dotcom-rendering/src/layouts/ImmersiveLayout.tsx index 69cf8833b4b..a8e65de4e33 100644 --- a/dotcom-rendering/src/layouts/ImmersiveLayout.tsx +++ b/dotcom-rendering/src/layouts/ImmersiveLayout.tsx @@ -342,7 +342,7 @@ export const ImmersiveLayout = (props: WebProps | AppProps) => { <Section fullWidth={true} showTopBorder={false} - backgroundColour={sourcePalette.labs[400]} + backgroundColour={sourcePalette.labs[100]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" > diff --git a/dotcom-rendering/src/layouts/InteractiveLayout.tsx b/dotcom-rendering/src/layouts/InteractiveLayout.tsx index 3b4708d1212..955c66951f9 100644 --- a/dotcom-rendering/src/layouts/InteractiveLayout.tsx +++ b/dotcom-rendering/src/layouts/InteractiveLayout.tsx @@ -296,7 +296,7 @@ export const InteractiveLayout = (props: WebProps | AppsProps) => { <Section fullWidth={true} showTopBorder={false} - backgroundColour={sourcePalette.labs[400]} + backgroundColour={sourcePalette.labs[100]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" > From 93088302ec4f8aa85cbcdfc4e76b2fcb27199c1a Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:34:42 +0100 Subject: [PATCH 70/80] Rename, refactor --- dotcom-rendering/src/components/ArticleMeta.web.tsx | 8 +------- dotcom-rendering/src/components/ArticleTitle.tsx | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleMeta.web.tsx b/dotcom-rendering/src/components/ArticleMeta.web.tsx index 58d2a510d28..e165c0b6636 100644 --- a/dotcom-rendering/src/components/ArticleMeta.web.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.web.tsx @@ -325,12 +325,6 @@ export const ArticleMeta = ({ const isImmersive = format.display === ArticleDisplay.Immersive; - const isPortraitOrLandscapeImmersive = - layoutType === 'immersiveLandscapeDefault' || - layoutType === 'immersivePortraitDefault' || - layoutType === 'immersiveLandscapeFeature' || - layoutType === 'immersivePortraitFeature'; - const isAudio = format.design === ArticleDesign.Audio; const seriesTag = getSeriesTag(tags); @@ -373,7 +367,7 @@ export const ArticleMeta = ({ ) : ( '' )} - {isImmersive && isPortraitOrLandscapeImmersive ? ( + {isImmersiveGrid ? ( <div css={css` display: flex; diff --git a/dotcom-rendering/src/components/ArticleTitle.tsx b/dotcom-rendering/src/components/ArticleTitle.tsx index 0137f523936..78b1de7ce82 100644 --- a/dotcom-rendering/src/components/ArticleTitle.tsx +++ b/dotcom-rendering/src/components/ArticleTitle.tsx @@ -29,7 +29,7 @@ const sectionStyles = css` } `; -const immersiveMargins = css` +const immersiveGridMargins = css` max-width: 500px; min-width: 200px; margin-bottom: 4px; @@ -85,7 +85,7 @@ export const ArticleTitle = ({ format.display === ArticleDisplay.Immersive ? layoutType == null ? legacyImmersiveMargins - : immersiveMargins + : immersiveGridMargins : undefined } > From 0c77fb65d2c8cbd83a7a20097d04ee72124c3ac7 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:24:34 +0100 Subject: [PATCH 71/80] Refactor ternary --- .../src/layouts/StandardLayoutArticleGrid.tsx | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 719fdfb8bb0..f4a520e9416 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -298,22 +298,20 @@ export const StandardLayoutArticleGrid = ({ area="headline" layoutType={layoutType} css={[ - isImmersive - ? css` - z-index: ${getZIndex('articleHeadline')}; - ` - : undefined, - layoutType === 'immersivePortraitDefault' || - layoutType === 'immersivePortraitFeature' - ? css` - ${from.desktop} { - border-bottom: 1px solid - ${themePalette('--article-border')}; - border-top: 1px solid - ${themePalette('--article-border')}; - } - ` - : undefined, + isImmersive && + css` + z-index: ${getZIndex('articleHeadline')}; + `, + (layoutType === 'immersivePortraitDefault' || + layoutType === 'immersivePortraitFeature') && + css` + ${from.desktop} { + border-bottom: 1px solid + ${themePalette('--article-border')}; + border-top: 1px solid + ${themePalette('--article-border')}; + } + `, isImmersiveLandscape && css` ${from.desktop} { From c2abc008e2c1a13a5b1529d1fcd5b0d0e663a00b Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:36:01 +0100 Subject: [PATCH 72/80] Refactoring addressing comments --- .../src/layouts/StandardLayoutArticleGrid.tsx | 44 ++++++++------- dotcom-rendering/src/lib/getZIndex.ts | 3 + dotcom-rendering/src/paletteDeclarations.ts | 56 ++++++------------- 3 files changed, 42 insertions(+), 61 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index f4a520e9416..b996bc38dbf 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -174,6 +174,16 @@ export const StandardLayoutArticleGrid = ({ const isImmersiveLandscape = layoutType === 'immersiveLandscapeDefault' || layoutType === 'immersiveLandscapeFeature'; + const centreRuleColumn = (() => { + switch (layoutType) { + case 'immersivePortraitDefault': + case 'immersivePortraitFeature': + case 'immersiveLandscapeDefault': + return 4; + default: + return 3; + } + })(); const ageWarning = getAgeWarning( article.tags, @@ -193,22 +203,13 @@ export const StandardLayoutArticleGrid = ({ css` &::before, &::after { - z-index: 10; + z-index: ${getZIndex('immersiveGridOuterRules')}; } `, !isLabs && css` ${from.leftCol} { - ${grid.centreRule( - isImmersivePortrait - ? 4 - : isImmersiveLandscape - ? layoutType === - 'immersiveLandscapeFeature' - ? 3 - : 4 - : 3, - )} + ${grid.centreRule(centreRuleColumn)} } `, isImmersivePortrait && @@ -273,16 +274,17 @@ export const StandardLayoutArticleGrid = ({ area="title" layoutType={layoutType} element="aside" - css={ - isImmersive - ? css` - z-index: ${getZIndex('articleHeadline')}; - ${isImmersivePortrait && - `align-self: end; - margin-bottom: 2px;`} - ` - : undefined - } + css={[ + isImmersive && + css` + z-index: ${getZIndex('articleHeadline')}; + `, + isImmersivePortrait && + css` + align-self: end; + margin-bottom: 2px; + `, + ]} > <ArticleTitle format={format} diff --git a/dotcom-rendering/src/lib/getZIndex.ts b/dotcom-rendering/src/lib/getZIndex.ts index eed41a02d41..2b92726f8da 100644 --- a/dotcom-rendering/src/lib/getZIndex.ts +++ b/dotcom-rendering/src/lib/getZIndex.ts @@ -93,6 +93,9 @@ const indices = [ // Media overlay 'mediaOverlay', + // Vertical grid rule lines, kept above the main media in Labs immersive articles + 'immersiveGridOuterRules', + // Self-hosted video 'video-progress-bar-foreground', 'video-progress-bar-background', diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index d473ca1284d..c3f708c1ea1 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -68,12 +68,6 @@ const labsHostedDesigns: ArticleDesign[] = [ ArticleDesign.HostedGallery, ]; -/** Whether a Labs theme-wide colour override should be skipped for this design */ -const keepsOriginalLabsColour = ( - design: ArticleDesign, - excludedDesigns: readonly ArticleDesign[], -): boolean => excludedDesigns.includes(design); - const textblockBulletLight: PaletteFunction = ({ theme, design }) => { switch (theme) { case Pillar.News: { @@ -1311,10 +1305,7 @@ const followIconBackgroundLight: PaletteFunction = ({ design, theme }) => { } }; const followIconBackgroundDark: PaletteFunction = ({ theme, design }) => { - if ( - theme === ArticleSpecial.Labs && - !keepsOriginalLabsColour(design, [ArticleDesign.LiveBlog]) - ) { + if (theme === ArticleSpecial.Labs && design !== ArticleDesign.LiveBlog) { return sourcePalette.neutral[7]; } @@ -1347,10 +1338,7 @@ const followIconBackgroundDark: PaletteFunction = ({ theme, design }) => { }; const followIconFillLight: PaletteFunction = ({ design, theme }) => { - if ( - theme === ArticleSpecial.Labs && - !keepsOriginalLabsColour(design, [ArticleDesign.LiveBlog]) - ) { + if (theme === ArticleSpecial.Labs && design !== ArticleDesign.LiveBlog) { return sourcePalette.labs[200]; } @@ -1459,10 +1447,7 @@ const followIconFillLight: PaletteFunction = ({ design, theme }) => { } }; const followIconFillDark: PaletteFunction = ({ theme, design }) => { - if ( - theme === ArticleSpecial.Labs && - !keepsOriginalLabsColour(design, [ArticleDesign.LiveBlog]) - ) { + if (theme === ArticleSpecial.Labs && design !== ArticleDesign.LiveBlog) { return sourcePalette.labs[500]; } @@ -2512,11 +2497,11 @@ const standfirstLinkTextDark: PaletteFunction = ({ design, theme }) => { const standfirstTextLight: PaletteFunction = (format) => { if ( format.theme === ArticleSpecial.Labs && - !keepsOriginalLabsColour(format.design, [ + ![ ArticleDesign.LiveBlog, ...labsMediaDesigns, ...labsHostedDesigns, - ]) + ].includes(format.design) ) { return sourcePalette.labs[100]; } @@ -2550,10 +2535,7 @@ const standfirstTextLight: PaletteFunction = (format) => { const standfirstTextDark: PaletteFunction = ({ design, display, theme }) => { if ( theme === ArticleSpecial.Labs && - !keepsOriginalLabsColour(design, [ - ArticleDesign.LiveBlog, - ...labsHostedDesigns, - ]) + ![ArticleDesign.LiveBlog, ...labsHostedDesigns].includes(design) ) { return sourcePalette.labs[500]; } @@ -3981,13 +3963,13 @@ const shareButtonCopiedDark: PaletteFunction = () => sourcePalette.neutral[86]; const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { if ( theme === ArticleSpecial.Labs && - !keepsOriginalLabsColour(design, [ + ![ ArticleDesign.LiveBlog, ...labsGalleryDesigns, ...labsMediaDesigns, ArticleDesign.HostedArticle, ArticleDesign.HostedVideo, - ]) + ].includes(design) ) { return sourcePalette.labs[200]; } @@ -4061,13 +4043,13 @@ const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { const shareButtonDark: PaletteFunction = ({ design, theme }) => { if ( theme === ArticleSpecial.Labs && - !keepsOriginalLabsColour(design, [ + ![ ArticleDesign.LiveBlog, ...labsGalleryDesigns, ...labsMediaDesigns, ArticleDesign.HostedArticle, ArticleDesign.HostedVideo, - ]) + ].includes(design) ) { return sourcePalette.labs[500]; } @@ -5055,11 +5037,11 @@ const seriesTitleBackgroundLight: PaletteFunction = ({ } if ( theme === ArticleSpecial.Labs && - !keepsOriginalLabsColour(design, [ + ![ ArticleDesign.LiveBlog, ...labsGalleryDesigns, ...labsMediaDesigns, - ]) + ].includes(design) ) { return sourcePalette.labs[200]; } @@ -5130,10 +5112,7 @@ const seriesTitleBackgroundDark: PaletteFunction = ({ } if ( theme === ArticleSpecial.Labs && - !keepsOriginalLabsColour(design, [ - ArticleDesign.LiveBlog, - ...labsMediaDesigns, - ]) + ![ArticleDesign.LiveBlog, ...labsMediaDesigns].includes(design) ) { return sourcePalette.labs[500]; } @@ -5299,10 +5278,7 @@ const seriesOrSectionTitleTextLight: PaletteFunction = ({ const seriesTitleTextLight: PaletteFunction = (format) => { if ( format.theme === ArticleSpecial.Labs && - !keepsOriginalLabsColour(format.design, [ - ArticleDesign.LiveBlog, - ...labsMediaDesigns, - ]) + ![ArticleDesign.LiveBlog, ...labsMediaDesigns].includes(format.design) ) { return sourcePalette.neutral[100]; } @@ -5408,11 +5384,11 @@ const seriesOrSectionTitleTextDark: PaletteFunction = ({ const seriesTitleTextDark: PaletteFunction = (format) => { if ( format.theme === ArticleSpecial.Labs && - !keepsOriginalLabsColour(format.design, [ + ![ ArticleDesign.LiveBlog, ...labsGalleryDesigns, ...labsMediaDesigns, - ]) + ].includes(format.design) ) { return sourcePalette.neutral[7]; } From fb9520b51e580750db8d05707846eba456f28cf6 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:41:10 +0100 Subject: [PATCH 73/80] Labs header link hover colour fix --- dotcom-rendering/src/components/LabsHeader.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dotcom-rendering/src/components/LabsHeader.tsx b/dotcom-rendering/src/components/LabsHeader.tsx index dd67931354d..fe699895fa0 100644 --- a/dotcom-rendering/src/components/LabsHeader.tsx +++ b/dotcom-rendering/src/components/LabsHeader.tsx @@ -153,6 +153,10 @@ const Logo = ({ editionId }: { editionId: EditionId }) => ( svg { fill: currentColor; } + + &:hover { + color: inherit; + } `} > <LabsLogo /> From 7ba74a12dd6d79e8217b0487e2fdbf469645fc2b Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:34:42 +0100 Subject: [PATCH 74/80] Revert Labs header, standfirst, share button and listen-to-article colour changes These have been split out into a separate PR (dc/labs-standfirst-share-listen-colours) to keep this focused on the new Labs immersive grid layout and bug fixes. --- .../src/components/LabsHeader.stories.tsx | 2 +- .../src/components/LabsHeader.tsx | 53 +++--------- dotcom-rendering/src/layouts/FrontLayout.tsx | 2 +- .../src/layouts/FullPageInteractiveLayout.tsx | 4 +- .../src/layouts/GalleryLayout.tsx | 2 +- .../src/layouts/ImmersiveLayout.tsx | 2 +- .../src/layouts/InteractiveLayout.tsx | 2 +- .../src/layouts/StandardLayout.tsx | 8 +- dotcom-rendering/src/paletteDeclarations.ts | 82 ++++--------------- 9 files changed, 38 insertions(+), 119 deletions(-) diff --git a/dotcom-rendering/src/components/LabsHeader.stories.tsx b/dotcom-rendering/src/components/LabsHeader.stories.tsx index bc75f03f2cc..7bda8b0c501 100644 --- a/dotcom-rendering/src/components/LabsHeader.stories.tsx +++ b/dotcom-rendering/src/components/LabsHeader.stories.tsx @@ -12,7 +12,7 @@ export const Default = () => { <Section fullWidth={true} showTopBorder={false} - backgroundColour={palette.labs[100]} + backgroundColour={palette.labs[400]} borderColour={palette.neutral[60]} > <LabsHeader editionId="UK" /> diff --git a/dotcom-rendering/src/components/LabsHeader.tsx b/dotcom-rendering/src/components/LabsHeader.tsx index fe699895fa0..77076f444b8 100644 --- a/dotcom-rendering/src/components/LabsHeader.tsx +++ b/dotcom-rendering/src/components/LabsHeader.tsx @@ -17,13 +17,7 @@ import { LABS_HEADER_HEIGHT } from '../lib/labs-constants'; import LabsLogo from '../static/logos/the-guardian-labs.svg'; import { Details } from './Details'; -const FlexWrapper = ({ - children, - textColour, -}: { - children: React.ReactNode; - textColour: string; -}) => ( +const FlexWrapper = ({ children }: { children: React.ReactNode }) => ( <div css={css` position: relative; @@ -31,7 +25,7 @@ const FlexWrapper = ({ display: flex; justify-content: space-between; - color: ${textColour}; + color: ${palette.neutral[7]}; `} > {children} @@ -96,17 +90,11 @@ const Title = () => ( </div> ); -const About = ({ - backgroundColour, - textColour, -}: { - backgroundColour: string; - textColour: string; -}) => ( +const About = () => ( <div css={css` ${textSans15}; - background-color: ${backgroundColour}; + background-color: ${palette.labs[400]}; border-top: 1px solid ${palette.neutral[60]}; width: 100vw; @@ -122,6 +110,10 @@ const About = ({ ${from.mobileLandscape} { padding: ${space[3]}px 20px; } + + > a { + color: black; + } `} > <p> @@ -133,7 +125,6 @@ const About = ({ iconSide="right" size="xsmall" priority="subdued" - theme={{ textSubdued: textColour }} icon={<SvgArrowRightStraight />} href="https://www.theguardian.com/info/2016/jan/25/content-funding" > @@ -147,32 +138,13 @@ const Logo = ({ editionId }: { editionId: EditionId }) => ( href={`https://www.theguardian.com/guardian-labs${getLabsUrlSuffix( editionId, )}`} - cssOverrides={css` - display: flex; - color: inherit; - svg { - fill: currentColor; - } - - &:hover { - color: inherit; - } - `} > <LabsLogo /> </Link> ); -export const LabsHeader = ({ - editionId, - textColour = palette.neutral[100], - backgroundColour = palette.labs[100], -}: { - editionId: EditionId; - textColour?: string; - backgroundColour?: string; -}) => ( - <FlexWrapper textColour={textColour}> +export const LabsHeader = ({ editionId }: { editionId: EditionId }) => ( + <FlexWrapper> <Left> <HeaderSection isFirst={true}> <Title /> @@ -194,10 +166,7 @@ export const LabsHeader = ({ } `} > - <About - backgroundColour={backgroundColour} - textColour={textColour} - /> + <About /> </Details> </HeaderSection> </Left> diff --git a/dotcom-rendering/src/layouts/FrontLayout.tsx b/dotcom-rendering/src/layouts/FrontLayout.tsx index 0b7e0072158..d2507a918bf 100644 --- a/dotcom-rendering/src/layouts/FrontLayout.tsx +++ b/dotcom-rendering/src/layouts/FrontLayout.tsx @@ -246,7 +246,7 @@ export const FrontLayout = ({ front, NAV }: Props) => { <Section fullWidth={true} showTopBorder={false} - backgroundColour={sourcePalette.labs[100]} + backgroundColour={sourcePalette.labs[400]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" > diff --git a/dotcom-rendering/src/layouts/FullPageInteractiveLayout.tsx b/dotcom-rendering/src/layouts/FullPageInteractiveLayout.tsx index 3998e36f578..a799f339ce1 100644 --- a/dotcom-rendering/src/layouts/FullPageInteractiveLayout.tsx +++ b/dotcom-rendering/src/layouts/FullPageInteractiveLayout.tsx @@ -234,7 +234,7 @@ export const FullPageInteractiveLayout = (props: WebProps | AppsProps) => { fullWidth={true} showTopBorder={false} padSides={true} - backgroundColour={sourcePalette.labs[100]} + backgroundColour={sourcePalette.labs[400]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" > @@ -257,7 +257,7 @@ export const FullPageInteractiveLayout = (props: WebProps | AppsProps) => { fullWidth={true} showTopBorder={false} padSides={true} - backgroundColour={sourcePalette.labs[100]} + backgroundColour={sourcePalette.labs[400]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" > diff --git a/dotcom-rendering/src/layouts/GalleryLayout.tsx b/dotcom-rendering/src/layouts/GalleryLayout.tsx index fb8cd0351d2..c00dccf3a5e 100644 --- a/dotcom-rendering/src/layouts/GalleryLayout.tsx +++ b/dotcom-rendering/src/layouts/GalleryLayout.tsx @@ -436,7 +436,7 @@ const GalleryLabsHeader = (props: { <Section fullWidth={true} showTopBorder={false} - backgroundColour={sourcePalette.labs[100]} + backgroundColour={sourcePalette.labs[400]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" element="aside" diff --git a/dotcom-rendering/src/layouts/ImmersiveLayout.tsx b/dotcom-rendering/src/layouts/ImmersiveLayout.tsx index a8e65de4e33..69cf8833b4b 100644 --- a/dotcom-rendering/src/layouts/ImmersiveLayout.tsx +++ b/dotcom-rendering/src/layouts/ImmersiveLayout.tsx @@ -342,7 +342,7 @@ export const ImmersiveLayout = (props: WebProps | AppProps) => { <Section fullWidth={true} showTopBorder={false} - backgroundColour={sourcePalette.labs[100]} + backgroundColour={sourcePalette.labs[400]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" > diff --git a/dotcom-rendering/src/layouts/InteractiveLayout.tsx b/dotcom-rendering/src/layouts/InteractiveLayout.tsx index 955c66951f9..3b4708d1212 100644 --- a/dotcom-rendering/src/layouts/InteractiveLayout.tsx +++ b/dotcom-rendering/src/layouts/InteractiveLayout.tsx @@ -296,7 +296,7 @@ export const InteractiveLayout = (props: WebProps | AppsProps) => { <Section fullWidth={true} showTopBorder={false} - backgroundColour={sourcePalette.labs[100]} + backgroundColour={sourcePalette.labs[400]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" > diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 764314e7381..b3536cefd1f 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -135,16 +135,12 @@ export const StandardLayout = (props: WebProps | AppProps) => { <Section fullWidth={true} showTopBorder={false} - backgroundColour={sourcePalette.labs[100]} + backgroundColour={sourcePalette.labs[400]} borderColour={sourcePalette.neutral[60]} sectionId="labs-header" element="aside" > - <LabsHeader - editionId={editionId} - textColour={sourcePalette.neutral[100]} - backgroundColour={sourcePalette.labs[100]} - /> + <LabsHeader editionId={editionId} /> </Section> </Stuck> )} diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index c3f708c1ea1..4cd1d6a2971 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -62,11 +62,6 @@ const labsMediaDesigns: ArticleDesign[] = [ ArticleDesign.Audio, ArticleDesign.Picture, ]; -const labsHostedDesigns: ArticleDesign[] = [ - ArticleDesign.HostedArticle, - ArticleDesign.HostedVideo, - ArticleDesign.HostedGallery, -]; const textblockBulletLight: PaletteFunction = ({ theme, design }) => { switch (theme) { @@ -1305,10 +1300,6 @@ const followIconBackgroundLight: PaletteFunction = ({ design, theme }) => { } }; const followIconBackgroundDark: PaletteFunction = ({ theme, design }) => { - if (theme === ArticleSpecial.Labs && design !== ArticleDesign.LiveBlog) { - return sourcePalette.neutral[7]; - } - switch (design) { case ArticleDesign.DeadBlog: return sourcePalette.neutral[7]; @@ -1338,10 +1329,6 @@ const followIconBackgroundDark: PaletteFunction = ({ theme, design }) => { }; const followIconFillLight: PaletteFunction = ({ design, theme }) => { - if (theme === ArticleSpecial.Labs && design !== ArticleDesign.LiveBlog) { - return sourcePalette.labs[200]; - } - switch (design) { case ArticleDesign.Gallery: case ArticleDesign.HostedGallery: @@ -1388,6 +1375,8 @@ const followIconFillLight: PaletteFunction = ({ design, theme }) => { return sourcePalette.culture[400]; case Pillar.Lifestyle: return sourcePalette.lifestyle[400]; + case ArticleSpecial.Labs: + return sourcePalette.labs[300]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[300]; case ArticleSpecial.SpecialReportAlt: @@ -1418,6 +1407,8 @@ const followIconFillLight: PaletteFunction = ({ design, theme }) => { return sourcePalette.culture[400]; case Pillar.Lifestyle: return sourcePalette.lifestyle[400]; + case ArticleSpecial.Labs: + return sourcePalette.labs[300]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[300]; case ArticleSpecial.SpecialReportAlt: @@ -1447,15 +1438,13 @@ const followIconFillLight: PaletteFunction = ({ design, theme }) => { } }; const followIconFillDark: PaletteFunction = ({ theme, design }) => { - if (theme === ArticleSpecial.Labs && design !== ArticleDesign.LiveBlog) { - return sourcePalette.labs[500]; - } - switch (design) { case ArticleDesign.LiveBlog: return sourcePalette.neutral[93]; case ArticleDesign.Standard: switch (theme) { + case ArticleSpecial.Labs: + return sourcePalette.labs[300]; case Pillar.Opinion: return sourcePalette.opinion[500]; case Pillar.Sport: @@ -1493,6 +1482,8 @@ const followIconFillDark: PaletteFunction = ({ theme, design }) => { return sourcePalette.culture[500]; case Pillar.Lifestyle: return sourcePalette.lifestyle[500]; + case ArticleSpecial.Labs: + return sourcePalette.labs[400]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[700]; case ArticleSpecial.SpecialReportAlt: @@ -1511,6 +1502,8 @@ const followIconFillDark: PaletteFunction = ({ theme, design }) => { return sourcePalette.culture[500]; case Pillar.Lifestyle: return sourcePalette.lifestyle[500]; + case ArticleSpecial.Labs: + return sourcePalette.labs[300]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[700]; case ArticleSpecial.SpecialReportAlt: @@ -2495,17 +2488,6 @@ const standfirstLinkTextDark: PaletteFunction = ({ design, theme }) => { }; const standfirstTextLight: PaletteFunction = (format) => { - if ( - format.theme === ArticleSpecial.Labs && - ![ - ArticleDesign.LiveBlog, - ...labsMediaDesigns, - ...labsHostedDesigns, - ].includes(format.design) - ) { - return sourcePalette.labs[100]; - } - switch (format.design) { case ArticleDesign.LiveBlog: return sourcePalette.neutral[100]; @@ -2533,13 +2515,6 @@ const standfirstTextLight: PaletteFunction = (format) => { }; const standfirstTextDark: PaletteFunction = ({ design, display, theme }) => { - if ( - theme === ArticleSpecial.Labs && - ![ArticleDesign.LiveBlog, ...labsHostedDesigns].includes(design) - ) { - return sourcePalette.labs[500]; - } - switch (design) { case ArticleDesign.LiveBlog: case ArticleDesign.DeadBlog: @@ -2550,7 +2525,12 @@ const standfirstTextDark: PaletteFunction = ({ design, display, theme }) => { case ArticleDesign.Picture: case ArticleDesign.Video: case ArticleDesign.Audio: - return sourcePalette.neutral[86]; + switch (theme) { + case ArticleSpecial.Labs: + return sourcePalette.neutral[7]; + default: + return sourcePalette.neutral[86]; + } case ArticleDesign.Standard: case ArticleDesign.Review: case ArticleDesign.Explainer: @@ -3961,19 +3941,6 @@ const shareButtonCopiedLight: PaletteFunction = ({ design }) => { const shareButtonCopiedDark: PaletteFunction = () => sourcePalette.neutral[86]; const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { - if ( - theme === ArticleSpecial.Labs && - ![ - ArticleDesign.LiveBlog, - ...labsGalleryDesigns, - ...labsMediaDesigns, - ArticleDesign.HostedArticle, - ArticleDesign.HostedVideo, - ].includes(design) - ) { - return sourcePalette.labs[200]; - } - switch (design) { case ArticleDesign.Gallery: case ArticleDesign.HostedGallery: @@ -4004,12 +3971,12 @@ const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { switch (theme) { case Pillar.News: return sourcePalette.news[300]; + case ArticleSpecial.Labs: + return sourcePalette.neutral[7]; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[300]; case ArticleSpecial.SpecialReportAlt: return sourcePalette.specialReportAlt[100]; - case ArticleSpecial.Labs: - return sourcePalette.neutral[7]; default: return pillarPalette(theme, 400); } @@ -4041,19 +4008,6 @@ const shareButtonLight: PaletteFunction = ({ design, theme, display }) => { }; const shareButtonDark: PaletteFunction = ({ design, theme }) => { - if ( - theme === ArticleSpecial.Labs && - ![ - ArticleDesign.LiveBlog, - ...labsGalleryDesigns, - ...labsMediaDesigns, - ArticleDesign.HostedArticle, - ArticleDesign.HostedVideo, - ].includes(design) - ) { - return sourcePalette.labs[500]; - } - switch (design) { case ArticleDesign.Gallery: case ArticleDesign.HostedArticle: From 2f2788b40f07d8874cc81bc3923ccfbca4647ef4 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Thu, 13 Aug 2026 13:18:30 +0100 Subject: [PATCH 75/80] Add labs immersive grid ab test --- ab-testing/config/abTests.ts | 12 +++++++ .../src/layouts/DecideLayout.stories.tsx | 34 +++++++++++++++---- dotcom-rendering/src/layouts/DecideLayout.tsx | 17 ++++++++-- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/ab-testing/config/abTests.ts b/ab-testing/config/abTests.ts index f351dc2839c..4653046f40a 100644 --- a/ab-testing/config/abTests.ts +++ b/ab-testing/config/abTests.ts @@ -31,6 +31,18 @@ const ABTests: ABTest[] = [ groups: ["enable"], shouldForceMetricsCollection: false, }, + { + name: "commercial-labs-immersive-grid", + description: + "New grid-based immersive layout for Guardian Labs articles", + owners: ["commercial.dev@guardian.co.uk"], + status: "ON", + expirationDate: "2026-08-31", + type: "server", + audienceSize: 0 / 100, + groups: ["enable"], + shouldForceMetricsCollection: false, + }, { name: "growth-holdback-group", description: diff --git a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx index a235ea2febe..7bbb4cadc5c 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx @@ -28,7 +28,11 @@ import { getCurrentPillar } from '../lib/layoutHelpers'; import { extractNAV } from '../model/extract-nav'; import { type Article, enhanceArticleType } from '../types/article'; import type { ImageBlockElement } from '../types/content'; -import { DecideLayout, type Props as DecideLayoutProps } from './DecideLayout'; +import { + DecideLayout, + LABS_IMMERSIVE_GRID_AB_TEST, + type Props as DecideLayoutProps, +} from './DecideLayout'; export type HydratedLayoutDecoratorArgs = { colourScheme?: 'light' | 'dark'; @@ -265,12 +269,28 @@ const labsImmersiveArticle = ({ }: { orientation: 'portrait' | 'landscape'; design: ArticleDesign.PhotoEssay | ArticleDesign.Feature; -}): Article => ({ - ...(orientation === 'portrait' - ? photoEssayImmersiveLabsPortraitArticle - : photoEssayImmersiveLabsArticle), - design, -}); +}): Article => { + const base = + orientation === 'portrait' + ? photoEssayImmersiveLabsPortraitArticle + : photoEssayImmersiveLabsArticle; + return { + ...base, + design, + frontendData: { + ...base.frontendData, + config: { + ...base.frontendData.config, + // Opt these stories into the new layout regardless of the + // 0% production rollout, so they keep demonstrating it. + serverSideABTests: { + ...base.frontendData.config.serverSideABTests, + [LABS_IMMERSIVE_GRID_AB_TEST]: 'enable', + }, + }, + }, + }; +}; const immersiveLabsParameters = { ...webParameters, diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index 2a763cf1e56..eb0399542fa 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -36,6 +36,17 @@ interface WebProps extends BaseProps { export type Props = WebProps | AppProps; +/** + * Guards the new grid-based immersive layout for Guardian Labs articles + * behind a 0% a/b test + */ +export const LABS_IMMERSIVE_GRID_AB_TEST = 'commercial-labs-immersive-grid'; + +const isInLabsImmersiveGridTest = (article: Article): boolean => + article.frontendData.config.serverSideABTests[ + LABS_IMMERSIVE_GRID_AB_TEST + ] === 'enable'; + const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { const format = { design: article.design, @@ -58,7 +69,8 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { ); } default: { - return article.theme === ArticleSpecial.Labs ? ( + return article.theme === ArticleSpecial.Labs && + isInLabsImmersiveGridTest(article) ? ( <StandardLayout article={article.frontendData} format={format} @@ -241,7 +253,8 @@ const DecideLayoutWeb = ({ article, NAV, renderingTarget }: WebProps) => { ); } default: { - return article.theme === ArticleSpecial.Labs ? ( + return article.theme === ArticleSpecial.Labs && + isInLabsImmersiveGridTest(article) ? ( <StandardLayout article={article.frontendData} format={format} From 334ebb1d01efd84e294a8a681fa7ef2f684028ff Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:41:56 +0100 Subject: [PATCH 76/80] Revert unnecessary palette overrides --- .../src/components/ArticleHeadline.tsx | 35 ++++++++---- .../src/layouts/DecideLayout.stories.tsx | 2 +- dotcom-rendering/src/paletteDeclarations.ts | 54 +++---------------- 3 files changed, 33 insertions(+), 58 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 7549d4b4cd6..aa0a18d9877 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -12,6 +12,7 @@ import { headlineMedium28, headlineMedium34, headlineMedium50, + palette as sourcePalette, space, textSansBold28, textSansBold34, @@ -282,6 +283,15 @@ const invertedText = css` } `; +/** + * Used only by the immersiveLandscapeFeature grid layout, the headline sits + * over the blurred panel added in Figure.tsx, rather than the shared + * --headline-colour used by the legacy immersive layout. + */ +const gridFeatureHeadlineText = css` + color: ${sourcePalette.neutral[7]}; +`; + const maxWidth = css` ${from.desktop} { max-width: 620px; @@ -478,6 +488,7 @@ export const ArticleHeadline = ({ starRating, }: Props) => { const isInverted = layoutType === 'immersiveLandscapeDefault'; + const isGridFeature = layoutType === 'immersiveLandscapeFeature'; const isLegacyImmersive = layoutType == null; switch (format.display) { case ArticleDisplay.Immersive: { @@ -515,11 +526,13 @@ export const ArticleHeadline = ({ ] : isInverted ? [invertedText, darkBackground] - : css` - color: ${themePalette( - '--headline-colour', - )}; - `, + : isGridFeature + ? gridFeatureHeadlineText + : css` + color: ${themePalette( + '--headline-colour', + )}; + `, ]} > {headlineString} @@ -562,11 +575,13 @@ export const ArticleHeadline = ({ ] : isInverted ? [invertedText, darkBackground] - : css` - color: ${themePalette( - '--headline-colour', - )}; - `, + : isGridFeature + ? gridFeatureHeadlineText + : css` + color: ${themePalette( + '--headline-colour', + )}; + `, ]} > <span diff --git a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx index 7bbb4cadc5c..c5791f1cb8a 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx @@ -30,8 +30,8 @@ import { type Article, enhanceArticleType } from '../types/article'; import type { ImageBlockElement } from '../types/content'; import { DecideLayout, - LABS_IMMERSIVE_GRID_AB_TEST, type Props as DecideLayoutProps, + LABS_IMMERSIVE_GRID_AB_TEST, } from './DecideLayout'; export type HydratedLayoutDecoratorArgs = { diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 15dc829efe5..551d5043b2d 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -49,15 +49,6 @@ const pillarPalette = ( } }; -/** - * Design groups that keep their pre-existing colours rather than the - * Labs theme-wide overrides used elsewhere in this file. - */ -const labsGalleryDesigns: ArticleDesign[] = [ - ArticleDesign.Gallery, - ArticleDesign.HostedGallery, -]; - const labsMediaDesigns: ArticleDesign[] = [ ArticleDesign.Video, ArticleDesign.Audio, @@ -109,9 +100,7 @@ const textblockTextDark: PaletteFunction = () => 'inherit'; const headlineTextLight: PaletteFunction = ({ design, display, theme }) => { switch (display) { case ArticleDisplay.Immersive: - return theme === ArticleSpecial.Labs - ? sourcePalette.neutral[7] - : sourcePalette.neutral[97]; + return sourcePalette.neutral[97]; default: { switch (design) { case ArticleDesign.Editorial: @@ -226,8 +215,6 @@ const headlineBackgroundLight: PaletteFunction = ({ switch (display) { case ArticleDisplay.Immersive: switch (theme) { - case ArticleSpecial.Labs: - return 'transparent'; case ArticleSpecial.SpecialReport: return sourcePalette.specialReport[300]; default: @@ -5037,16 +5024,6 @@ const seriesTitleBackgroundLight: PaletteFunction = ({ if (theme === ArticleSpecial.SpecialReport) { return sourcePalette.brandAlt[400]; } - if ( - theme === ArticleSpecial.Labs && - ![ - ArticleDesign.LiveBlog, - ...labsGalleryDesigns, - ...labsMediaDesigns, - ].includes(design) - ) { - return sourcePalette.labs[200]; - } switch (display) { case ArticleDisplay.Immersive: switch (theme) { @@ -5057,7 +5034,7 @@ const seriesTitleBackgroundLight: PaletteFunction = ({ case Pillar.Lifestyle: return pillarPalette(theme, 400); case ArticleSpecial.Labs: - return sourcePalette.labs[200]; + return sourcePalette.labs[300]; case ArticleSpecial.SpecialReportAlt: return sourcePalette.specialReportAlt[300]; } @@ -5112,12 +5089,6 @@ const seriesTitleBackgroundDark: PaletteFunction = ({ if (theme === ArticleSpecial.SpecialReport) { return sourcePalette.brandAlt[400]; } - if ( - theme === ArticleSpecial.Labs && - ![ArticleDesign.LiveBlog, ...labsMediaDesigns].includes(design) - ) { - return sourcePalette.labs[500]; - } switch (display) { case ArticleDisplay.Immersive: switch (theme) { @@ -5128,7 +5099,7 @@ const seriesTitleBackgroundDark: PaletteFunction = ({ case Pillar.Lifestyle: return pillarPalette(theme, 400); case ArticleSpecial.Labs: - return sourcePalette.labs[500]; + return sourcePalette.labs[300]; case ArticleSpecial.SpecialReportAlt: return sourcePalette.specialReportAlt[300]; } @@ -5280,9 +5251,9 @@ const seriesOrSectionTitleTextLight: PaletteFunction = ({ const seriesTitleTextLight: PaletteFunction = (format) => { if ( format.theme === ArticleSpecial.Labs && - ![ArticleDesign.LiveBlog, ...labsMediaDesigns].includes(format.design) + format.design !== ArticleDesign.LiveBlog ) { - return sourcePalette.neutral[100]; + return sourcePalette.neutral[7]; } return seriesOrSectionTitleTextLight(format); }; @@ -5383,19 +5354,8 @@ const seriesOrSectionTitleTextDark: PaletteFunction = ({ }; /** Used by the series tag only; the article section link keeps `articleSectionLinkTextDark` */ -const seriesTitleTextDark: PaletteFunction = (format) => { - if ( - format.theme === ArticleSpecial.Labs && - ![ - ArticleDesign.LiveBlog, - ...labsGalleryDesigns, - ...labsMediaDesigns, - ].includes(format.design) - ) { - return sourcePalette.neutral[7]; - } - return seriesOrSectionTitleTextDark(format); -}; +const seriesTitleTextDark: PaletteFunction = (format) => + seriesOrSectionTitleTextDark(format); const articleSectionLinkTextDark: PaletteFunction = seriesOrSectionTitleTextDark; From 501720305d1aa7527dc7781b99c5a0eb64732de0 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:18:09 +0100 Subject: [PATCH 77/80] Fix palette colours --- .../src/components/ArticleHeadline.tsx | 28 +++++-------------- dotcom-rendering/src/paletteDeclarations.ts | 15 ++++++++++ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index aa0a18d9877..f7548407253 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -12,7 +12,6 @@ import { headlineMedium28, headlineMedium34, headlineMedium50, - palette as sourcePalette, space, textSansBold28, textSansBold34, @@ -284,12 +283,12 @@ const invertedText = css` `; /** - * Used only by the immersiveLandscapeFeature grid layout, the headline sits - * over the blurred panel added in Figure.tsx, rather than the shared - * --headline-colour used by the legacy immersive layout. + * Used by the non-inverted immersive grid layouts, whose headlines sit on a + * plain or light blurred background rather than the shared --headline-colour + * (used by the legacy immersive layout and the inverted grid layout). */ -const gridFeatureHeadlineText = css` - color: ${sourcePalette.neutral[7]}; +const gridHeadlineText = css` + color: ${themePalette('--immersive-grid-headline-colour')}; `; const maxWidth = css` @@ -488,7 +487,6 @@ export const ArticleHeadline = ({ starRating, }: Props) => { const isInverted = layoutType === 'immersiveLandscapeDefault'; - const isGridFeature = layoutType === 'immersiveLandscapeFeature'; const isLegacyImmersive = layoutType == null; switch (format.display) { case ArticleDisplay.Immersive: { @@ -526,13 +524,7 @@ export const ArticleHeadline = ({ ] : isInverted ? [invertedText, darkBackground] - : isGridFeature - ? gridFeatureHeadlineText - : css` - color: ${themePalette( - '--headline-colour', - )}; - `, + : gridHeadlineText, ]} > {headlineString} @@ -575,13 +567,7 @@ export const ArticleHeadline = ({ ] : isInverted ? [invertedText, darkBackground] - : isGridFeature - ? gridFeatureHeadlineText - : css` - color: ${themePalette( - '--headline-colour', - )}; - `, + : gridHeadlineText, ]} > <span diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 551d5043b2d..e87b9f7c687 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -207,6 +207,17 @@ const headlineMatchTextLight: PaletteFunction = (format) => const headlineMatchTextDark: PaletteFunction = (format) => seriesTitleMatchTextDark(format); +/** + * Only used by the non-inverted immersive grid layouts (portrait, and + * landscape feature), whose headlines sit on a plain or light blurred + * background rather than the solid dark box `--headline-colour` expects. + */ +const immersiveGridHeadlineTextLight: PaletteFunction = () => + sourcePalette.neutral[7]; + +const immersiveGridHeadlineTextDark: PaletteFunction = () => + sourcePalette.neutral[97]; + const headlineBackgroundLight: PaletteFunction = ({ display, design, @@ -7573,6 +7584,10 @@ const paletteColours = { light: imageTitleBackground, dark: imageTitleBackground, }, + '--immersive-grid-headline-colour': { + light: immersiveGridHeadlineTextLight, + dark: immersiveGridHeadlineTextDark, + }, '--interactive-atom-background': { light: interactiveAtomBackgroundLight, dark: interactiveAtomBackgroundDark, From 283942f8ef727ddc5000feca41740553cb9467f0 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:15:41 +0100 Subject: [PATCH 78/80] Fix color scope bug --- dotcom-rendering/src/components/ArticleHeadline.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index f7548407253..3c81c5b575c 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -271,12 +271,13 @@ const darkBackground = css` `; const invertedText = css` + color: white; + background-color: black; + white-space: pre-wrap; + padding-bottom: ${space[1]}px; + padding-right: ${space[1]}px; + ${from.desktop} { - color: white; - background-color: black; - white-space: pre-wrap; - padding-bottom: ${space[1]}px; - padding-right: ${space[1]}px; margin-left: -10px; padding-left: 10px; } From eb023c5c0e9eb7df1e235878dfbaabd76d472516 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:23:19 +0100 Subject: [PATCH 79/80] Add labs immersive articles to gen-fixtures and capture mobile snapshots --- .../fe-articles/LabsImmersiveGlobalX.ts | 2291 +++++++++++++++++ .../fe-articles/LabsImmersiveParrtjima.ts | 2018 +++++++++++++++ .../LabsImmersiveVictorianWater.ts | 2011 +++++++++++++++ .../scripts/test-data/gen-fixtures.js | 12 + .../src/layouts/DecideLayout.stories.tsx | 74 +- 5 files changed, 6390 insertions(+), 16 deletions(-) create mode 100644 dotcom-rendering/fixtures/generated/fe-articles/LabsImmersiveGlobalX.ts create mode 100644 dotcom-rendering/fixtures/generated/fe-articles/LabsImmersiveParrtjima.ts create mode 100644 dotcom-rendering/fixtures/generated/fe-articles/LabsImmersiveVictorianWater.ts diff --git a/dotcom-rendering/fixtures/generated/fe-articles/LabsImmersiveGlobalX.ts b/dotcom-rendering/fixtures/generated/fe-articles/LabsImmersiveGlobalX.ts new file mode 100644 index 00000000000..a466324140b --- /dev/null +++ b/dotcom-rendering/fixtures/generated/fe-articles/LabsImmersiveGlobalX.ts @@ -0,0 +1,2291 @@ +/** + * DO NOT EDIT THIS FILE! + * + * This file was automatically generated using the gen-fixtures.js script. Any edits + * you make here will be lost. + * + * If the data in these fixtures is not what you expect then + * + * 1. Refresh the data using 'make gen-fixtures' or + * 2. if the latest live data is not what you need, then consider editing + * gen-fixtures.js directly. + */ + +import type { FEArticle } from '../../../src/frontend/feArticle'; + +export const LabsImmersiveGlobalX: FEArticle = { + version: 3, + headline: 'Rare earth: the commodities powering our AI future', + standfirst: + '<p>As AI technology evolves, a diverse commodities market is rapidly expanding behind the scenes</p>', + webTitle: 'Rare earth: the commodities powering our AI future', + mainMediaElements: [ + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'immersive', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '16:9', + height: '1125', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/2000.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '16:9', + height: '563', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/1000.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '16:9', + height: '281', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/500.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '16:9', + height: '79', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/140.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '16:9', + height: '3074', + width: '5464', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/5464.jpg', + }, + { + index: 5, + fields: { + aspectRatio: '16:9', + isMaster: 'true', + height: '3074', + width: '5464', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg', + }, + ], + }, + elementId: '5a4950ed-b5ff-443b-90f8-dc3a82ec2682', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=620&quality=85&auto=format&fit=max&s=abb44d7eaaefd7492c3da9641ab9c064', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=7c2b86f53ca2cc2690942b6db665288b', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=700&quality=85&auto=format&fit=max&s=837e61e315b7a1ecb19db2327ec76ecb', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=2b7bf9f80f95005dd1507400459cb8ea', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=620&quality=85&auto=format&fit=max&s=abb44d7eaaefd7492c3da9641ab9c064', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=7c2b86f53ca2cc2690942b6db665288b', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=645&quality=85&auto=format&fit=max&s=cc6d0fbd6da5c11d758076a839d9663d', + width: 645, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=645&quality=45&auto=format&fit=max&dpr=2&s=ed5bfc039e787568a7d2aded064becf9', + width: 1290, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=465&quality=85&auto=format&fit=max&s=6b052b6f0fff33532966e51f28af6b34', + width: 465, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=9e28300321d3522bb67dd99981a15dfd', + width: 930, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [], + }, + { + weighting: 'supporting', + srcSet: [], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=1020&quality=85&auto=format&fit=max&s=45fc831c673bfe94f540e6883d2c6d9a', + width: 1020, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=1020&quality=45&auto=format&fit=max&dpr=2&s=140e174b0cb2d4b78482e795d2aee468', + width: 2040, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=940&quality=85&auto=format&fit=max&s=84d181bef28cd06c6f4eccb58053741d', + width: 940, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=940&quality=45&auto=format&fit=max&dpr=2&s=076f1734724c057da9851cc80718b4d0', + width: 1880, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=700&quality=85&auto=format&fit=max&s=837e61e315b7a1ecb19db2327ec76ecb', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=2b7bf9f80f95005dd1507400459cb8ea', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=700&quality=85&auto=format&fit=max&s=837e61e315b7a1ecb19db2327ec76ecb', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=2b7bf9f80f95005dd1507400459cb8ea', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=660&quality=85&auto=format&fit=max&s=c6a111ac6fcbbd48bf8e01ceefe55754', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=3480015930dba1a27d0c5de07a2355b6', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=645&quality=85&auto=format&fit=max&s=cc6d0fbd6da5c11d758076a839d9663d', + width: 645, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=645&quality=45&auto=format&fit=max&dpr=2&s=ed5bfc039e787568a7d2aded064becf9', + width: 1290, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=465&quality=85&auto=format&fit=max&s=6b052b6f0fff33532966e51f28af6b34', + width: 465, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=9e28300321d3522bb67dd99981a15dfd', + width: 930, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=1900&quality=85&auto=format&fit=max&s=bcecd79f1240c65f2c28adb5d348866b', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=32223c2225070253b2e997e2669e110a', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=1300&quality=85&auto=format&fit=max&s=08e22428e9d42f15e5848eb1d8b84c09', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=6fb6ed00b5fa6fe353b7b098d9640b4e', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=1140&quality=85&auto=format&fit=max&s=19a488c9ff11d933b92aae89406e54de', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=c4eb5377df90d7eb949c488255428eff', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=980&quality=85&auto=format&fit=max&s=30cf7c3a1df99f7de4b73636fe7a1ebf', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=f4fee00188e4206c67edf2e54d406b21', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=740&quality=85&auto=format&fit=max&s=0b2835ba39f2eedf228676d1426eed5f', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=6d97a0e04363342c3e4b97246eb2fcb6', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=660&quality=85&auto=format&fit=max&s=c6a111ac6fcbbd48bf8e01ceefe55754', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=3480015930dba1a27d0c5de07a2355b6', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=480&quality=85&auto=format&fit=max&s=a332e9746e6fc956b545f5b5c89d2db4', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/master/5464.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=dbc5e349e1cc055b0759385ab7ae3ef8', + width: 960, + }, + ], + }, + ], + data: { + alt: 'Aerial view of open pit iron ore and heavy mining equipment.', + credit: 'Photograph: Anton Petrus/Getty Images', + }, + }, + ], + main: '<figure class="element element-image" data-media-id="68dcd461900ec3b3232d488ffc63aa3a03954f95"> <img src="https://media.guim.co.uk/68dcd461900ec3b3232d488ffc63aa3a03954f95/0_566_5464_3074/1000.jpg" alt="Aerial view of open pit iron ore and heavy mining equipment." width="1000" height="563" class="gu-image" /> </figure>', + keyEvents: [], + blocks: [ + { + id: '69c46a4a8f0836c671f18653', + elements: [ + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>The public face of modern computing is intangible. In daily life, we see and use generative AI, diagnostics, virtual assistants and intelligent algorithms.</p>', + elementId: 'dd76b712-e371-4cd3-9fbc-8a96e507f55c', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>But it looks quite different behind the scenes. A complex infrastructure supports the hardware, networks and systems that power AI.</p>', + elementId: 'ced7e7f5-291f-432f-8fc3-e80ac349ef08', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Justin Lin is an investment strategist at the exchange-traded fund (ETF) provider Global X, where he performs research and analysis across commodities and the energy transition. “Alongside the advancements of AI models, which are getting smarter by the day,” he says, “the interesting part is the infrastructure and commodities underlying them.”</p>', + elementId: '678ac119-fb03-4f8d-a35b-cdcdf8792f95', + }, + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'thumbnail', + media: { + allImages: [ + { + index: 0, + fields: { + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/png', + url: 'https://media.guim.co.uk/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/1000.png', + }, + { + index: 1, + fields: { + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/png', + url: 'https://media.guim.co.uk/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/1000.png', + }, + { + index: 2, + fields: { + isMaster: 'true', + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/png', + url: 'https://media.guim.co.uk/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png', + }, + { + index: 3, + fields: { + height: '500', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/png', + url: 'https://media.guim.co.uk/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/500.png', + }, + { + index: 4, + fields: { + height: '140', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/png', + url: 'https://media.guim.co.uk/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/140.png', + }, + ], + }, + elementId: 'df4257be-dcba-4d10-9df9-4b856f945b18', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=620&quality=85&auto=format&fit=max&s=c54adb4cb15d882321f2494158161881', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=620&quality=45&auto=format&fit=max&dpr=2&s=ba477f1262eacfbac216863ab68bcda2', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=605&quality=85&auto=format&fit=max&s=5dcf0120a0b1c13f227ad39e619eecfb', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=605&quality=45&auto=format&fit=max&dpr=2&s=c28b595fb9b1254d0c215d4bdc1d284f', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=445&quality=85&auto=format&fit=max&s=9143757a1d077e10b7a3565dc3fede54', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=445&quality=45&auto=format&fit=max&dpr=2&s=173bb8fead0a131e52c42e9a2e89bf16', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=140&quality=85&auto=format&fit=max&s=82a9c348bbe5341daaedfaf084421c4f', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=140&quality=45&auto=format&fit=max&dpr=2&s=a547146395f78dd07e60d589d11457c1', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=120&quality=85&auto=format&fit=max&s=e009e423bb7240720cce3701f8ea62ed', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=120&quality=45&auto=format&fit=max&dpr=2&s=aafac1582b98141a6a26f99e195fcb93', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=380&quality=85&auto=format&fit=max&s=1d778084e903ad1ef2076b79092f54f5', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=380&quality=45&auto=format&fit=max&dpr=2&s=ecf3b533288ebfa2e7728c03cd5622db', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=300&quality=85&auto=format&fit=max&s=5cd14cec7323aedd4bfcc68b1e1331ae', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=300&quality=45&auto=format&fit=max&dpr=2&s=399cc88ccfba1308c9b2207217664e00', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=620&quality=85&auto=format&fit=max&s=c54adb4cb15d882321f2494158161881', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=620&quality=45&auto=format&fit=max&dpr=2&s=ba477f1262eacfbac216863ab68bcda2', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=605&quality=85&auto=format&fit=max&s=5dcf0120a0b1c13f227ad39e619eecfb', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=605&quality=45&auto=format&fit=max&dpr=2&s=c28b595fb9b1254d0c215d4bdc1d284f', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=445&quality=85&auto=format&fit=max&s=9143757a1d077e10b7a3565dc3fede54', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=445&quality=45&auto=format&fit=max&dpr=2&s=173bb8fead0a131e52c42e9a2e89bf16', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=880&quality=85&auto=format&fit=max&s=41b89202deb6e32bcc680e23eeed4498', + width: 880, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=880&quality=45&auto=format&fit=max&dpr=2&s=8794b9536dcbbba5a3a80dda9fab09e5', + width: 1760, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=800&quality=85&auto=format&fit=max&s=129c5d60a4996d362b251f3d98fd9953', + width: 800, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=800&quality=45&auto=format&fit=max&dpr=2&s=fc5280973dbb8d9f789cf2a329d09dd2', + width: 1600, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=640&quality=85&auto=format&fit=max&s=934fe441e97ac5e27931426dcb334e33', + width: 640, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=640&quality=45&auto=format&fit=max&dpr=2&s=56189381a9e3bd30c423f76d528decdd', + width: 1280, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=620&quality=85&auto=format&fit=max&s=c54adb4cb15d882321f2494158161881', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=620&quality=45&auto=format&fit=max&dpr=2&s=ba477f1262eacfbac216863ab68bcda2', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=605&quality=85&auto=format&fit=max&s=5dcf0120a0b1c13f227ad39e619eecfb', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=605&quality=45&auto=format&fit=max&dpr=2&s=c28b595fb9b1254d0c215d4bdc1d284f', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=445&quality=85&auto=format&fit=max&s=9143757a1d077e10b7a3565dc3fede54', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=445&quality=45&auto=format&fit=max&dpr=2&s=173bb8fead0a131e52c42e9a2e89bf16', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=620&quality=85&auto=format&fit=max&s=c54adb4cb15d882321f2494158161881', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=620&quality=45&auto=format&fit=max&dpr=2&s=ba477f1262eacfbac216863ab68bcda2', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=605&quality=85&auto=format&fit=max&s=5dcf0120a0b1c13f227ad39e619eecfb', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=605&quality=45&auto=format&fit=max&dpr=2&s=c28b595fb9b1254d0c215d4bdc1d284f', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=445&quality=85&auto=format&fit=max&s=9143757a1d077e10b7a3565dc3fede54', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=445&quality=45&auto=format&fit=max&dpr=2&s=173bb8fead0a131e52c42e9a2e89bf16', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=1300&quality=85&auto=format&fit=max&s=9d912ceb57e29c775b762317d474f5ee', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=1300&quality=45&auto=format&fit=max&dpr=2&s=2a5243f4900595b1be3b00e9e47ab95a', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=1140&quality=85&auto=format&fit=max&s=88428fbfe2000a9569c5b411bb4507c5', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=1140&quality=45&auto=format&fit=max&dpr=2&s=c059b3c53f13476ed8c6f676aa99315a', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=1125&quality=85&auto=format&fit=max&s=1d5f13cb1bca7c98ccf0cb88f5d69284', + width: 1125, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=1125&quality=45&auto=format&fit=max&dpr=2&s=66a31b9d4ee5556abec88d163dc739e0', + width: 2250, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=965&quality=85&auto=format&fit=max&s=834ff1fabbe21e091c3515b28ef88fa1', + width: 965, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=965&quality=45&auto=format&fit=max&dpr=2&s=65e59b05043d4fba1aea5b0d102e96cf', + width: 1930, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=725&quality=85&auto=format&fit=max&s=883d85120b4ccab70f90d642ef92f1ad', + width: 725, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=725&quality=45&auto=format&fit=max&dpr=2&s=87c7c9c1b1d854facd60f7cddb8f8aa4', + width: 1450, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=645&quality=85&auto=format&fit=max&s=0557b2c60d8abb98fc3b44991a892a0c', + width: 645, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=645&quality=45&auto=format&fit=max&dpr=2&s=07022128c2f79c4e5be97753a4d9c5a3', + width: 1290, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=465&quality=85&auto=format&fit=max&s=c0e13f4266f5d37c9d2a45301af77250', + width: 465, + }, + { + src: 'https://i.guim.co.uk/img/media/549ed53a742d50515ddb928c2296f72c812d85d4/0_0_1000_1000/master/1000.png?width=465&quality=45&auto=format&fit=max&dpr=2&s=455fae63058498881792740c5824b943', + width: 930, + }, + ], + }, + ], + data: { + alt: 'Headshot of Justin Lin, Global X Investment Strategist', + caption: 'Justin Lin, Global X investment strategist', + credit: 'Photograph: Global X', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>In particular, he says, AI data centres are changing the nature of demand for electricity, putting increased pressure on electrical infrastructure.</p>', + elementId: '93972f3f-ff85-4497-8191-c9ab2368c02f', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>“The most immediate play a lot of investors think about is investing in data centres or utility companies providing the electricity,” Lin says. “But the bigger picture is the demand for more robust and expanded infrastructure in terms of the grid.”</p>', + elementId: '018d5a5e-a2ff-4bbc-b3a2-f8c0a6b66c72', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '<h2>Energy for generative AI technologies</h2>', + elementId: 'dd609055-17b3-42a1-96c0-950c3de61745', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>With electricity so critical to advanced technologies, copper is in high demand, along with uranium and nuclear power, as well as metals required to support energy generation such as solar, which relies on silver.</p>', + elementId: 'ede7fb40-734a-40d6-a61e-0dd8b2986a00', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>However, Lin says plenty of attention is also being focused on the precise manufacturing processes behind components such as permanent magnets, which use rare earth minerals including neodymium, praseodymium and terbium. These materials are also used in cooling systems, robotics, electric motors and other specialised hardware embedded in AI systems.</p>', + elementId: '5d9b631b-56a2-45ff-a159-29ac5c0c5ee2', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '<h2>‘Demand for everything’</h2>', + elementId: '99bba65a-4120-4103-88e6-8d96f9d08a38', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Such expansive demand, Lin says, is unprecedented. Historically, rapid technological evolution has focused on a single commodity.</p>', + elementId: 'c8d483df-aa56-4007-a557-35c145e0729c', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>“The megatrend around AI involves the most commodities that we’ve ever seen,” he says. “Think about previous megatrends … the electrification of the world was a pure copper play. If you go even further back, the creation of locomotives was a stimulant for coal demand.”</p>', + elementId: '13dbf7e1-7f4f-4d53-86f8-68472eb5de02', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Now, he says, AI is “creating demand for everything”, from exotic metals for chips to lithium for batteries – and this represents a growing opportunity. “For investors, there are more options than would be traditionally available.”</p>', + elementId: '440bc3b2-dba9-46a9-86c9-db77ee78eb78', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>It’s a long-term investment case, Lin says. The AI trade is nascent, even as it becomes part of our everyday life. But as development continues – whether that be in building data centres, electrification of the grid, or shifts towards clean energy – so will the sourcing, processing and distribution of commodities. Global X offers multiple ETFs that cover commodities crucial to the AI race, from copper under <a href="https://www.globalxetfs.com.au/funds/wire/" rel="nofollow">ASX: WIRE</a> to critical metals under <a href="https://www.globalxetfs.com.au/funds/gmtl/" rel="nofollow">ASX: GMTL</a>.</p>', + elementId: 'aef91c51-b18c-402c-95b8-26255d08b394', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>“In the grand scheme of development, these are multi-decade trends that have to be executed on a global scale,” Lin says. “We believe there is a lot of potential value still to come.”</p>', + elementId: 'c09890f4-7b93-4461-b403-3fb9da26c723', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p><em><strong><a href="https://www.globalxetfs.com.au/?utm_source=the-guardian&utm_medium=sponsored-content&utm_campaign=2026-partnership&utm_content=cta" rel="nofollow">Explore Global X Beyond Ordinary ETFs</a>.</strong></em></p>', + elementId: '9798f684-d4e9-4478-8879-51dbf7b50791', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p><em><sub><sup>Issued by Global X Management (AUS) Limited (‘Global X’) (AFSL 466778, ACN 150 433 828). This is general information only and not personal advice. This communication doesn’t consider your personal circumstances or needs. Investors should consider whether Global X products are appropriate for them, obtain financial advice and read the product disclosure statement (PDS) and target market determination (TMD) before making investment decisions. All PDSs and TMDs are available on our website: </sup></sub><a href="https://aus01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fprotect-au.mimecast.com%2Fs%2F9UePCYWK7VCk9J6t96LEP%3Fdomain%3Dglobalxetfs.com.au%2F&data=05%7C02%7Cnatalie.trueman%40sourceservices.com.au%7Ce3864fd216e9434a827f08dc1b0e7d54%7C30aa122549fe41c7bf24be257609e13c%7C1%7C0%7C638414996289827576%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000%7C%7C%7C&sdata=01p4a8QpLTGOu%2Fh5jbebmFBHFaZVPt5a9r9QSVTqmHE%3D&reserved=0" rel="nofollow"><sub><sup>www.globalxetfs.com.au</sup></sub></a><sub><sup>. Investment in any products issued by Global X is subject to risks, including possible delays in repayment and loss of income and principal invested. Past performance is not a reliable indicator of future performance. This content may not be reproduced, distributed or published by any recipient for any purpose. Global X nor any of its affiliates make any warranty as to the accuracy of any data used or displayed in this communication or to the performance of any product.</sup></sub></em></p>', + elementId: '428e55e8-c062-43e2-9458-d13677343a3b', + }, + ], + attributes: { + pinned: false, + keyEvent: false, + summary: false, + }, + blockCreatedOn: 1775078701000, + blockCreatedOnDisplay: '22.25 BST', + blockLastUpdated: 1775078696000, + blockLastUpdatedDisplay: '22.24 BST', + contributors: [], + primaryDateLine: 'Wed 1 Apr 2026 22.25 BST', + secondaryDateLine: 'Last modified on Thu 23 Apr 2026 18.03 BST', + }, + ], + author: {}, + webPublicationDate: '2026-04-01T21:25:01.000Z', + webPublicationDateDeprecated: '2026-04-01T21:25:01.000Z', + webPublicationDateDisplay: 'Wed 1 Apr 2026 22.25 BST', + webPublicationSecondaryDateDisplay: + 'Last modified on Thu 23 Apr 2026 18.03 BST', + editionLongForm: 'UK edition', + editionId: 'UK', + pageId: 'global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + canonicalUrl: + 'https://www.theguardian.com/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + format: { + design: 'ArticleDesign', + theme: 'Labs', + display: 'ImmersiveDisplay', + }, + designType: 'AdvertisementFeature', + tags: [ + { + id: 'global-x-invest-in-innovation/global-x-invest-in-innovation', + type: 'PaidContent', + title: 'Global X: Invest in innovation', + }, + { + id: 'type/article', + type: 'Type', + title: 'Article', + }, + { + id: 'tone/advertisement-features', + type: 'Tone', + title: 'Advertisement features', + }, + { + id: 'world/critical-minerals', + type: 'Keyword', + title: 'Critical minerals', + }, + { + id: 'tracking/commissioningdesk/au-labs', + type: 'Tracking', + title: 'AU Labs', + }, + ], + pillar: 'news', + isLegacyInteractive: false, + isImmersive: true, + sectionLabel: 'Global X: Invest in innovation', + sectionUrl: 'global-x-invest-in-innovation', + sectionName: 'global-x-invest-in-innovation', + subMetaSectionLinks: [], + subMetaKeywordLinks: [ + { + url: '/global-x-invest-in-innovation/global-x-invest-in-innovation', + title: 'Global X: Invest in innovation', + }, + { + url: '/world/critical-minerals', + title: 'Critical minerals', + }, + { + url: '/tone/advertisement-features', + title: 'advertisement features', + }, + ], + shouldHideAds: false, + isAdFreeUser: false, + webURL: 'https://www.theguardian.com/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + linkedData: [ + { + '@type': 'NewsArticle', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + publisher: { + '@type': 'Organization', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com#publisher', + name: 'The Guardian', + url: 'https://www.theguardian.com/', + logo: { + '@type': 'ImageObject', + url: 'https://uploads.guim.co.uk/2018/01/31/TheGuardian_AMP.png', + width: 190, + height: 60, + }, + sameAs: [ + 'https://www.facebook.com/theguardian', + 'https://twitter.com/guardian', + 'https://www.youtube.com/user/TheGuardian', + ], + }, + isAccessibleForFree: true, + isPartOf: { + '@type': ['CreativeWork', 'Product'], + name: 'The Guardian', + productID: 'theguardian.com:basic', + }, + image: [ + 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/457_0_4550_3640/master/4550.jpg?width=700&quality=85&auto=format&fit=max&s=d7e713af81d22b84471d0c983e1f7f2c', + 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/457_0_4550_3640/master/4550.jpg?width=1200&height=1200&quality=85&auto=format&fit=crop&s=a48f2287a220424d0aa1abecf29d247e', + 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/457_0_4550_3640/master/4550.jpg?width=1200&height=900&quality=85&auto=format&fit=crop&s=062d718851c9154a374a8d25cf16188e', + 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/457_0_4550_3640/master/4550.jpg?width=1200&quality=85&auto=format&fit=max&s=166740c7fe9773f48898e310e0bc46ec', + ], + author: [ + { + '@type': 'Person', + name: 'Guardian staff reporter', + }, + ], + datePublished: '2026-04-01T21:25:01.000Z', + headline: 'Rare earth: the commodities powering our AI future', + dateModified: '2026-04-23T17:03:49.000Z', + mainEntityOfPage: + 'https://www.theguardian.com/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + }, + { + '@type': 'WebPage', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + potentialAction: { + '@type': 'ViewAction', + target: 'android-app://com.guardian/https/www.theguardian.com/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + }, + }, + ], + openGraphData: { + 'og:url': + 'https://www.theguardian.com/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + 'article:author': 'Guardian Staff', + 'og:image:width': '700', + 'og:image': + 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/457_0_4550_3640/master/4550.jpg?width=700&quality=85&auto=format&fit=max&s=d7e713af81d22b84471d0c983e1f7f2c', + 'al:ios:url': + 'gnmguardian://global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future?contenttype=Article&source=applinks', + 'article:publisher': 'https://www.facebook.com/theguardian', + 'og:title': 'Rare earth: the commodities powering our AI future', + 'fb:app_id': '180444840287', + 'article:modified_time': '2026-04-23T17:03:49.000Z', + 'og:image:height': '560', + 'og:description': + 'As AI technology evolves, a diverse commodities market is rapidly expanding behind the scenes', + 'og:type': 'article', + 'al:ios:app_store_id': '409128287', + 'article:section': 'Global X: Invest in innovation', + 'article:published_time': '2026-04-01T21:25:01.000Z', + 'article:tag': 'Global X: Invest in innovation,Critical minerals', + 'al:ios:app_name': 'The Guardian', + 'og:site_name': 'the Guardian', + }, + twitterData: { + 'twitter:app:id:iphone': '409128287', + 'twitter:app:name:googleplay': 'The Guardian', + 'twitter:app:name:ipad': 'The Guardian', + 'twitter:card': 'summary_large_image', + 'twitter:app:name:iphone': 'The Guardian', + 'twitter:app:id:ipad': '409128287', + 'twitter:app:id:googleplay': 'com.guardian', + 'twitter:app:url:googleplay': + 'guardian://www.theguardian.com/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + 'twitter:app:url:iphone': + 'gnmguardian://global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future?contenttype=Article&source=twitter', + 'twitter:image': + 'https://i.guim.co.uk/img/media/68dcd461900ec3b3232d488ffc63aa3a03954f95/457_0_4550_3640/master/4550.jpg?width=700&quality=85&auto=format&fit=max&s=d7e713af81d22b84471d0c983e1f7f2c', + 'twitter:site': '@guardian', + 'twitter:app:url:ipad': + 'gnmguardian://global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future?contenttype=Article&source=twitter', + }, + config: { + references: [ + { + 'rich-link': + 'https://www.theguardian.com/environment/2015/oct/19/sign-up-to-the-green-light-email', + }, + ], + shortUrlId: '/p/d8ex5', + switches: { + prebidAppnexusUkRow: true, + clickToView: true, + prebidTrustx: true, + scAdFreeBanner: false, + compareVariantDecision: false, + enableSentryReporting: true, + lazyLoadContainers: true, + adFreeStrictExpiryEnforcement: false, + liveblogRendering: true, + remarketing: true, + registerWithPhone: false, + targeting: true, + extendedMostPopularFronts: true, + slotBodyEnd: true, + emailInlineInFooter: true, + facebookTrackingPixel: true, + serviceWorkerEnabled: false, + iasAdTargeting: true, + extendedMostPopular: true, + prebidAnalytics: true, + imrWorldwide: true, + acast: true, + twitterUwt: true, + prebidAppnexusInvcode: true, + prebidAppnexus: true, + enableDiscussionSwitch: true, + prebidXaxis: true, + interactiveFullHeaderSwitch: false, + discussionAllPageSize: true, + prebidUserSync: true, + audioOnwardJourneySwitch: true, + mobileStickyPrebid: true, + breakingNews: true, + externalVideoEmbeds: true, + carrotTrafficDriver: true, + geoMostPopular: true, + weAreHiring: true, + relatedContent: true, + thirdPartyEmbedTracking: true, + prebidOzone: true, + mostViewedFronts: true, + abSignInGateMainControl: true, + ampPrebid: true, + googleSearch: true, + brazeSwitch: true, + consentManagement: true, + commercial: true, + prebidSonobi: true, + idProfileNavigation: true, + confiantAdVerification: true, + discussionAllowAnonymousRecommendsSwitch: false, + scrollDepth: true, + permutive: true, + comscore: true, + webFonts: true, + prebidImproveDigital: true, + ophan: true, + crosswordSvgThumbnails: true, + prebidTriplelift: true, + weather: true, + commercialOutbrainNewids: true, + dotcomRendering: true, + abSignInGateMainVariant: true, + hostedVideoAutoplay: true, + abAdblockAsk: true, + prebidPubmatic: true, + autoRefresh: true, + enhanceTweets: true, + prebidIndexExchange: true, + prebidOpenx: true, + idCookieRefresh: true, + sharingComments: true, + abSignInGateMandatory: true, + discussionPageSize: true, + smartAppBanner: false, + boostGaUserTimingFidelity: false, + historyTags: true, + mobileStickyLeaderboard: true, + abDeeplyReadTest: false, + surveys: true, + remoteBanner: true, + inizio: true, + prebidHeaderBidding: true, + a9HeaderBidding: true, + lightbox: true, + }, + keywordIds: + 'environment/climate-change,environment/environment,science/scienceofclimatechange,science/science,world/eu,world/europe-news,world/world,environment/flooding,world/wildfires,world/natural-disasters', + sharedAdTargeting: { + ct: 'article', + co: ['jennifer-rankin'], + url: '/environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', + su: ['0'], + edition: 'uk', + tn: ['news'], + p: 'ng', + k: [ + 'eu', + 'flooding', + 'world', + 'europe-news', + 'natural-disasters', + 'science', + 'environment', + 'climate-change', + 'wildfires', + 'scienceofclimatechange', + ], + sh: 'https://www.theguardian.com/p/d8ex5', + }, + toneIds: 'tone/news', + dcrSentryDsn: + 'https://1937ab71c8804b2b8438178dfdd6468f@sentry.io/1377847', + discussionApiUrl: 'https://discussion.theguardian.com/discussion-api', + sentryPublicApiKey: '344003a8d11c41d8800fbad8383fdc50', + commercialBundleUrl: + 'https://assets.guim.co.uk/javascripts/bc58c17d75809551440f/graun.commercial.dcr.js', + discussionApiClientHeader: 'nextgen', + shouldHideReaderRevenue: false, + sentryHost: 'app.getsentry.com/35463', + isPaidContent: true, + headline: 'Headline string', + idApiUrl: 'https://idapi.theguardian.com', + showRelatedContent: true, + adUnit: '/59666047/theguardian.com/environment/article/ng', + videoDuration: 0, + stage: 'PROD', + isSensitive: false, + isDev: false, + ajaxUrl: 'https://api.nextgen.guardianapps.co.uk', + keywords: + 'Climate change,Environment,Climate change,Science,European Union,Europe,World news,Flooding,Wildfires,Natural disasters and extreme weather', + revisionNumber: 'DEV', + section: 'environment', + isPhotoEssay: false, + ampIframeUrl: + 'https://assets.guim.co.uk/data/vendor/b242a49b1588bb36bdaacefe001ca77a/amp-iframe.html', + isLive: false, + host: 'https://www.theguardian.com', + brazeApiKey: '7f28c639-8bda-48ff-a3f6-24345abfc07c', + contentType: 'Article', + idUrl: 'https://profile.theguardian.com', + author: 'Jennifer Rankin', + dfpAccountId: '59666047', + pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', + googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', + mmaUrl: 'https://manage.theguardian.com', + serverSideABTests: {}, + edition: 'UK', + ipsosTag: 'environment', + isLiveBlog: false, + frontendAssetsFullURL: 'https://assets.guim.co.uk/', + webPublicationDate: 1581314427000, + discussionD2Uid: 'zHoBy6HNKsk', + }, + guardianBaseURL: 'https://www.theguardian.com', + contentType: 'Article', + hasRelated: true, + hasStoryPackage: false, + beaconURL: '//phar.gu-web.net', + isCommentable: false, + commercialProperties: { + US: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Global X', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/23/Mar/2026/b266f868-26d9-4632-816d-08ea8b3bad9e-Global-X-Logo.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://www.globalxetfs.com.au/?utm_source=the-guardian&utm_medium=sponsored-content&utm_campaign=2026-partnership&utm_content=logo', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 's', + value: 'global-x-invest-in-innovation', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4ketb', + }, + { + name: 'edition', + value: 'us', + }, + { + name: 'k', + value: [ + 'global-x-invest-in-innovation', + 'critical-minerals', + ], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'url', + value: '/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + }, + { + name: 'p', + value: 'ng', + }, + ], + }, + AU: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Global X', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/23/Mar/2026/b266f868-26d9-4632-816d-08ea8b3bad9e-Global-X-Logo.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://www.globalxetfs.com.au/?utm_source=the-guardian&utm_medium=sponsored-content&utm_campaign=2026-partnership&utm_content=logo', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 's', + value: 'global-x-invest-in-innovation', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4ketb', + }, + { + name: 'edition', + value: 'au', + }, + { + name: 'k', + value: [ + 'global-x-invest-in-innovation', + 'critical-minerals', + ], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'url', + value: '/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + }, + { + name: 'p', + value: 'ng', + }, + ], + }, + UK: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Global X', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/23/Mar/2026/b266f868-26d9-4632-816d-08ea8b3bad9e-Global-X-Logo.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://www.globalxetfs.com.au/?utm_source=the-guardian&utm_medium=sponsored-content&utm_campaign=2026-partnership&utm_content=logo', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 's', + value: 'global-x-invest-in-innovation', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4ketb', + }, + { + name: 'edition', + value: 'uk', + }, + { + name: 'k', + value: [ + 'global-x-invest-in-innovation', + 'critical-minerals', + ], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'url', + value: '/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + }, + { + name: 'p', + value: 'ng', + }, + ], + }, + INT: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Global X', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/23/Mar/2026/b266f868-26d9-4632-816d-08ea8b3bad9e-Global-X-Logo.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://www.globalxetfs.com.au/?utm_source=the-guardian&utm_medium=sponsored-content&utm_campaign=2026-partnership&utm_content=logo', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 's', + value: 'global-x-invest-in-innovation', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4ketb', + }, + { + name: 'k', + value: [ + 'global-x-invest-in-innovation', + 'critical-minerals', + ], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'edition', + value: 'int', + }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'url', + value: '/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + }, + { + name: 'p', + value: 'ng', + }, + ], + }, + EUR: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Global X', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/23/Mar/2026/b266f868-26d9-4632-816d-08ea8b3bad9e-Global-X-Logo.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://www.globalxetfs.com.au/?utm_source=the-guardian&utm_medium=sponsored-content&utm_campaign=2026-partnership&utm_content=logo', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 's', + value: 'global-x-invest-in-innovation', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4ketb', + }, + { + name: 'k', + value: [ + 'global-x-invest-in-innovation', + 'critical-minerals', + ], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 'edition', + value: 'eur', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'url', + value: '/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + }, + { + name: 'p', + value: 'ng', + }, + ], + }, + }, + pageType: { + hasShowcaseMainElement: false, + isFront: false, + isLiveblog: false, + isMinuteArticle: false, + isPaidContent: true, + isPreview: false, + isSensitive: false, + }, + trailText: + 'As AI technology evolves, a diverse commodities market is rapidly expanding behind the scenes', + nav: { + currentUrl: '/global-x-invest-in-innovation', + pillars: [ + { + title: 'News', + url: '/', + longTitle: 'Headlines', + iconName: 'home', + children: [ + { + title: 'UK', + url: '/uk-news', + longTitle: 'UK news', + children: [ + { + title: 'UK politics', + url: '/politics', + }, + { + title: 'Education', + url: '/education', + children: [ + { + title: 'Schools', + url: '/education/schools', + }, + { + title: 'Teachers', + url: '/teacher-network', + }, + { + title: 'Universities', + url: '/education/universities', + }, + { + title: 'Students', + url: '/education/students', + }, + ], + }, + { + title: 'Media', + url: '/media', + }, + { + title: 'Society', + url: '/society', + }, + { + title: 'Law', + url: '/law', + }, + { + title: 'Scotland', + url: '/uk/scotland', + }, + { + title: 'Wales', + url: '/uk/wales', + }, + { + title: 'Northern Ireland', + url: '/uk/northernireland', + }, + ], + }, + { + title: 'US politics', + url: '/us-news/us-politics', + }, + { + title: 'World', + url: '/world', + longTitle: 'World news', + children: [ + { + title: 'Europe', + url: '/world/europe-news', + }, + { + title: 'US news', + url: '/us-news', + longTitle: 'US news', + }, + { + title: 'Americas', + url: '/world/americas', + }, + { + title: 'Asia', + url: '/world/asia', + }, + { + title: 'Australia', + url: '/australia-news', + longTitle: 'Australia news', + }, + { + title: 'Middle East', + url: '/world/middleeast', + }, + { + title: 'Africa', + url: '/world/africa', + }, + { + title: 'Inequality', + url: '/inequality', + }, + { + title: 'Global development', + url: '/global-development', + }, + ], + }, + { + title: 'Climate crisis', + url: '/environment/climate-crisis', + }, + { + title: 'Middle East', + url: '/world/middleeast', + }, + { + title: 'Ukraine', + url: '/world/ukraine', + }, + { + title: 'Football', + url: '/football', + children: [ + { + title: 'Live scores', + url: '/football/live', + longTitle: 'football/live', + }, + { + title: 'Tables', + url: '/football/tables', + longTitle: 'football/tables', + }, + { + title: 'Fixtures', + url: '/football/fixtures', + longTitle: 'football/fixtures', + }, + { + title: 'Results', + url: '/football/results', + longTitle: 'football/results', + }, + { + title: 'Competitions', + url: '/football/competitions', + longTitle: 'football/competitions', + }, + { + title: 'Clubs', + url: '/football/teams', + longTitle: 'football/teams', + }, + ], + }, + { + title: 'Newsletters', + url: '/email-newsletters', + }, + { + title: 'Business', + url: '/business', + children: [ + { + title: 'Economics', + url: '/business/economics', + }, + { + title: 'Banking', + url: '/business/banking', + }, + { + title: 'Money', + url: '/money', + children: [ + { + title: 'Property', + url: '/money/property', + }, + { + title: 'Pensions', + url: '/money/pensions', + }, + { + title: 'Savings', + url: '/money/savings', + }, + { + title: 'Borrowing', + url: '/money/debt', + }, + { + title: 'Careers', + url: '/money/work-and-careers', + }, + ], + }, + { + title: 'Markets', + url: '/business/stock-markets', + }, + { + title: 'Project Syndicate', + url: '/business/series/project-syndicate-economists', + }, + { + title: 'B2B', + url: '/business-to-business', + }, + { + title: 'Retail', + url: '/business/retail', + }, + ], + }, + { + title: 'Environment', + url: '/environment', + children: [ + { + title: 'Climate crisis', + url: '/environment/climate-crisis', + }, + { + title: 'Wildlife', + url: '/environment/wildlife', + }, + { + title: 'Energy', + url: '/environment/energy', + }, + { + title: 'Pollution', + url: '/environment/pollution', + }, + ], + }, + { + title: 'UK politics', + url: '/politics', + }, + { + title: 'Science', + url: '/science', + }, + { + title: 'Tech', + url: '/technology', + }, + { + title: 'Global development', + url: '/global-development', + }, + { + title: 'Obituaries', + url: '/obituaries', + }, + ], + }, + { + title: 'Opinion', + url: '/commentisfree', + longTitle: 'Opinion home', + iconName: 'home', + children: [ + { + title: 'The Guardian view', + url: '/profile/editorial', + }, + { + title: 'Columnists', + url: '/uk/columnists', + }, + { + title: 'Cartoons', + url: '/tone/cartoons', + }, + { + title: 'Opinion videos', + url: '/type/video+tone/comment', + }, + { + title: 'Letters', + url: '/tone/letters', + }, + ], + }, + { + title: 'Sport', + url: '/sport', + longTitle: 'Sport home', + iconName: 'home', + children: [ + { + title: 'Football', + url: '/football', + children: [ + { + title: 'Live scores', + url: '/football/live', + longTitle: 'football/live', + }, + { + title: 'Tables', + url: '/football/tables', + longTitle: 'football/tables', + }, + { + title: 'Fixtures', + url: '/football/fixtures', + longTitle: 'football/fixtures', + }, + { + title: 'Results', + url: '/football/results', + longTitle: 'football/results', + }, + { + title: 'Competitions', + url: '/football/competitions', + longTitle: 'football/competitions', + }, + { + title: 'Clubs', + url: '/football/teams', + longTitle: 'football/teams', + }, + ], + }, + { + title: 'Cricket', + url: '/sport/cricket', + }, + { + title: 'Rugby union', + url: '/sport/rugby-union', + }, + { + title: 'Tennis', + url: '/sport/tennis', + }, + { + title: 'Cycling', + url: '/sport/cycling', + }, + { + title: 'F1', + url: '/sport/formulaone', + }, + { + title: 'Golf', + url: '/sport/golf', + }, + { + title: 'Boxing', + url: '/sport/boxing', + }, + { + title: 'Rugby league', + url: '/sport/rugbyleague', + }, + { + title: 'Racing', + url: '/sport/horse-racing', + }, + { + title: 'US sports', + url: '/sport/us-sport', + }, + ], + }, + { + title: 'Culture', + url: '/culture', + longTitle: 'Culture home', + iconName: 'home', + children: [ + { + title: 'Film', + url: '/film', + }, + { + title: 'Music', + url: '/music', + }, + { + title: 'TV & radio', + url: '/tv-and-radio', + }, + { + title: 'Books', + url: '/books', + }, + { + title: 'Art & design', + url: '/artanddesign', + }, + { + title: 'Stage', + url: '/stage', + }, + { + title: 'Games', + url: '/games', + }, + { + title: 'Classical', + url: '/music/classicalmusicandopera', + }, + ], + }, + { + title: 'Lifestyle', + url: '/lifeandstyle', + longTitle: 'Lifestyle home', + iconName: 'home', + children: [ + { + title: 'The Filter', + url: '/uk/thefilter', + }, + { + title: 'Fashion', + url: '/fashion', + }, + { + title: 'Food', + url: '/food', + }, + { + title: 'Recipes', + url: '/tone/recipes', + }, + { + title: 'Travel', + url: '/travel', + children: [ + { + title: 'UK', + url: '/travel/uk', + }, + { + title: 'Europe', + url: '/travel/europe', + }, + { + title: 'US', + url: '/travel/usa', + }, + ], + }, + { + title: 'Health & fitness', + url: '/lifeandstyle/health-and-wellbeing', + }, + { + title: 'Women', + url: '/lifeandstyle/women', + }, + { + title: 'Men', + url: '/lifeandstyle/men', + }, + { + title: 'Love & sex', + url: '/lifeandstyle/love-and-sex', + }, + { + title: 'Beauty', + url: '/fashion/beauty', + }, + { + title: 'Home & garden', + url: '/lifeandstyle/home-and-garden', + }, + { + title: 'Money', + url: '/money', + children: [ + { + title: 'Property', + url: '/money/property', + }, + { + title: 'Pensions', + url: '/money/pensions', + }, + { + title: 'Savings', + url: '/money/savings', + }, + { + title: 'Borrowing', + url: '/money/debt', + }, + { + title: 'Careers', + url: '/money/work-and-careers', + }, + ], + }, + { + title: 'Cars', + url: '/technology/motoring', + }, + ], + }, + ], + otherLinks: [ + { + title: 'The Guardian app', + url: 'https://app.adjust.com/16xt6hai', + }, + { + title: 'Video', + url: '/video', + }, + { + title: 'Podcasts', + url: '/podcasts', + }, + { + title: 'Pictures', + url: '/inpictures', + }, + { + title: 'Newsletters', + url: '/email-newsletters', + }, + { + title: "Today's paper", + url: '/theguardian', + children: [ + { + title: 'Obituaries', + url: '/obituaries', + }, + { + title: 'G2', + url: '/theguardian/g2', + }, + { + title: 'Journal', + url: '/theguardian/journal', + }, + { + title: 'Saturday', + url: '/theguardian/saturday', + }, + ], + }, + { + title: 'Inside the Guardian', + url: 'https://www.theguardian.com/insidetheguardian', + }, + { + title: 'Guardian Weekly', + url: 'https://www.theguardian.com/weekly?INTCMP=gdnwb_mawns_editorial_gweekly_GW_TopNav_UK', + }, + { + title: 'Crosswords', + url: '/crosswords', + children: [ + { + title: 'Blog', + url: '/crosswords/crossword-blog', + }, + { + title: 'Quick', + url: '/crosswords/series/quick', + }, + { + title: 'Sunday quick', + url: '/crosswords/series/sunday-quick', + }, + { + title: 'Mini', + url: '/crosswords/series/mini-crossword', + }, + { + title: 'Quick cryptic', + url: '/crosswords/series/quick-cryptic', + }, + { + title: 'Quiptic', + url: '/crosswords/series/quiptic', + }, + { + title: 'Cryptic', + url: '/crosswords/series/cryptic', + }, + { + title: 'Prize', + url: '/crosswords/series/prize', + }, + { + title: 'Genius', + url: '/crosswords/series/genius', + }, + { + title: 'Weekend', + url: '/crosswords/series/weekend-crossword', + }, + { + title: 'Special', + url: '/crosswords/series/special', + }, + ], + }, + { + title: 'Wordiply', + url: 'https://www.wordiply.com', + }, + { + title: 'Corrections', + url: '/theguardian/series/corrections-and-clarifications', + }, + { + title: 'Tips', + url: 'https://www.theguardian.com/tips', + }, + ], + brandExtensions: [ + { + title: 'Search jobs', + url: 'https://jobs.theguardian.com', + }, + { + title: 'Hire with Guardian Jobs', + url: 'https://recruiters.theguardian.com/?utm_source=gdnwb&utm_medium=navbar&utm_campaign=Guardian_Navbar_Recruiters&CMP_TU=trdmkt&CMP_BUNIT=jobs', + }, + { + title: 'Holidays', + url: 'https://holidays.theguardian.com?INTCMP=holidays_uk_web_newheader', + }, + { + title: 'Live events', + url: 'https://www.theguardian.com/guardian-live-events?INTCMP=live_uk_header_dropdown', + }, + { + title: 'About Us', + url: '/about', + }, + { + title: 'Digital Archive', + url: 'https://theguardian.newspapers.com', + }, + { + title: 'Guardian Print Shop', + url: '/artanddesign/series/gnm-print-sales', + }, + { + title: 'Patrons', + url: 'https://patrons.theguardian.com/?INTCMP=header_patrons', + }, + { + title: 'Guardian Licensing', + url: 'https://licensing.theguardian.com/', + }, + ], + readerRevenueLinks: { + header: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=header_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=header_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=header_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=header_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_supporter_cta%22%7D', + }, + footer: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=footer_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=footer_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=footer_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=footer_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_supporter_cta%22%7D', + }, + sideMenu: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=side_menu_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=side_menu_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=side_menu_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=mobilenav_print_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22mobilenav_print_cta%22%7D', + }, + ampHeader: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=header_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=header_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=amp_header_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22amp_header_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=header_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_supporter_cta%22%7D', + }, + ampFooter: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=amp_footer_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=amp_footer_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=footer_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=amp_footer_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_supporter_cta%22%7D', + }, + }, + }, + showBottomSocialButtons: true, + pageFooter: { + footerLinks: [ + [ + { + text: 'About us', + url: '/about', + dataLinkName: 'uk : footer : about us', + extraClasses: '', + }, + { + text: 'Help', + url: 'https://manage.theguardian.com/help-centre', + dataLinkName: 'uk : footer : tech feedback', + extraClasses: 'js-tech-feedback-report', + }, + { + text: 'Complaints & corrections', + url: '/info/complaints-and-corrections', + dataLinkName: 'complaints', + extraClasses: '', + }, + { + text: 'Contact us', + url: '/help/contact-us', + dataLinkName: 'uk : footer : contact us', + extraClasses: '', + }, + { + text: 'Tip us off', + url: 'https://www.theguardian.com/tips', + dataLinkName: 'uk : footer : tips', + extraClasses: '', + }, + { + text: 'SecureDrop', + url: 'https://www.theguardian.com/securedrop', + dataLinkName: 'securedrop', + extraClasses: '', + }, + { + text: 'Privacy policy', + url: '/info/privacy', + dataLinkName: 'privacy', + extraClasses: '', + }, + { + text: 'Cookie policy', + url: '/info/cookies', + dataLinkName: 'cookie', + extraClasses: '', + }, + { + text: 'Modern Slavery Act', + url: 'https://uploads.guim.co.uk/2026/08/10/Modern_Slavery_Statement_2026.pdf', + dataLinkName: 'uk : footer : modern slavery act statement', + extraClasses: '', + }, + { + text: 'Tax strategy', + url: 'https://uploads.guim.co.uk/2026/07/22/STL_Global_Tax_Strategy_for_the_year_to_31_March_2027.pdf', + dataLinkName: 'uk : footer : tax strategy', + extraClasses: '', + }, + { + text: 'Terms & conditions', + url: '/help/terms-of-service', + dataLinkName: 'terms', + extraClasses: '', + }, + ], + [ + { + text: 'All topics', + url: '/index/subjects/a', + dataLinkName: 'uk : footer : all topics', + extraClasses: '', + }, + { + text: 'All writers', + url: '/index/contributors', + dataLinkName: 'uk : footer : all contributors', + extraClasses: '', + }, + { + text: 'Newsletters', + url: '/email-newsletters?INTCMP=DOTCOM_FOOTER_NEWSLETTER_UK', + dataLinkName: 'uk : footer : newsletters', + extraClasses: '', + }, + { + text: 'Digital newspaper archive', + url: 'https://theguardian.newspapers.com', + dataLinkName: 'digital newspaper archive', + extraClasses: '', + }, + { + text: 'Bluesky', + url: 'https://bsky.app/profile/theguardian.com', + dataLinkName: 'uk : footer : Bluesky', + extraClasses: '', + }, + { + text: 'Facebook', + url: 'https://www.facebook.com/theguardian', + dataLinkName: 'uk : footer : Facebook', + extraClasses: '', + }, + { + text: 'Instagram', + url: 'https://www.instagram.com/guardian', + dataLinkName: 'uk : footer : Instagram', + extraClasses: '', + }, + { + text: 'LinkedIn', + url: 'https://www.linkedin.com/company/theguardian', + dataLinkName: 'uk : footer : LinkedIn', + extraClasses: '', + }, + { + text: 'Threads', + url: 'https://www.threads.com/@guardian', + dataLinkName: 'uk : footer : Threads', + extraClasses: '', + }, + { + text: 'TikTok', + url: 'https://www.tiktok.com/@guardian', + dataLinkName: 'uk : footer : TikTok', + extraClasses: '', + }, + { + text: 'YouTube', + url: 'https://www.youtube.com/user/TheGuardian', + dataLinkName: 'uk : footer : YouTube', + extraClasses: '', + }, + ], + [ + { + text: 'Advertise with us', + url: 'https://advertising.theguardian.com', + dataLinkName: 'uk : footer : advertise with us', + extraClasses: '', + }, + { + text: 'Guardian Labs', + url: '/guardian-labs', + dataLinkName: 'uk : footer : guardian labs', + extraClasses: '', + }, + { + text: 'Search jobs', + url: 'https://jobs.theguardian.com', + dataLinkName: 'uk : footer : jobs', + extraClasses: '', + }, + { + text: 'Patrons', + url: 'https://patrons.theguardian.com?INTCMP=footer_patrons', + dataLinkName: 'uk : footer : patrons', + extraClasses: '', + }, + { + text: 'Work with us', + url: 'https://workwithus.theguardian.com/', + dataLinkName: 'uk : footer : work with us', + extraClasses: '', + }, + { + text: 'Accessibility settings', + url: '/help/accessibility-help', + dataLinkName: 'accessibility settings', + extraClasses: '', + }, + ], + ], + }, + publication: 'theguardian.com', + shouldHideReaderRevenue: true, + slotMachineFlags: '', + contributionsServiceUrl: 'https://contributions.guardianapis.com', + isSpecialReport: false, + showTableOfContents: false, + lang: 'en', + isRightToLeftLang: false, +}; diff --git a/dotcom-rendering/fixtures/generated/fe-articles/LabsImmersiveParrtjima.ts b/dotcom-rendering/fixtures/generated/fe-articles/LabsImmersiveParrtjima.ts new file mode 100644 index 00000000000..805735377bd --- /dev/null +++ b/dotcom-rendering/fixtures/generated/fe-articles/LabsImmersiveParrtjima.ts @@ -0,0 +1,2018 @@ +/** + * DO NOT EDIT THIS FILE! + * + * This file was automatically generated using the gen-fixtures.js script. Any edits + * you make here will be lost. + * + * If the data in these fixtures is not what you expect then + * + * 1. Refresh the data using 'make gen-fixtures' or + * 2. if the latest live data is not what you need, then consider editing + * gen-fixtures.js directly. + */ + +import type { FEArticle } from '../../../src/frontend/feArticle'; + +export const LabsImmersiveParrtjima: FEArticle = { + version: 3, + headline: + 'Ancient stories in a new light: what to expect as Parrtjima returns to the Red Centre', + standfirst: + '<p>Parrtjima – A Festival in Light returns to Alice Springs (Mparntwe) for its 10th instalment on 4-13 April 2025, promising a dazzling blend of immersive art, live music and cultural storytelling under a star-studded desert sky</p>', + webTitle: + 'Ancient stories in a new light: what to expect as Parrtjima returns to the Red Centre', + mainMediaElements: [ + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'immersive', + media: { + allImages: [ + { + index: 0, + fields: { + height: '667', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/1000.jpg', + }, + { + index: 1, + fields: { + height: '333', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/500.jpg', + }, + { + index: 2, + fields: { + height: '93', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/140.jpg', + }, + { + index: 3, + fields: { + height: '1067', + width: '1600', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/1600.jpg', + }, + { + index: 4, + fields: { + isMaster: 'true', + height: '1067', + width: '1600', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg', + }, + ], + }, + elementId: '225b250e-acdc-4a78-b187-a1e832c1f659', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=620&quality=85&auto=format&fit=max&s=2cdf5ca3aa66622d3c4335eb24426f8a', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=a63e39f78960828a242bc83467f6787e', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=700&quality=85&auto=format&fit=max&s=d25e62abc3994a7ebce200dec15146e6', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=e26031ec9abe5f2db896e50f60479273', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=620&quality=85&auto=format&fit=max&s=2cdf5ca3aa66622d3c4335eb24426f8a', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=a63e39f78960828a242bc83467f6787e', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=645&quality=85&auto=format&fit=max&s=4facb648706318584cdde6bd095ba80a', + width: 645, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=645&quality=45&auto=format&fit=max&dpr=2&s=b645aad75b19e90af10493f7c3e0da00', + width: 1290, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=465&quality=85&auto=format&fit=max&s=667a331b173be8395f3fe6993e71edd2', + width: 465, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=81ff01bcbe7f8f3137b57b43460f01dd', + width: 930, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [], + }, + { + weighting: 'supporting', + srcSet: [], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=1020&quality=85&auto=format&fit=max&s=92fe1cb152b086849750900042889992', + width: 1020, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=1020&quality=45&auto=format&fit=max&dpr=2&s=ac02d036e1184092f9e758728b3fbc85', + width: 2040, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=940&quality=85&auto=format&fit=max&s=4d4f44103bdaac33e8a2877b76e827a2', + width: 940, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=940&quality=45&auto=format&fit=max&dpr=2&s=591fd7604dc76fdbf106dd23704c6aed', + width: 1880, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=700&quality=85&auto=format&fit=max&s=d25e62abc3994a7ebce200dec15146e6', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=e26031ec9abe5f2db896e50f60479273', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=700&quality=85&auto=format&fit=max&s=d25e62abc3994a7ebce200dec15146e6', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=e26031ec9abe5f2db896e50f60479273', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=660&quality=85&auto=format&fit=max&s=e6d9548e147e19e857e6122c72a06e79', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=2de732909c82b7df35ae218d1aa316b4', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=645&quality=85&auto=format&fit=max&s=4facb648706318584cdde6bd095ba80a', + width: 645, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=645&quality=45&auto=format&fit=max&dpr=2&s=b645aad75b19e90af10493f7c3e0da00', + width: 1290, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=465&quality=85&auto=format&fit=max&s=667a331b173be8395f3fe6993e71edd2', + width: 465, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=81ff01bcbe7f8f3137b57b43460f01dd', + width: 930, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=1900&quality=85&auto=format&fit=max&s=e79c819ebfca2bb98d09897e45bc020e', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=6fd4df199188a78c12998e4b8af01f65', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=1300&quality=85&auto=format&fit=max&s=4b006db634dcf7512bb8fc32ea844920', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=b0c0c2da495401763123d83c4223d23c', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=1140&quality=85&auto=format&fit=max&s=e62da3f61313b00265380d944fb945e0', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=8477f8d5a6b0e97136a721fd632550e0', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=980&quality=85&auto=format&fit=max&s=b22358525e4de77596f36944fc29a816', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=2075ed8ae91a584fa0c6de7e367fe78f', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=740&quality=85&auto=format&fit=max&s=7b19e783d7b899a433d61cb18d1b5abc', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=453d52bceb407b1ef527fbe9d608f2a5', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=660&quality=85&auto=format&fit=max&s=e6d9548e147e19e857e6122c72a06e79', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=2de732909c82b7df35ae218d1aa316b4', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=480&quality=85&auto=format&fit=max&s=98dc7b5a9422f5c339bc0c23fb329650', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/master/1600.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=973675dd9344329c668a783b4a8ceaf9', + width: 960, + }, + ], + }, + ], + data: { + alt: 'First Nations digital artwork projection', + caption: 'Photograph: Lisa Hatz', + credit: 'Photograph: Lisa Hatz Photography/Lisa Hatz', + }, + }, + ], + main: '<figure class="element element-image" data-media-id="8aa5ef0d67d64b6057d098f04392d6b2e3b011ea"> <img src="https://media.guim.co.uk/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_0_1600_1067/1000.jpg" alt="First Nations digital artwork projection" width="1000" height="667" class="gu-image" /> <figcaption> <span class="element-image__caption">Photograph: Lisa Hatz</span> <span class="element-image__credit">Photograph: Lisa Hatz Photography/Lisa Hatz</span> </figcaption> </figure>', + keyEvents: [], + blocks: [ + { + id: '678513fb8f08a644578d73a4', + elements: [ + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>While Parrtjima – A Festival in Light may be celebrating a decade of brilliance, its mission remains the same: to shine a spotlight on the beauty, depth and timeless wisdom of Aboriginal culture.</p>', + elementId: '07ec468e-95d3-4135-9177-bcf009470bd4', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>This year’s program offers something for everyone – from awe-inspiring performances and interactive art to fun, belly laughs and cultural reflection. Far more than just a breathtaking visual display, the festival is a celebration of the world’s oldest living culture.</p>', + elementId: '74c99937-ad1a-4954-9e47-9caccb5f6ece', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Here are just some of the experiences to look forward to at Parrtjima – A Festival in Light in April, 2025.</p>', + elementId: '804c8411-7d63-416b-ad7a-099c9b969121', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '<h2>Troy Cassar-Daley takes to the stage</h2>', + elementId: '0deb637c-d8d5-43c2-99ac-8457d30a2272', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>One of Australia’s most celebrated musicians, Troy Cassar-Daley, will feature on the opening weekend of this year’s festival. Known for his soulful storytelling through country and western music, Cassar-Daley has won more than 40 Golden Guitar awards. His powerful live performance will resonate across the desert plains, offering a moving experience for festivalgoers.</p>', + elementId: 'ac463594-889a-4305-8cf4-229c58c03f5e', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>“Most people across the Territory love country and western music and Troy brings such excellence to the stage,” says Parrtjima’s curator, Rhoda Roberts. “Seeing him perform out there will be magical.”</p>', + elementId: '826e5efe-bf36-45e7-bf3a-84116ce78de8', + }, + { + _type: 'model.dotcomrendering.pageElements.InteractiveBlockElement', + scriptUrl: + 'https://interactive.guim.co.uk/2025/01/vertical-slideshow-v2/boot.js', + alt: '15ZLthiz_Djwg9PVSMU3zV48D8ur7m6XO3TO38K4mmjE', + elementId: '3a42613b-c65d-4ebd-b3a1-63d1ab898b48', + isMandatory: false, + role: 'immersive', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p><sup>Photographs: supplied by Parrtjima</sup></p>', + elementId: '5a0ee9c7-dff3-44dc-9477-6a0f795f4a03', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '<h2>Darwin Symphony Orchestra in the desert</h2>', + elementId: '12b61c2f-8741-43fb-a23b-0a3f44e4feec', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>The Darwin Symphony Orchestra will perform at Parrtjima for the first time on Saturday 12 April. The orchestra is collaborating with Indigenous musicians to present an open-air performance that will bring together more than 50 musicians in a dazzling show of light and sound over 90 minutes, as the orchestra shares the stage with Desert Divas Catherine Satour, Casii Williams and Bronwyn Stuart.</p>', + elementId: 'b1beebcc-33c6-43aa-a068-a328a5b24b43', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>The orchestra will also perform Gurrulwa Guligi (Big Wind), an original piece by Bilawara Lee and Netanela Mizrahi.</p>', + elementId: 'eed82b83-9714-4546-8a25-b40d92750b08', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>“To hear a symphony out in the desert looking up at that starry, starry sky will be an incredible experience,” Roberts says.</p>', + elementId: '7e80b360-f61a-4961-bd75-3240d0ce5def', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>While entry to the festival is free, the Darwin Symphony Orchestra performance will be ticketed – so book early. For an extra special treat, you can also book in for an extended experience, including a Welcome to Country by a local custodian and bush food-infused canapés and drinks, before watching the show from a prime vantage point.</p>', + elementId: 'b2f801b5-31a9-4e29-a9bd-48ea3739e462', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '<h2>Where art and play collide</h2>', + elementId: 'f044f2d3-4370-464c-9f31-07bea85cedd4', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>The heart of Parrtjima lies in its light installations, in which ancient stories are illuminated through digital technology. Each piece is designed with deep cultural meaning in mind, blending traditional art with music and high-tech displays.</p>', + elementId: '206ae2db-244f-4fe1-a127-dab39b7e043e', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>“I love the installations; for me, they are what makes the festival,” Roberts says.</p>', + elementId: 'a979468d-81d5-46a7-a014-bd98c5d1f34f', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Roberts says she is most proud of one of Parrtjima’s most popular installations, Grounded, a massive, animated art projection that brings stories to life through interactive visuals spread out like a magic carpet at Alice Springs Desert Park. Local artists from Central and Western Desert regions create stunning works that are digitally projected onto the red sands.</p>', + elementId: '646adbe3-9f5a-483a-81e3-527d8050d7f4', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>But the magic truly happens when children – and the young at heart – run, jump and play among the projections and installations.</p>', + elementId: '6a652a1a-b6be-4fc1-9fa4-0652bcf2c01c', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>“They become part of the artwork,” Roberts says. “It doesn’t matter where they come from; they’re squealing and laughing and hearing Aboriginal language and music … they’re taking away a joyous memory of Aboriginal culture – and it’s experiences that create change.”</p>', + elementId: '1e4c718b-b9dc-4725-96f4-85b6ca9e7d2b', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '<h2>Indigenous comedy</h2>', + elementId: 'fc95b2cb-d8b0-42bb-98ad-3b41ebcf3997', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>For the first time, Parrtjima will host an Indigenous comedy night. “I’m very excited about it because people don’t often see Aboriginal people laugh,” Roberts says. “It’s not to say we can’t share serious stories, but to actually hear black fella comedy – it’s very dry and they take the mickey out of each other – people don’t often come across that style of comedy.”</p>', + elementId: '07ea4dd9-58b1-400a-abad-5eed3177f0f8', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Expect an evening of witty storytelling and cultural insights delivered in hilarious ways.</p>', + elementId: '47aad3a1-46e2-4545-945c-93e825b1562b', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>When you’re not enjoying installations, workshops, performances and talks, you can buy authentic Indigenous art from the Buy Blak Market on site on the opening weekend, have a bite to eat at the outdoor food court, listen to local DJs and bands, and soak up the vibe. “There’s such a buzz in the air, it’s fun – I love it,” Roberts says.</p>', + elementId: 'a8e73361-5a9a-4f6b-abfa-88aa2470d1bc', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>The event is free to attend and runs nightly, leaving your days free to explore. After a few days at the festival, heading to Uluru-Kata Tjuta National Park for cultural immersion of a different kind is recommended – it’s a beautiful 450km drive.</p>', + elementId: 'ac953447-6358-4491-bd54-f201cc5f715f', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p><a href="https://parrtjimaaustralia.com.au/what-to-expect-at-parrtjima-2025/" rel="nofollow">Register</a> for Any Day Festival Admission, to access the venue on any evening throughout the festival. The venue is open from 6pm to 10pm, with speakers and workshop sessions starting at 6.45pm and live music from 7.30pm.</p>', + elementId: '40bdebcc-9724-43c1-b5c8-5d1110c64de3', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p><em><strong><a href="https://parrtjimaaustralia.com.au/?utm_campaign=Parrtjima%202025&utm_source=display&utm_medium=display&utm_content=guardian" rel="nofollow">Discover Parrtjima – A Festival in Light.</a></strong></em></p>', + elementId: '9cc258ee-6f4d-413d-a151-220d08c4c3b4', + }, + ], + attributes: { + pinned: false, + keyEvent: false, + summary: false, + }, + blockCreatedOn: 1736906844000, + blockCreatedOnDisplay: '02.07 GMT', + blockLastUpdated: 1736980095000, + blockLastUpdatedDisplay: '22.28 GMT', + blockFirstPublished: 1736980096000, + blockFirstPublishedDisplay: '22.28 GMT', + blockFirstPublishedDisplayNoTimezone: '22.28', + contributors: [], + primaryDateLine: 'Wed 15 Jan 2025 02.07 GMT', + secondaryDateLine: 'Last modified on Thu 23 Jan 2025 01.58 GMT', + }, + ], + author: {}, + webPublicationDate: '2025-01-15T02:07:24.000Z', + webPublicationDateDeprecated: '2025-01-15T02:07:24.000Z', + webPublicationDateDisplay: 'Wed 15 Jan 2025 02.07 GMT', + webPublicationSecondaryDateDisplay: + 'Last modified on Thu 23 Jan 2025 01.58 GMT', + editionLongForm: 'UK edition', + editionId: 'UK', + pageId: 'parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + canonicalUrl: + 'https://www.theguardian.com/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + format: { + design: 'ArticleDesign', + theme: 'Labs', + display: 'ImmersiveDisplay', + }, + designType: 'AdvertisementFeature', + tags: [ + { + id: 'parrtjima-2025-where-ancient-stories-come-to-light/parrtjima-2025-where-ancient-stories-come-to-light', + type: 'PaidContent', + title: 'Parrtjima 2025: Where ancient stories come to light', + }, + { + id: 'type/article', + type: 'Type', + title: 'Article', + }, + { + id: 'tone/advertisement-features', + type: 'Tone', + title: 'Advertisement features', + }, + { + id: 'tracking/commissioningdesk/au-labs', + type: 'Tracking', + title: 'AU Labs', + }, + ], + pillar: 'news', + isLegacyInteractive: false, + isImmersive: true, + sectionLabel: 'Parrtjima 2025: Where ancient stories come to light', + sectionUrl: 'parrtjima-2025-where-ancient-stories-come-to-light', + sectionName: 'parrtjima-2025-where-ancient-stories-come-to-light', + subMetaSectionLinks: [], + subMetaKeywordLinks: [ + { + url: '/parrtjima-2025-where-ancient-stories-come-to-light/parrtjima-2025-where-ancient-stories-come-to-light', + title: 'Parrtjima 2025: Where ancient stories come to light', + }, + { + url: '/tone/advertisement-features', + title: 'advertisement features', + }, + ], + shouldHideAds: false, + isAdFreeUser: false, + webURL: 'https://www.theguardian.com/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + linkedData: [ + { + '@type': 'NewsArticle', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + publisher: { + '@type': 'Organization', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com#publisher', + name: 'The Guardian', + url: 'https://www.theguardian.com/', + logo: { + '@type': 'ImageObject', + url: 'https://uploads.guim.co.uk/2018/01/31/TheGuardian_AMP.png', + width: 190, + height: 60, + }, + sameAs: [ + 'https://www.facebook.com/theguardian', + 'https://twitter.com/guardian', + 'https://www.youtube.com/user/TheGuardian', + ], + }, + isAccessibleForFree: true, + isPartOf: { + '@type': ['CreativeWork', 'Product'], + name: 'The Guardian', + productID: 'theguardian.com:basic', + }, + image: [ + 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_107_1600_960/master/1600.jpg?width=700&quality=85&auto=format&fit=max&s=d219197c6b7e8119cde7fa5059267e96', + 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_107_1600_960/master/1600.jpg?width=1200&height=1200&quality=85&auto=format&fit=crop&s=9b9537648bcbf442f5122b583ed7297c', + 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_107_1600_960/master/1600.jpg?width=1200&height=900&quality=85&auto=format&fit=crop&s=bb78f00c0b7eb786d70325793ccb98a4', + 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_107_1600_960/master/1600.jpg?width=1200&quality=85&auto=format&fit=max&s=8d366bf94208c9c27e7aa1c0b26d7f6f', + ], + author: [ + { + '@type': 'Person', + name: 'Guardian staff reporter', + }, + ], + datePublished: '2025-01-15T02:07:24.000Z', + headline: + 'Ancient stories in a new light: what to expect as Parrtjima returns to the Red Centre', + dateModified: '2025-01-23T01:58:57.000Z', + mainEntityOfPage: + 'https://www.theguardian.com/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + }, + { + '@type': 'WebPage', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + potentialAction: { + '@type': 'ViewAction', + target: 'android-app://com.guardian/https/www.theguardian.com/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + }, + }, + ], + openGraphData: { + 'og:url': + 'https://www.theguardian.com/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + 'article:author': 'Guardian Staff', + 'og:image:width': '700', + 'og:image': + 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_107_1600_960/master/1600.jpg?width=700&quality=85&auto=format&fit=max&s=d219197c6b7e8119cde7fa5059267e96', + 'al:ios:url': + 'gnmguardian://parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre?contenttype=Article&source=applinks', + 'article:publisher': 'https://www.facebook.com/theguardian', + 'og:title': + 'Ancient stories in a new light: what to expect as Parrtjima returns to the Red Centre', + 'fb:app_id': '180444840287', + 'article:modified_time': '2025-01-23T01:58:57.000Z', + 'og:image:height': '420', + 'og:description': + 'Parrtjima – A Festival in Light returns to Alice Springs (Mparntwe) for its 10th instalment on 4-13 April 2025, promising a dazzling blend of immersive art, live music and cultural storytelling under a star-studded desert sky', + 'og:type': 'article', + 'al:ios:app_store_id': '409128287', + 'article:section': + 'Parrtjima 2025: Where ancient stories come to light', + 'article:published_time': '2025-01-15T02:07:24.000Z', + 'article:tag': 'Parrtjima 2025: Where ancient stories come to light', + 'al:ios:app_name': 'The Guardian', + 'og:site_name': 'the Guardian', + }, + twitterData: { + 'twitter:app:id:iphone': '409128287', + 'twitter:app:name:googleplay': 'The Guardian', + 'twitter:app:name:ipad': 'The Guardian', + 'twitter:card': 'summary_large_image', + 'twitter:app:name:iphone': 'The Guardian', + 'twitter:app:id:ipad': '409128287', + 'twitter:app:id:googleplay': 'com.guardian', + 'twitter:app:url:googleplay': + 'guardian://www.theguardian.com/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + 'twitter:app:url:iphone': + 'gnmguardian://parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre?contenttype=Article&source=twitter', + 'twitter:image': + 'https://i.guim.co.uk/img/media/8aa5ef0d67d64b6057d098f04392d6b2e3b011ea/0_107_1600_960/master/1600.jpg?width=700&quality=85&auto=format&fit=max&s=d219197c6b7e8119cde7fa5059267e96', + 'twitter:site': '@guardian', + 'twitter:app:url:ipad': + 'gnmguardian://parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre?contenttype=Article&source=twitter', + }, + config: { + references: [ + { + 'rich-link': + 'https://www.theguardian.com/environment/2015/oct/19/sign-up-to-the-green-light-email', + }, + ], + shortUrlId: '/p/d8ex5', + switches: { + prebidAppnexusUkRow: true, + clickToView: true, + prebidTrustx: true, + scAdFreeBanner: false, + compareVariantDecision: false, + enableSentryReporting: true, + lazyLoadContainers: true, + adFreeStrictExpiryEnforcement: false, + liveblogRendering: true, + remarketing: true, + registerWithPhone: false, + targeting: true, + extendedMostPopularFronts: true, + slotBodyEnd: true, + emailInlineInFooter: true, + facebookTrackingPixel: true, + serviceWorkerEnabled: false, + iasAdTargeting: true, + extendedMostPopular: true, + prebidAnalytics: true, + imrWorldwide: true, + acast: true, + twitterUwt: true, + prebidAppnexusInvcode: true, + prebidAppnexus: true, + enableDiscussionSwitch: true, + prebidXaxis: true, + interactiveFullHeaderSwitch: false, + discussionAllPageSize: true, + prebidUserSync: true, + audioOnwardJourneySwitch: true, + mobileStickyPrebid: true, + breakingNews: true, + externalVideoEmbeds: true, + carrotTrafficDriver: true, + geoMostPopular: true, + weAreHiring: true, + relatedContent: true, + thirdPartyEmbedTracking: true, + prebidOzone: true, + mostViewedFronts: true, + abSignInGateMainControl: true, + ampPrebid: true, + googleSearch: true, + brazeSwitch: true, + consentManagement: true, + commercial: true, + prebidSonobi: true, + idProfileNavigation: true, + confiantAdVerification: true, + discussionAllowAnonymousRecommendsSwitch: false, + scrollDepth: true, + permutive: true, + comscore: true, + webFonts: true, + prebidImproveDigital: true, + ophan: true, + crosswordSvgThumbnails: true, + prebidTriplelift: true, + weather: true, + commercialOutbrainNewids: true, + dotcomRendering: true, + abSignInGateMainVariant: true, + hostedVideoAutoplay: true, + abAdblockAsk: true, + prebidPubmatic: true, + autoRefresh: true, + enhanceTweets: true, + prebidIndexExchange: true, + prebidOpenx: true, + idCookieRefresh: true, + sharingComments: true, + abSignInGateMandatory: true, + discussionPageSize: true, + smartAppBanner: false, + boostGaUserTimingFidelity: false, + historyTags: true, + mobileStickyLeaderboard: true, + abDeeplyReadTest: false, + surveys: true, + remoteBanner: true, + inizio: true, + prebidHeaderBidding: true, + a9HeaderBidding: true, + lightbox: true, + }, + keywordIds: + 'environment/climate-change,environment/environment,science/scienceofclimatechange,science/science,world/eu,world/europe-news,world/world,environment/flooding,world/wildfires,world/natural-disasters', + sharedAdTargeting: { + ct: 'article', + co: ['jennifer-rankin'], + url: '/environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', + su: ['0'], + edition: 'uk', + tn: ['news'], + p: 'ng', + k: [ + 'eu', + 'flooding', + 'world', + 'europe-news', + 'natural-disasters', + 'science', + 'environment', + 'climate-change', + 'wildfires', + 'scienceofclimatechange', + ], + sh: 'https://www.theguardian.com/p/d8ex5', + }, + toneIds: 'tone/news', + dcrSentryDsn: + 'https://1937ab71c8804b2b8438178dfdd6468f@sentry.io/1377847', + discussionApiUrl: 'https://discussion.theguardian.com/discussion-api', + sentryPublicApiKey: '344003a8d11c41d8800fbad8383fdc50', + commercialBundleUrl: + 'https://assets.guim.co.uk/javascripts/bc58c17d75809551440f/graun.commercial.dcr.js', + discussionApiClientHeader: 'nextgen', + shouldHideReaderRevenue: false, + sentryHost: 'app.getsentry.com/35463', + isPaidContent: true, + headline: 'Headline string', + idApiUrl: 'https://idapi.theguardian.com', + showRelatedContent: true, + adUnit: '/59666047/theguardian.com/environment/article/ng', + videoDuration: 0, + stage: 'PROD', + isSensitive: false, + isDev: false, + ajaxUrl: 'https://api.nextgen.guardianapps.co.uk', + keywords: + 'Climate change,Environment,Climate change,Science,European Union,Europe,World news,Flooding,Wildfires,Natural disasters and extreme weather', + revisionNumber: 'DEV', + section: 'environment', + isPhotoEssay: false, + ampIframeUrl: + 'https://assets.guim.co.uk/data/vendor/b242a49b1588bb36bdaacefe001ca77a/amp-iframe.html', + isLive: false, + host: 'https://www.theguardian.com', + brazeApiKey: '7f28c639-8bda-48ff-a3f6-24345abfc07c', + contentType: 'Article', + idUrl: 'https://profile.theguardian.com', + author: 'Jennifer Rankin', + dfpAccountId: '59666047', + pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', + googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', + mmaUrl: 'https://manage.theguardian.com', + serverSideABTests: {}, + edition: 'UK', + ipsosTag: 'environment', + isLiveBlog: false, + frontendAssetsFullURL: 'https://assets.guim.co.uk/', + webPublicationDate: 1581314427000, + discussionD2Uid: 'zHoBy6HNKsk', + }, + guardianBaseURL: 'https://www.theguardian.com', + contentType: 'Article', + hasRelated: true, + hasStoryPackage: false, + beaconURL: '//phar.gu-web.net', + isCommentable: false, + commercialProperties: { + US: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Northern Territory Major Events Company', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/07/Jan/2025/cb60e65c-002c-46c3-8335-61565be07372-Parrtjima logo_resized.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://parrtjimaaustralia.com.au/?utm_campaign=Parrtjima%202025&utm_source=display&utm_medium=display&utm_content=guardian', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 'su', + value: ['0'], + }, + { + name: 'edition', + value: 'us', + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'parrtjima-2025-where-ancient-stories-come-to-light', + }, + { + name: 'url', + value: '/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/xxbctd', + }, + { + name: 'k', + value: [ + 'parrtjima-2025-where-ancient-stories-come-to-light', + ], + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, + ], + }, + AU: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Northern Territory Major Events Company', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/07/Jan/2025/cb60e65c-002c-46c3-8335-61565be07372-Parrtjima logo_resized.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://parrtjimaaustralia.com.au/?utm_campaign=Parrtjima%202025&utm_source=display&utm_medium=display&utm_content=guardian', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 'su', + value: ['0'], + }, + { + name: 'edition', + value: 'au', + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'parrtjima-2025-where-ancient-stories-come-to-light', + }, + { + name: 'url', + value: '/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/xxbctd', + }, + { + name: 'k', + value: [ + 'parrtjima-2025-where-ancient-stories-come-to-light', + ], + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, + ], + }, + UK: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Northern Territory Major Events Company', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/07/Jan/2025/cb60e65c-002c-46c3-8335-61565be07372-Parrtjima logo_resized.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://parrtjimaaustralia.com.au/?utm_campaign=Parrtjima%202025&utm_source=display&utm_medium=display&utm_content=guardian', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 'su', + value: ['0'], + }, + { + name: 'edition', + value: 'uk', + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'parrtjima-2025-where-ancient-stories-come-to-light', + }, + { + name: 'url', + value: '/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/xxbctd', + }, + { + name: 'k', + value: [ + 'parrtjima-2025-where-ancient-stories-come-to-light', + ], + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, + ], + }, + INT: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Northern Territory Major Events Company', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/07/Jan/2025/cb60e65c-002c-46c3-8335-61565be07372-Parrtjima logo_resized.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://parrtjimaaustralia.com.au/?utm_campaign=Parrtjima%202025&utm_source=display&utm_medium=display&utm_content=guardian', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 'su', + value: ['0'], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'parrtjima-2025-where-ancient-stories-come-to-light', + }, + { + name: 'url', + value: '/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/xxbctd', + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'k', + value: [ + 'parrtjima-2025-where-ancient-stories-come-to-light', + ], + }, + { + name: 'edition', + value: 'int', + }, + ], + }, + EUR: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Northern Territory Major Events Company', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/07/Jan/2025/cb60e65c-002c-46c3-8335-61565be07372-Parrtjima logo_resized.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://parrtjimaaustralia.com.au/?utm_campaign=Parrtjima%202025&utm_source=display&utm_medium=display&utm_content=guardian', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 'su', + value: ['0'], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'parrtjima-2025-where-ancient-stories-come-to-light', + }, + { + name: 'edition', + value: 'eur', + }, + { + name: 'url', + value: '/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/xxbctd', + }, + { + name: 'k', + value: [ + 'parrtjima-2025-where-ancient-stories-come-to-light', + ], + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, + ], + }, + }, + pageType: { + hasShowcaseMainElement: false, + isFront: false, + isLiveblog: false, + isMinuteArticle: false, + isPaidContent: true, + isPreview: false, + isSensitive: false, + }, + trailText: + 'Parrtjima – A Festival in Light returns to Alice Springs (Mparntwe) for its 10th instalment on 4-13 April 2025, promising a dazzling blend of immersive art, live music and cultural storytelling under a star-studded desert sky', + nav: { + currentUrl: '/parrtjima-2025-where-ancient-stories-come-to-light', + pillars: [ + { + title: 'News', + url: '/', + longTitle: 'Headlines', + iconName: 'home', + children: [ + { + title: 'UK', + url: '/uk-news', + longTitle: 'UK news', + children: [ + { + title: 'UK politics', + url: '/politics', + }, + { + title: 'Education', + url: '/education', + children: [ + { + title: 'Schools', + url: '/education/schools', + }, + { + title: 'Teachers', + url: '/teacher-network', + }, + { + title: 'Universities', + url: '/education/universities', + }, + { + title: 'Students', + url: '/education/students', + }, + ], + }, + { + title: 'Media', + url: '/media', + }, + { + title: 'Society', + url: '/society', + }, + { + title: 'Law', + url: '/law', + }, + { + title: 'Scotland', + url: '/uk/scotland', + }, + { + title: 'Wales', + url: '/uk/wales', + }, + { + title: 'Northern Ireland', + url: '/uk/northernireland', + }, + ], + }, + { + title: 'US politics', + url: '/us-news/us-politics', + }, + { + title: 'World', + url: '/world', + longTitle: 'World news', + children: [ + { + title: 'Europe', + url: '/world/europe-news', + }, + { + title: 'US news', + url: '/us-news', + longTitle: 'US news', + }, + { + title: 'Americas', + url: '/world/americas', + }, + { + title: 'Asia', + url: '/world/asia', + }, + { + title: 'Australia', + url: '/australia-news', + longTitle: 'Australia news', + }, + { + title: 'Middle East', + url: '/world/middleeast', + }, + { + title: 'Africa', + url: '/world/africa', + }, + { + title: 'Inequality', + url: '/inequality', + }, + { + title: 'Global development', + url: '/global-development', + }, + ], + }, + { + title: 'Climate crisis', + url: '/environment/climate-crisis', + }, + { + title: 'Middle East', + url: '/world/middleeast', + }, + { + title: 'Ukraine', + url: '/world/ukraine', + }, + { + title: 'Football', + url: '/football', + children: [ + { + title: 'Live scores', + url: '/football/live', + longTitle: 'football/live', + }, + { + title: 'Tables', + url: '/football/tables', + longTitle: 'football/tables', + }, + { + title: 'Fixtures', + url: '/football/fixtures', + longTitle: 'football/fixtures', + }, + { + title: 'Results', + url: '/football/results', + longTitle: 'football/results', + }, + { + title: 'Competitions', + url: '/football/competitions', + longTitle: 'football/competitions', + }, + { + title: 'Clubs', + url: '/football/teams', + longTitle: 'football/teams', + }, + ], + }, + { + title: 'Newsletters', + url: '/email-newsletters', + }, + { + title: 'Business', + url: '/business', + children: [ + { + title: 'Economics', + url: '/business/economics', + }, + { + title: 'Banking', + url: '/business/banking', + }, + { + title: 'Money', + url: '/money', + children: [ + { + title: 'Property', + url: '/money/property', + }, + { + title: 'Pensions', + url: '/money/pensions', + }, + { + title: 'Savings', + url: '/money/savings', + }, + { + title: 'Borrowing', + url: '/money/debt', + }, + { + title: 'Careers', + url: '/money/work-and-careers', + }, + ], + }, + { + title: 'Markets', + url: '/business/stock-markets', + }, + { + title: 'Project Syndicate', + url: '/business/series/project-syndicate-economists', + }, + { + title: 'B2B', + url: '/business-to-business', + }, + { + title: 'Retail', + url: '/business/retail', + }, + ], + }, + { + title: 'Environment', + url: '/environment', + children: [ + { + title: 'Climate crisis', + url: '/environment/climate-crisis', + }, + { + title: 'Wildlife', + url: '/environment/wildlife', + }, + { + title: 'Energy', + url: '/environment/energy', + }, + { + title: 'Pollution', + url: '/environment/pollution', + }, + ], + }, + { + title: 'UK politics', + url: '/politics', + }, + { + title: 'Science', + url: '/science', + }, + { + title: 'Tech', + url: '/technology', + }, + { + title: 'Global development', + url: '/global-development', + }, + { + title: 'Obituaries', + url: '/obituaries', + }, + ], + }, + { + title: 'Opinion', + url: '/commentisfree', + longTitle: 'Opinion home', + iconName: 'home', + children: [ + { + title: 'The Guardian view', + url: '/profile/editorial', + }, + { + title: 'Columnists', + url: '/uk/columnists', + }, + { + title: 'Cartoons', + url: '/tone/cartoons', + }, + { + title: 'Opinion videos', + url: '/type/video+tone/comment', + }, + { + title: 'Letters', + url: '/tone/letters', + }, + ], + }, + { + title: 'Sport', + url: '/sport', + longTitle: 'Sport home', + iconName: 'home', + children: [ + { + title: 'Football', + url: '/football', + children: [ + { + title: 'Live scores', + url: '/football/live', + longTitle: 'football/live', + }, + { + title: 'Tables', + url: '/football/tables', + longTitle: 'football/tables', + }, + { + title: 'Fixtures', + url: '/football/fixtures', + longTitle: 'football/fixtures', + }, + { + title: 'Results', + url: '/football/results', + longTitle: 'football/results', + }, + { + title: 'Competitions', + url: '/football/competitions', + longTitle: 'football/competitions', + }, + { + title: 'Clubs', + url: '/football/teams', + longTitle: 'football/teams', + }, + ], + }, + { + title: 'Cricket', + url: '/sport/cricket', + }, + { + title: 'Rugby union', + url: '/sport/rugby-union', + }, + { + title: 'Tennis', + url: '/sport/tennis', + }, + { + title: 'Cycling', + url: '/sport/cycling', + }, + { + title: 'F1', + url: '/sport/formulaone', + }, + { + title: 'Golf', + url: '/sport/golf', + }, + { + title: 'Boxing', + url: '/sport/boxing', + }, + { + title: 'Rugby league', + url: '/sport/rugbyleague', + }, + { + title: 'Racing', + url: '/sport/horse-racing', + }, + { + title: 'US sports', + url: '/sport/us-sport', + }, + ], + }, + { + title: 'Culture', + url: '/culture', + longTitle: 'Culture home', + iconName: 'home', + children: [ + { + title: 'Film', + url: '/film', + }, + { + title: 'Music', + url: '/music', + }, + { + title: 'TV & radio', + url: '/tv-and-radio', + }, + { + title: 'Books', + url: '/books', + }, + { + title: 'Art & design', + url: '/artanddesign', + }, + { + title: 'Stage', + url: '/stage', + }, + { + title: 'Games', + url: '/games', + }, + { + title: 'Classical', + url: '/music/classicalmusicandopera', + }, + ], + }, + { + title: 'Lifestyle', + url: '/lifeandstyle', + longTitle: 'Lifestyle home', + iconName: 'home', + children: [ + { + title: 'The Filter', + url: '/uk/thefilter', + }, + { + title: 'Fashion', + url: '/fashion', + }, + { + title: 'Food', + url: '/food', + }, + { + title: 'Recipes', + url: '/tone/recipes', + }, + { + title: 'Travel', + url: '/travel', + children: [ + { + title: 'UK', + url: '/travel/uk', + }, + { + title: 'Europe', + url: '/travel/europe', + }, + { + title: 'US', + url: '/travel/usa', + }, + ], + }, + { + title: 'Health & fitness', + url: '/lifeandstyle/health-and-wellbeing', + }, + { + title: 'Women', + url: '/lifeandstyle/women', + }, + { + title: 'Men', + url: '/lifeandstyle/men', + }, + { + title: 'Love & sex', + url: '/lifeandstyle/love-and-sex', + }, + { + title: 'Beauty', + url: '/fashion/beauty', + }, + { + title: 'Home & garden', + url: '/lifeandstyle/home-and-garden', + }, + { + title: 'Money', + url: '/money', + children: [ + { + title: 'Property', + url: '/money/property', + }, + { + title: 'Pensions', + url: '/money/pensions', + }, + { + title: 'Savings', + url: '/money/savings', + }, + { + title: 'Borrowing', + url: '/money/debt', + }, + { + title: 'Careers', + url: '/money/work-and-careers', + }, + ], + }, + { + title: 'Cars', + url: '/technology/motoring', + }, + ], + }, + ], + otherLinks: [ + { + title: 'The Guardian app', + url: 'https://app.adjust.com/16xt6hai', + }, + { + title: 'Video', + url: '/video', + }, + { + title: 'Podcasts', + url: '/podcasts', + }, + { + title: 'Pictures', + url: '/inpictures', + }, + { + title: 'Newsletters', + url: '/email-newsletters', + }, + { + title: "Today's paper", + url: '/theguardian', + children: [ + { + title: 'Obituaries', + url: '/obituaries', + }, + { + title: 'G2', + url: '/theguardian/g2', + }, + { + title: 'Journal', + url: '/theguardian/journal', + }, + { + title: 'Saturday', + url: '/theguardian/saturday', + }, + ], + }, + { + title: 'Inside the Guardian', + url: 'https://www.theguardian.com/insidetheguardian', + }, + { + title: 'Guardian Weekly', + url: 'https://www.theguardian.com/weekly?INTCMP=gdnwb_mawns_editorial_gweekly_GW_TopNav_UK', + }, + { + title: 'Crosswords', + url: '/crosswords', + children: [ + { + title: 'Blog', + url: '/crosswords/crossword-blog', + }, + { + title: 'Quick', + url: '/crosswords/series/quick', + }, + { + title: 'Sunday quick', + url: '/crosswords/series/sunday-quick', + }, + { + title: 'Mini', + url: '/crosswords/series/mini-crossword', + }, + { + title: 'Quick cryptic', + url: '/crosswords/series/quick-cryptic', + }, + { + title: 'Quiptic', + url: '/crosswords/series/quiptic', + }, + { + title: 'Cryptic', + url: '/crosswords/series/cryptic', + }, + { + title: 'Prize', + url: '/crosswords/series/prize', + }, + { + title: 'Genius', + url: '/crosswords/series/genius', + }, + { + title: 'Weekend', + url: '/crosswords/series/weekend-crossword', + }, + { + title: 'Special', + url: '/crosswords/series/special', + }, + ], + }, + { + title: 'Wordiply', + url: 'https://www.wordiply.com', + }, + { + title: 'Corrections', + url: '/theguardian/series/corrections-and-clarifications', + }, + { + title: 'Tips', + url: 'https://www.theguardian.com/tips', + }, + ], + brandExtensions: [ + { + title: 'Search jobs', + url: 'https://jobs.theguardian.com', + }, + { + title: 'Hire with Guardian Jobs', + url: 'https://recruiters.theguardian.com/?utm_source=gdnwb&utm_medium=navbar&utm_campaign=Guardian_Navbar_Recruiters&CMP_TU=trdmkt&CMP_BUNIT=jobs', + }, + { + title: 'Holidays', + url: 'https://holidays.theguardian.com?INTCMP=holidays_uk_web_newheader', + }, + { + title: 'Live events', + url: 'https://www.theguardian.com/guardian-live-events?INTCMP=live_uk_header_dropdown', + }, + { + title: 'About Us', + url: '/about', + }, + { + title: 'Digital Archive', + url: 'https://theguardian.newspapers.com', + }, + { + title: 'Guardian Print Shop', + url: '/artanddesign/series/gnm-print-sales', + }, + { + title: 'Patrons', + url: 'https://patrons.theguardian.com/?INTCMP=header_patrons', + }, + { + title: 'Guardian Licensing', + url: 'https://licensing.theguardian.com/', + }, + ], + readerRevenueLinks: { + header: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=header_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=header_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=header_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=header_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_supporter_cta%22%7D', + }, + footer: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=footer_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=footer_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=footer_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=footer_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_supporter_cta%22%7D', + }, + sideMenu: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=side_menu_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=side_menu_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=side_menu_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=mobilenav_print_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22mobilenav_print_cta%22%7D', + }, + ampHeader: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=header_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=header_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=amp_header_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22amp_header_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=header_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_supporter_cta%22%7D', + }, + ampFooter: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=amp_footer_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=amp_footer_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=footer_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=amp_footer_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_supporter_cta%22%7D', + }, + }, + }, + showBottomSocialButtons: true, + pageFooter: { + footerLinks: [ + [ + { + text: 'About us', + url: '/about', + dataLinkName: 'uk : footer : about us', + extraClasses: '', + }, + { + text: 'Help', + url: 'https://manage.theguardian.com/help-centre', + dataLinkName: 'uk : footer : tech feedback', + extraClasses: 'js-tech-feedback-report', + }, + { + text: 'Complaints & corrections', + url: '/info/complaints-and-corrections', + dataLinkName: 'complaints', + extraClasses: '', + }, + { + text: 'Contact us', + url: '/help/contact-us', + dataLinkName: 'uk : footer : contact us', + extraClasses: '', + }, + { + text: 'Tip us off', + url: 'https://www.theguardian.com/tips', + dataLinkName: 'uk : footer : tips', + extraClasses: '', + }, + { + text: 'SecureDrop', + url: 'https://www.theguardian.com/securedrop', + dataLinkName: 'securedrop', + extraClasses: '', + }, + { + text: 'Privacy policy', + url: '/info/privacy', + dataLinkName: 'privacy', + extraClasses: '', + }, + { + text: 'Cookie policy', + url: '/info/cookies', + dataLinkName: 'cookie', + extraClasses: '', + }, + { + text: 'Modern Slavery Act', + url: 'https://uploads.guim.co.uk/2026/08/10/Modern_Slavery_Statement_2026.pdf', + dataLinkName: 'uk : footer : modern slavery act statement', + extraClasses: '', + }, + { + text: 'Tax strategy', + url: 'https://uploads.guim.co.uk/2026/07/22/STL_Global_Tax_Strategy_for_the_year_to_31_March_2027.pdf', + dataLinkName: 'uk : footer : tax strategy', + extraClasses: '', + }, + { + text: 'Terms & conditions', + url: '/help/terms-of-service', + dataLinkName: 'terms', + extraClasses: '', + }, + ], + [ + { + text: 'All topics', + url: '/index/subjects/a', + dataLinkName: 'uk : footer : all topics', + extraClasses: '', + }, + { + text: 'All writers', + url: '/index/contributors', + dataLinkName: 'uk : footer : all contributors', + extraClasses: '', + }, + { + text: 'Newsletters', + url: '/email-newsletters?INTCMP=DOTCOM_FOOTER_NEWSLETTER_UK', + dataLinkName: 'uk : footer : newsletters', + extraClasses: '', + }, + { + text: 'Digital newspaper archive', + url: 'https://theguardian.newspapers.com', + dataLinkName: 'digital newspaper archive', + extraClasses: '', + }, + { + text: 'Bluesky', + url: 'https://bsky.app/profile/theguardian.com', + dataLinkName: 'uk : footer : Bluesky', + extraClasses: '', + }, + { + text: 'Facebook', + url: 'https://www.facebook.com/theguardian', + dataLinkName: 'uk : footer : Facebook', + extraClasses: '', + }, + { + text: 'Instagram', + url: 'https://www.instagram.com/guardian', + dataLinkName: 'uk : footer : Instagram', + extraClasses: '', + }, + { + text: 'LinkedIn', + url: 'https://www.linkedin.com/company/theguardian', + dataLinkName: 'uk : footer : LinkedIn', + extraClasses: '', + }, + { + text: 'Threads', + url: 'https://www.threads.com/@guardian', + dataLinkName: 'uk : footer : Threads', + extraClasses: '', + }, + { + text: 'TikTok', + url: 'https://www.tiktok.com/@guardian', + dataLinkName: 'uk : footer : TikTok', + extraClasses: '', + }, + { + text: 'YouTube', + url: 'https://www.youtube.com/user/TheGuardian', + dataLinkName: 'uk : footer : YouTube', + extraClasses: '', + }, + ], + [ + { + text: 'Advertise with us', + url: 'https://advertising.theguardian.com', + dataLinkName: 'uk : footer : advertise with us', + extraClasses: '', + }, + { + text: 'Guardian Labs', + url: '/guardian-labs', + dataLinkName: 'uk : footer : guardian labs', + extraClasses: '', + }, + { + text: 'Search jobs', + url: 'https://jobs.theguardian.com', + dataLinkName: 'uk : footer : jobs', + extraClasses: '', + }, + { + text: 'Patrons', + url: 'https://patrons.theguardian.com?INTCMP=footer_patrons', + dataLinkName: 'uk : footer : patrons', + extraClasses: '', + }, + { + text: 'Work with us', + url: 'https://workwithus.theguardian.com/', + dataLinkName: 'uk : footer : work with us', + extraClasses: '', + }, + { + text: 'Accessibility settings', + url: '/help/accessibility-help', + dataLinkName: 'accessibility settings', + extraClasses: '', + }, + ], + ], + }, + publication: 'theguardian.com', + shouldHideReaderRevenue: true, + slotMachineFlags: '', + contributionsServiceUrl: 'https://contributions.guardianapis.com', + isSpecialReport: false, + showTableOfContents: false, + lang: 'en', + isRightToLeftLang: false, +}; diff --git a/dotcom-rendering/fixtures/generated/fe-articles/LabsImmersiveVictorianWater.ts b/dotcom-rendering/fixtures/generated/fe-articles/LabsImmersiveVictorianWater.ts new file mode 100644 index 00000000000..af99fcce21d --- /dev/null +++ b/dotcom-rendering/fixtures/generated/fe-articles/LabsImmersiveVictorianWater.ts @@ -0,0 +1,2011 @@ +/** + * DO NOT EDIT THIS FILE! + * + * This file was automatically generated using the gen-fixtures.js script. Any edits + * you make here will be lost. + * + * If the data in these fixtures is not what you expect then + * + * 1. Refresh the data using 'make gen-fixtures' or + * 2. if the latest live data is not what you need, then consider editing + * gen-fixtures.js directly. + */ + +import type { FEArticle } from '../../../src/frontend/feArticle'; + +export const LabsImmersiveVictorianWater: FEArticle = { + version: 3, + headline: 'Small changes, big wins: simple ways to save water at home', + standfirst: + '<p>Every Victorian has a responsibility to make every drop of water count. Here’s how you can do your bit</p>', + webTitle: 'Small changes, big wins: simple ways to save water at home', + mainMediaElements: [ + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'immersive', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '16:9', + height: '1125', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/2000.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '16:9', + height: '563', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/1000.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '16:9', + height: '281', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/500.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '16:9', + height: '79', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/140.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '16:9', + height: '4875', + width: '8660', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/8660.jpg', + }, + { + index: 5, + fields: { + aspectRatio: '16:9', + isMaster: 'true', + height: '4875', + width: '8660', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg', + }, + ], + }, + elementId: '7607a115-e80a-478a-abcb-7232d3b21cd9', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=620&quality=85&auto=format&fit=max&s=68425a5d7558ea3db3b3c45368adc38c', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=e51d0a997e2a2efd46624fdd0b722688', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=700&quality=85&auto=format&fit=max&s=24e72a944c09d8b5554e3fc4707b80b4', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=64e256f78d05960e4594238301b54e40', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=620&quality=85&auto=format&fit=max&s=68425a5d7558ea3db3b3c45368adc38c', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=e51d0a997e2a2efd46624fdd0b722688', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=645&quality=85&auto=format&fit=max&s=70680969cb534c8b3e905952d0ca2730', + width: 645, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=645&quality=45&auto=format&fit=max&dpr=2&s=64900b132f4c879732385027412e7cdf', + width: 1290, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=465&quality=85&auto=format&fit=max&s=acc2c22b2f3e05f2d95e1ae2ce9feb13', + width: 465, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=227bde5ff537f8168b8365b761d20edd', + width: 930, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [], + }, + { + weighting: 'supporting', + srcSet: [], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=1020&quality=85&auto=format&fit=max&s=dd076d12cf83e811b5089c84fddb4954', + width: 1020, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=1020&quality=45&auto=format&fit=max&dpr=2&s=fb6cc9cb6e6bbf2d7dde1f3f2c3699a7', + width: 2040, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=940&quality=85&auto=format&fit=max&s=9563573c76c97045591236b8cd6115a0', + width: 940, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=940&quality=45&auto=format&fit=max&dpr=2&s=1912b3cddd0501374fda9e3ad937f224', + width: 1880, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=700&quality=85&auto=format&fit=max&s=24e72a944c09d8b5554e3fc4707b80b4', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=64e256f78d05960e4594238301b54e40', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=700&quality=85&auto=format&fit=max&s=24e72a944c09d8b5554e3fc4707b80b4', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=64e256f78d05960e4594238301b54e40', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=660&quality=85&auto=format&fit=max&s=82cc62319c2b728c6f770f3edc72c108', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=b329ee096ad4c73b63013bdc7d8187c5', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=645&quality=85&auto=format&fit=max&s=70680969cb534c8b3e905952d0ca2730', + width: 645, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=645&quality=45&auto=format&fit=max&dpr=2&s=64900b132f4c879732385027412e7cdf', + width: 1290, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=465&quality=85&auto=format&fit=max&s=acc2c22b2f3e05f2d95e1ae2ce9feb13', + width: 465, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=227bde5ff537f8168b8365b761d20edd', + width: 930, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=1900&quality=85&auto=format&fit=max&s=e5883f8d502cb63e8968e3930f5b36ae', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=50c357eef9ba337c15455e7152ffef67', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=1300&quality=85&auto=format&fit=max&s=efacc6c3c79f59ae09f835961a636e8a', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=ae0cc536f45d72800cdfe754d677b8a4', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=1140&quality=85&auto=format&fit=max&s=e41b0519630454eb03a45ffa54176b40', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=a9f6a217fdc03c3bec43c28903a0b0f0', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=980&quality=85&auto=format&fit=max&s=69f0a38e7fc64fef69d88e82253612cb', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=06b088cc535e827b5ed897a424720026', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=740&quality=85&auto=format&fit=max&s=f1e71792a286a6726f6fc8b3d51985f1', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=f2264d97ca9ca68d7e0aa81b2d8af53b', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=660&quality=85&auto=format&fit=max&s=82cc62319c2b728c6f770f3edc72c108', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=b329ee096ad4c73b63013bdc7d8187c5', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=480&quality=85&auto=format&fit=max&s=cf522242c069dd78084800f964b8f55e', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/master/8660.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=591fbe6c76b77e5527543c31e0b71fa2', + width: 960, + }, + ], + }, + ], + data: { + alt: 'A father and son packing a dishwasher in their kitchen.', + credit: 'Photograph: Victorian Government', + }, + }, + ], + main: '<figure class="element element-image" data-media-id="f7c1429b1a4921efc43d2f4acc3a65045abfbd40"> <img src="https://media.guim.co.uk/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/0_0_8660_4875/1000.jpg" alt="A father and son packing a dishwasher in their kitchen." width="1000" height="563" class="gu-image" /> </figure>', + keyEvents: [], + blocks: [ + { + id: '69aa5a698f08ef977b470939', + elements: [ + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Life in Victoria comes with many great traditions: trekking up the hill to the MCG; pitching a tent at Wilsons Prom; waiting for the cool change on a hot summer’s night. It’s time to make “saving water” one of them.</p>', + elementId: '66a9a1e5-6239-455e-924a-9ffbc9eb8912', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>The summer of 2025 was Victoria’s<a href="https://www.bom.gov.au/climate/current/season/vic/archive/202502.summary.shtml" rel="nofollow"> driest in more than a decade</a>, and water storage levels are down <a href="https://www.water.vic.gov.au/our-programs/water-monitoring-and-reporting/current-water-snapshot" rel="nofollow">17%</a> compared with last year. As our population grows in the coming years, millions more people will rely on stored water.</p>', + elementId: 'b7fd89b5-1646-4cfc-b181-2dd54c02338d', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>We need to think every day about how we can use less.</p>', + elementId: 'bc3904d1-4cd3-4800-a9a5-1aecf689f0a0', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '<h2>Reconnect with the value of water</h2>', + elementId: 'c634cd4c-ab2c-484b-8c35-ae76bc1479b8', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>It feels obvious to say water is important – it sustains life, after all. But it’s also critical to our economy, powering agriculture, industry, energy and even technology. Access to clean water is crucial for equality, sanitation and disease prevention, and the health of our waterways has an impact on biodiversity and sustainability. Australia’s rivers, lakes and oceans also hold cultural significance for many First Nations groups.</p>', + elementId: '184237ff-62df-4658-a23b-a8414b36f3c1', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>While our water storages fall, our communities – and demand – continue to grow. About <a href="https://www.water.vic.gov.au/about-us/news/news-stories/latest-victorian-water-accounts-data-released" rel="nofollow">17%</a> of our stored water goes to towns and cities, where we use it for everyday essentials such as drinking, showering, gardening and washing our dogs. As La Niña – a weather pattern that typically brings more rain and cooler temperatures – <a href="https://www.bom.gov.au/climate/enso/" rel="nofollow">weakens</a>, we must all take steps to make every drop count.</p>', + elementId: '1db97f1c-11c8-4ca5-bd59-665d95d07a0f', + }, + { + _type: 'model.dotcomrendering.pageElements.InteractiveBlockElement', + scriptUrl: + 'https://interactive.guim.co.uk/embed/iframe-wrapper/0.1/boot.js', + alt: 'Easy water wins flip cards', + elementId: '21977d69-06f0-43ad-a4aa-c54ce8089684', + isMandatory: false, + url: 'https://interactive.guim.co.uk/embed/2026/03/vicgov/index.html?k=12', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '<h2>Water-saving rules are in place all year round</h2>', + elementId: 'a9e6921b-10f0-45fe-bae7-067ecfa5c481', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Saving water never goes out of season. In Victoria, <a href="https://www.water.vic.gov.au/for-households/water-restrictions-and-rules/permanent-water-saving-rules" rel="nofollow">permanent water-saving rules</a> are in place all year round. These rules are designed to help every Victorian make better choices about how, when and why they turn on the tap. They include:</p>', + elementId: '25f9386d-5849-4bf1-94da-13760da1926e', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<ul>\n <li>\n <p>Using a leak-free hose with a trigger nozzle.</p></li>\n <li>\n <p>Only using garden watering systems between 6pm and 10am, when evaporation is lower.</p></li>\n <li>\n <p>Not using water to wash hard surfaces such as driveways or paths, except in certain circumstances.</p></li>\n</ul>', + elementId: '9729865b-669f-49d2-84ff-ec1ebe3bbb36', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>These rules only apply to drinking water supplied through the state’s water supply system.</p>', + elementId: '35ebda97-972d-44e6-a7e5-e920f12069c6', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Other water sources, such as rainwater and bore water, aren’t affected.</p>', + elementId: '149bb6a5-95f8-4834-b941-3dc2bd852dd4', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>They’re also different from the water restrictions you might remember from previous years; Victorians can still shower, wash their cars and top up their pools without limitations.</p>', + elementId: '53d468db-105b-4ee1-9435-b6e1e1102864', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '<h2>Every little action counts</h2>', + elementId: '0695c44f-b65b-46a0-af13-7aa44224330f', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Sticking to these measures is great, but there are other everyday changes we can all make to save even more water. On average, Melburnians used about 169 litres of water a day in 2024-25, but with a few tweaks we could get that number below 150.</p>', + elementId: '3fc747ba-79e3-4a6c-b1bf-2b35962dbbd0', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Indoors, even turning off the tap while you brush your teeth can save many litres of water a week. Kick your habit of checking your pores in the mirror while the shower heats up. Consider popping a bucket in there to collect water for plants or toilet flushing (it’s called greywater, and, yes, you can use it to fill your cistern).</p>', + elementId: '0961d88d-c4d2-4bd8-a35d-1d551dbd9a29', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>If you have a garden, think about planting natives, which can thrive in dry conditions and support local wildlife. Mulch can help garden beds retain moisture and keep tree roots cool on hot, thirsty days.</p>', + elementId: '451a9c02-973a-4d8d-a1e9-f5783bcbad90', + }, + { + _type: 'model.dotcomrendering.pageElements.InteractiveBlockElement', + scriptUrl: + 'https://interactive.guim.co.uk/embed/iframe-wrapper/0.1/boot.js', + alt: 'Easy water wins flip cards', + elementId: 'eea38422-f15b-4da6-a96b-eef33ce4d9e1', + isMandatory: false, + url: 'https://interactive.guim.co.uk/embed/2026/03/vicgov/index.html?k=34', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '<h2>Good habits will keep the water flowing</h2>', + elementId: '65083775-2661-4ce4-a247-5f24813abba2', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>While the government is working on building water systems to secure future water supply, what we do at home matters just as much. Small actions can have a big impact when we all work together.</p>', + elementId: '837bef9f-e27e-41db-b80a-d0c2b5630dec', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Pop a reminder on the fridge or bathroom mirror. Collect dirty plates from around the house to make a full dishwasher load. Set an alarm for watering times. Keep thinking about making the best use of water until it becomes second nature.</p>', + elementId: 'e576755f-a926-427e-968a-dcd21e622c4c', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p>Collectively, we face a challenging fight for the water security of future generations. The good habits we develop now will help us all reconnect with the value of water – and keep it flowing for years to come.</p>', + elementId: 'b8054eb3-d33b-4744-bde4-d5cdf36e4cd7', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p><em><strong><a href="https://www.makeeverydropcount.com.au/en" rel="nofollow">Make every drop count.</a></strong></em></p>', + elementId: 'b514e711-449e-484c-93f9-0bf4f992fc09', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '<p><em>Authorised by the Victorian Government, Melbourne.</em></p>', + elementId: '60d7564d-f001-427e-ac05-53274699c886', + }, + ], + attributes: { + pinned: false, + keyEvent: false, + summary: false, + }, + blockCreatedOn: 1772772520000, + blockCreatedOnDisplay: '04.48 GMT', + blockLastUpdated: 1772772301000, + blockLastUpdatedDisplay: '04.45 GMT', + contributors: [], + primaryDateLine: 'Fri 6 Mar 2026 04.48 GMT', + secondaryDateLine: 'Last modified on Fri 13 Mar 2026 08.19 GMT', + }, + ], + author: {}, + webPublicationDate: '2026-03-06T04:48:40.000Z', + webPublicationDateDeprecated: '2026-03-06T04:48:40.000Z', + webPublicationDateDisplay: 'Fri 6 Mar 2026 04.48 GMT', + webPublicationSecondaryDateDisplay: + 'Last modified on Fri 13 Mar 2026 08.19 GMT', + editionLongForm: 'UK edition', + editionId: 'UK', + pageId: 'victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + canonicalUrl: + 'https://www.theguardian.com/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + format: { + design: 'ArticleDesign', + theme: 'Labs', + display: 'ImmersiveDisplay', + }, + designType: 'AdvertisementFeature', + tags: [ + { + id: 'victorian-government-make-every-drop-count/victorian-government-make-every-drop-count', + type: 'PaidContent', + title: 'Victorian Government: Make every drop count', + }, + { + id: 'type/article', + type: 'Type', + title: 'Article', + }, + { + id: 'tone/advertisement-features', + type: 'Tone', + title: 'Advertisement features', + }, + { + id: 'tracking/commissioningdesk/au-labs', + type: 'Tracking', + title: 'AU Labs', + }, + ], + pillar: 'news', + isLegacyInteractive: false, + isImmersive: true, + sectionLabel: 'Victorian Government: Make every drop count', + sectionUrl: 'victorian-government-make-every-drop-count', + sectionName: 'victorian-government-make-every-drop-count', + subMetaSectionLinks: [], + subMetaKeywordLinks: [ + { + url: '/victorian-government-make-every-drop-count/victorian-government-make-every-drop-count', + title: 'Victorian Government: Make every drop count', + }, + { + url: '/tone/advertisement-features', + title: 'advertisement features', + }, + ], + shouldHideAds: false, + isAdFreeUser: false, + webURL: 'https://www.theguardian.com/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + linkedData: [ + { + '@type': 'NewsArticle', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + publisher: { + '@type': 'Organization', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com#publisher', + name: 'The Guardian', + url: 'https://www.theguardian.com/', + logo: { + '@type': 'ImageObject', + url: 'https://uploads.guim.co.uk/2018/01/31/TheGuardian_AMP.png', + width: 190, + height: 60, + }, + sameAs: [ + 'https://www.facebook.com/theguardian', + 'https://twitter.com/guardian', + 'https://www.youtube.com/user/TheGuardian', + ], + }, + isAccessibleForFree: true, + isPartOf: { + '@type': ['CreativeWork', 'Product'], + name: 'The Guardian', + productID: 'theguardian.com:basic', + }, + image: [ + 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/722_0_7217_5773/master/7217.jpg?width=700&quality=85&auto=format&fit=max&s=7a2d4eed43d33cf44fef67f4a02f3beb', + 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/722_0_7217_5773/master/7217.jpg?width=1200&height=1200&quality=85&auto=format&fit=crop&s=1d0bd2b0db541b34f6f54f622c338c60', + 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/722_0_7217_5773/master/7217.jpg?width=1200&height=900&quality=85&auto=format&fit=crop&s=4e8b639611f6b66b3410ca6560d3912f', + 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/722_0_7217_5773/master/7217.jpg?width=1200&quality=85&auto=format&fit=max&s=fe37aa932931fcf7473c484027cdcd73', + ], + author: [ + { + '@type': 'Person', + name: 'Guardian staff reporter', + }, + ], + datePublished: '2026-03-06T04:48:40.000Z', + headline: + 'Small changes, big wins: simple ways to save water at home', + dateModified: '2026-03-13T08:19:20.000Z', + mainEntityOfPage: + 'https://www.theguardian.com/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + }, + { + '@type': 'WebPage', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + potentialAction: { + '@type': 'ViewAction', + target: 'android-app://com.guardian/https/www.theguardian.com/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + }, + }, + ], + openGraphData: { + 'og:url': + 'https://www.theguardian.com/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + 'article:author': 'Guardian Staff', + 'og:image:width': '700', + 'og:image': + 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/722_0_7217_5773/master/7217.jpg?width=700&quality=85&auto=format&fit=max&s=7a2d4eed43d33cf44fef67f4a02f3beb', + 'al:ios:url': + 'gnmguardian://victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home?contenttype=Article&source=applinks', + 'article:publisher': 'https://www.facebook.com/theguardian', + 'og:title': + 'Small changes, big wins: simple ways to save water at home', + 'fb:app_id': '180444840287', + 'article:modified_time': '2026-03-13T08:19:20.000Z', + 'og:image:height': '560', + 'og:description': + 'Every Victorian has a responsibility to make every drop of water count. Here’s how you can do your bit', + 'og:type': 'article', + 'al:ios:app_store_id': '409128287', + 'article:section': 'Victorian Government: Make every drop count', + 'article:published_time': '2026-03-06T04:48:40.000Z', + 'article:tag': 'Victorian Government: Make every drop count', + 'al:ios:app_name': 'The Guardian', + 'og:site_name': 'the Guardian', + }, + twitterData: { + 'twitter:app:id:iphone': '409128287', + 'twitter:app:name:googleplay': 'The Guardian', + 'twitter:app:name:ipad': 'The Guardian', + 'twitter:card': 'summary_large_image', + 'twitter:app:name:iphone': 'The Guardian', + 'twitter:app:id:ipad': '409128287', + 'twitter:app:id:googleplay': 'com.guardian', + 'twitter:app:url:googleplay': + 'guardian://www.theguardian.com/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + 'twitter:app:url:iphone': + 'gnmguardian://victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home?contenttype=Article&source=twitter', + 'twitter:image': + 'https://i.guim.co.uk/img/media/f7c1429b1a4921efc43d2f4acc3a65045abfbd40/722_0_7217_5773/master/7217.jpg?width=700&quality=85&auto=format&fit=max&s=7a2d4eed43d33cf44fef67f4a02f3beb', + 'twitter:site': '@guardian', + 'twitter:app:url:ipad': + 'gnmguardian://victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home?contenttype=Article&source=twitter', + }, + config: { + references: [ + { + 'rich-link': + 'https://www.theguardian.com/environment/2015/oct/19/sign-up-to-the-green-light-email', + }, + ], + shortUrlId: '/p/d8ex5', + switches: { + prebidAppnexusUkRow: true, + clickToView: true, + prebidTrustx: true, + scAdFreeBanner: false, + compareVariantDecision: false, + enableSentryReporting: true, + lazyLoadContainers: true, + adFreeStrictExpiryEnforcement: false, + liveblogRendering: true, + remarketing: true, + registerWithPhone: false, + targeting: true, + extendedMostPopularFronts: true, + slotBodyEnd: true, + emailInlineInFooter: true, + facebookTrackingPixel: true, + serviceWorkerEnabled: false, + iasAdTargeting: true, + extendedMostPopular: true, + prebidAnalytics: true, + imrWorldwide: true, + acast: true, + twitterUwt: true, + prebidAppnexusInvcode: true, + prebidAppnexus: true, + enableDiscussionSwitch: true, + prebidXaxis: true, + interactiveFullHeaderSwitch: false, + discussionAllPageSize: true, + prebidUserSync: true, + audioOnwardJourneySwitch: true, + mobileStickyPrebid: true, + breakingNews: true, + externalVideoEmbeds: true, + carrotTrafficDriver: true, + geoMostPopular: true, + weAreHiring: true, + relatedContent: true, + thirdPartyEmbedTracking: true, + prebidOzone: true, + mostViewedFronts: true, + abSignInGateMainControl: true, + ampPrebid: true, + googleSearch: true, + brazeSwitch: true, + consentManagement: true, + commercial: true, + prebidSonobi: true, + idProfileNavigation: true, + confiantAdVerification: true, + discussionAllowAnonymousRecommendsSwitch: false, + scrollDepth: true, + permutive: true, + comscore: true, + webFonts: true, + prebidImproveDigital: true, + ophan: true, + crosswordSvgThumbnails: true, + prebidTriplelift: true, + weather: true, + commercialOutbrainNewids: true, + dotcomRendering: true, + abSignInGateMainVariant: true, + hostedVideoAutoplay: true, + abAdblockAsk: true, + prebidPubmatic: true, + autoRefresh: true, + enhanceTweets: true, + prebidIndexExchange: true, + prebidOpenx: true, + idCookieRefresh: true, + sharingComments: true, + abSignInGateMandatory: true, + discussionPageSize: true, + smartAppBanner: false, + boostGaUserTimingFidelity: false, + historyTags: true, + mobileStickyLeaderboard: true, + abDeeplyReadTest: false, + surveys: true, + remoteBanner: true, + inizio: true, + prebidHeaderBidding: true, + a9HeaderBidding: true, + lightbox: true, + }, + keywordIds: + 'environment/climate-change,environment/environment,science/scienceofclimatechange,science/science,world/eu,world/europe-news,world/world,environment/flooding,world/wildfires,world/natural-disasters', + sharedAdTargeting: { + ct: 'article', + co: ['jennifer-rankin'], + url: '/environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', + su: ['0'], + edition: 'uk', + tn: ['news'], + p: 'ng', + k: [ + 'eu', + 'flooding', + 'world', + 'europe-news', + 'natural-disasters', + 'science', + 'environment', + 'climate-change', + 'wildfires', + 'scienceofclimatechange', + ], + sh: 'https://www.theguardian.com/p/d8ex5', + }, + toneIds: 'tone/news', + dcrSentryDsn: + 'https://1937ab71c8804b2b8438178dfdd6468f@sentry.io/1377847', + discussionApiUrl: 'https://discussion.theguardian.com/discussion-api', + sentryPublicApiKey: '344003a8d11c41d8800fbad8383fdc50', + commercialBundleUrl: + 'https://assets.guim.co.uk/javascripts/bc58c17d75809551440f/graun.commercial.dcr.js', + discussionApiClientHeader: 'nextgen', + shouldHideReaderRevenue: false, + sentryHost: 'app.getsentry.com/35463', + isPaidContent: true, + headline: 'Headline string', + idApiUrl: 'https://idapi.theguardian.com', + showRelatedContent: true, + adUnit: '/59666047/theguardian.com/environment/article/ng', + videoDuration: 0, + stage: 'PROD', + isSensitive: false, + isDev: false, + ajaxUrl: 'https://api.nextgen.guardianapps.co.uk', + keywords: + 'Climate change,Environment,Climate change,Science,European Union,Europe,World news,Flooding,Wildfires,Natural disasters and extreme weather', + revisionNumber: 'DEV', + section: 'environment', + isPhotoEssay: false, + ampIframeUrl: + 'https://assets.guim.co.uk/data/vendor/b242a49b1588bb36bdaacefe001ca77a/amp-iframe.html', + isLive: false, + host: 'https://www.theguardian.com', + brazeApiKey: '7f28c639-8bda-48ff-a3f6-24345abfc07c', + contentType: 'Article', + idUrl: 'https://profile.theguardian.com', + author: 'Jennifer Rankin', + dfpAccountId: '59666047', + pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', + googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', + mmaUrl: 'https://manage.theguardian.com', + serverSideABTests: {}, + edition: 'UK', + ipsosTag: 'environment', + isLiveBlog: false, + frontendAssetsFullURL: 'https://assets.guim.co.uk/', + webPublicationDate: 1581314427000, + discussionD2Uid: 'zHoBy6HNKsk', + }, + guardianBaseURL: 'https://www.theguardian.com', + contentType: 'Article', + hasRelated: true, + hasStoryPackage: false, + beaconURL: '//phar.gu-web.net', + isCommentable: false, + commercialProperties: { + US: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Victorian Government', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/06/Mar/2026/2b6dc666-be40-43b8-917e-20ee8eb4789c-make every drop count logo.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://www.makeeverydropcount.com.au/en', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 'su', + value: ['0'], + }, + { + name: 's', + value: 'victorian-government-make-every-drop-count', + }, + { + name: 'edition', + value: 'us', + }, + { + name: 'ct', + value: 'article', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4ggez', + }, + { + name: 'k', + value: ['victorian-government-make-every-drop-count'], + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'br', + value: 'p', + }, + { + name: 'url', + value: '/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + }, + { + name: 'sens', + value: 'f', + }, + ], + }, + AU: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Victorian Government', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/06/Mar/2026/2b6dc666-be40-43b8-917e-20ee8eb4789c-make every drop count logo.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://www.makeeverydropcount.com.au/en', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 'su', + value: ['0'], + }, + { + name: 's', + value: 'victorian-government-make-every-drop-count', + }, + { + name: 'edition', + value: 'au', + }, + { + name: 'ct', + value: 'article', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4ggez', + }, + { + name: 'k', + value: ['victorian-government-make-every-drop-count'], + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'br', + value: 'p', + }, + { + name: 'url', + value: '/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + }, + { + name: 'sens', + value: 'f', + }, + ], + }, + UK: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Victorian Government', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/06/Mar/2026/2b6dc666-be40-43b8-917e-20ee8eb4789c-make every drop count logo.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://www.makeeverydropcount.com.au/en', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 'su', + value: ['0'], + }, + { + name: 's', + value: 'victorian-government-make-every-drop-count', + }, + { + name: 'edition', + value: 'uk', + }, + { + name: 'ct', + value: 'article', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4ggez', + }, + { + name: 'k', + value: ['victorian-government-make-every-drop-count'], + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'br', + value: 'p', + }, + { + name: 'url', + value: '/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + }, + { + name: 'sens', + value: 'f', + }, + ], + }, + INT: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Victorian Government', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/06/Mar/2026/2b6dc666-be40-43b8-917e-20ee8eb4789c-make every drop count logo.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://www.makeeverydropcount.com.au/en', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 'su', + value: ['0'], + }, + { + name: 's', + value: 'victorian-government-make-every-drop-count', + }, + { + name: 'ct', + value: 'article', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4ggez', + }, + { + name: 'edition', + value: 'int', + }, + { + name: 'k', + value: ['victorian-government-make-every-drop-count'], + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'br', + value: 'p', + }, + { + name: 'url', + value: '/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + }, + { + name: 'sens', + value: 'f', + }, + ], + }, + EUR: { + branding: { + brandingType: { + name: 'paid-content', + }, + sponsorName: 'Victorian Government', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/06/Mar/2026/2b6dc666-be40-43b8-917e-20ee8eb4789c-make every drop count logo.png', + dimensions: { + width: 280, + height: 180, + }, + link: 'https://www.makeeverydropcount.com.au/en', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + }, + adTargeting: [ + { + name: 'su', + value: ['0'], + }, + { + name: 's', + value: 'victorian-government-make-every-drop-count', + }, + { + name: 'ct', + value: 'article', + }, + { + name: 'edition', + value: 'eur', + }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4ggez', + }, + { + name: 'k', + value: ['victorian-government-make-every-drop-count'], + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'br', + value: 'p', + }, + { + name: 'url', + value: '/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + }, + { + name: 'sens', + value: 'f', + }, + ], + }, + }, + pageType: { + hasShowcaseMainElement: false, + isFront: false, + isLiveblog: false, + isMinuteArticle: false, + isPaidContent: true, + isPreview: false, + isSensitive: false, + }, + trailText: + 'Every Victorian has a responsibility to make every drop of water count. Here’s how you can do your bit', + nav: { + currentUrl: '/victorian-government-make-every-drop-count', + pillars: [ + { + title: 'News', + url: '/', + longTitle: 'Headlines', + iconName: 'home', + children: [ + { + title: 'UK', + url: '/uk-news', + longTitle: 'UK news', + children: [ + { + title: 'UK politics', + url: '/politics', + }, + { + title: 'Education', + url: '/education', + children: [ + { + title: 'Schools', + url: '/education/schools', + }, + { + title: 'Teachers', + url: '/teacher-network', + }, + { + title: 'Universities', + url: '/education/universities', + }, + { + title: 'Students', + url: '/education/students', + }, + ], + }, + { + title: 'Media', + url: '/media', + }, + { + title: 'Society', + url: '/society', + }, + { + title: 'Law', + url: '/law', + }, + { + title: 'Scotland', + url: '/uk/scotland', + }, + { + title: 'Wales', + url: '/uk/wales', + }, + { + title: 'Northern Ireland', + url: '/uk/northernireland', + }, + ], + }, + { + title: 'US politics', + url: '/us-news/us-politics', + }, + { + title: 'World', + url: '/world', + longTitle: 'World news', + children: [ + { + title: 'Europe', + url: '/world/europe-news', + }, + { + title: 'US news', + url: '/us-news', + longTitle: 'US news', + }, + { + title: 'Americas', + url: '/world/americas', + }, + { + title: 'Asia', + url: '/world/asia', + }, + { + title: 'Australia', + url: '/australia-news', + longTitle: 'Australia news', + }, + { + title: 'Middle East', + url: '/world/middleeast', + }, + { + title: 'Africa', + url: '/world/africa', + }, + { + title: 'Inequality', + url: '/inequality', + }, + { + title: 'Global development', + url: '/global-development', + }, + ], + }, + { + title: 'Climate crisis', + url: '/environment/climate-crisis', + }, + { + title: 'Middle East', + url: '/world/middleeast', + }, + { + title: 'Ukraine', + url: '/world/ukraine', + }, + { + title: 'Football', + url: '/football', + children: [ + { + title: 'Live scores', + url: '/football/live', + longTitle: 'football/live', + }, + { + title: 'Tables', + url: '/football/tables', + longTitle: 'football/tables', + }, + { + title: 'Fixtures', + url: '/football/fixtures', + longTitle: 'football/fixtures', + }, + { + title: 'Results', + url: '/football/results', + longTitle: 'football/results', + }, + { + title: 'Competitions', + url: '/football/competitions', + longTitle: 'football/competitions', + }, + { + title: 'Clubs', + url: '/football/teams', + longTitle: 'football/teams', + }, + ], + }, + { + title: 'Newsletters', + url: '/email-newsletters', + }, + { + title: 'Business', + url: '/business', + children: [ + { + title: 'Economics', + url: '/business/economics', + }, + { + title: 'Banking', + url: '/business/banking', + }, + { + title: 'Money', + url: '/money', + children: [ + { + title: 'Property', + url: '/money/property', + }, + { + title: 'Pensions', + url: '/money/pensions', + }, + { + title: 'Savings', + url: '/money/savings', + }, + { + title: 'Borrowing', + url: '/money/debt', + }, + { + title: 'Careers', + url: '/money/work-and-careers', + }, + ], + }, + { + title: 'Markets', + url: '/business/stock-markets', + }, + { + title: 'Project Syndicate', + url: '/business/series/project-syndicate-economists', + }, + { + title: 'B2B', + url: '/business-to-business', + }, + { + title: 'Retail', + url: '/business/retail', + }, + ], + }, + { + title: 'Environment', + url: '/environment', + children: [ + { + title: 'Climate crisis', + url: '/environment/climate-crisis', + }, + { + title: 'Wildlife', + url: '/environment/wildlife', + }, + { + title: 'Energy', + url: '/environment/energy', + }, + { + title: 'Pollution', + url: '/environment/pollution', + }, + ], + }, + { + title: 'UK politics', + url: '/politics', + }, + { + title: 'Science', + url: '/science', + }, + { + title: 'Tech', + url: '/technology', + }, + { + title: 'Global development', + url: '/global-development', + }, + { + title: 'Obituaries', + url: '/obituaries', + }, + ], + }, + { + title: 'Opinion', + url: '/commentisfree', + longTitle: 'Opinion home', + iconName: 'home', + children: [ + { + title: 'The Guardian view', + url: '/profile/editorial', + }, + { + title: 'Columnists', + url: '/uk/columnists', + }, + { + title: 'Cartoons', + url: '/tone/cartoons', + }, + { + title: 'Opinion videos', + url: '/type/video+tone/comment', + }, + { + title: 'Letters', + url: '/tone/letters', + }, + ], + }, + { + title: 'Sport', + url: '/sport', + longTitle: 'Sport home', + iconName: 'home', + children: [ + { + title: 'Football', + url: '/football', + children: [ + { + title: 'Live scores', + url: '/football/live', + longTitle: 'football/live', + }, + { + title: 'Tables', + url: '/football/tables', + longTitle: 'football/tables', + }, + { + title: 'Fixtures', + url: '/football/fixtures', + longTitle: 'football/fixtures', + }, + { + title: 'Results', + url: '/football/results', + longTitle: 'football/results', + }, + { + title: 'Competitions', + url: '/football/competitions', + longTitle: 'football/competitions', + }, + { + title: 'Clubs', + url: '/football/teams', + longTitle: 'football/teams', + }, + ], + }, + { + title: 'Cricket', + url: '/sport/cricket', + }, + { + title: 'Rugby union', + url: '/sport/rugby-union', + }, + { + title: 'Tennis', + url: '/sport/tennis', + }, + { + title: 'Cycling', + url: '/sport/cycling', + }, + { + title: 'F1', + url: '/sport/formulaone', + }, + { + title: 'Golf', + url: '/sport/golf', + }, + { + title: 'Boxing', + url: '/sport/boxing', + }, + { + title: 'Rugby league', + url: '/sport/rugbyleague', + }, + { + title: 'Racing', + url: '/sport/horse-racing', + }, + { + title: 'US sports', + url: '/sport/us-sport', + }, + ], + }, + { + title: 'Culture', + url: '/culture', + longTitle: 'Culture home', + iconName: 'home', + children: [ + { + title: 'Film', + url: '/film', + }, + { + title: 'Music', + url: '/music', + }, + { + title: 'TV & radio', + url: '/tv-and-radio', + }, + { + title: 'Books', + url: '/books', + }, + { + title: 'Art & design', + url: '/artanddesign', + }, + { + title: 'Stage', + url: '/stage', + }, + { + title: 'Games', + url: '/games', + }, + { + title: 'Classical', + url: '/music/classicalmusicandopera', + }, + ], + }, + { + title: 'Lifestyle', + url: '/lifeandstyle', + longTitle: 'Lifestyle home', + iconName: 'home', + children: [ + { + title: 'The Filter', + url: '/uk/thefilter', + }, + { + title: 'Fashion', + url: '/fashion', + }, + { + title: 'Food', + url: '/food', + }, + { + title: 'Recipes', + url: '/tone/recipes', + }, + { + title: 'Travel', + url: '/travel', + children: [ + { + title: 'UK', + url: '/travel/uk', + }, + { + title: 'Europe', + url: '/travel/europe', + }, + { + title: 'US', + url: '/travel/usa', + }, + ], + }, + { + title: 'Health & fitness', + url: '/lifeandstyle/health-and-wellbeing', + }, + { + title: 'Women', + url: '/lifeandstyle/women', + }, + { + title: 'Men', + url: '/lifeandstyle/men', + }, + { + title: 'Love & sex', + url: '/lifeandstyle/love-and-sex', + }, + { + title: 'Beauty', + url: '/fashion/beauty', + }, + { + title: 'Home & garden', + url: '/lifeandstyle/home-and-garden', + }, + { + title: 'Money', + url: '/money', + children: [ + { + title: 'Property', + url: '/money/property', + }, + { + title: 'Pensions', + url: '/money/pensions', + }, + { + title: 'Savings', + url: '/money/savings', + }, + { + title: 'Borrowing', + url: '/money/debt', + }, + { + title: 'Careers', + url: '/money/work-and-careers', + }, + ], + }, + { + title: 'Cars', + url: '/technology/motoring', + }, + ], + }, + ], + otherLinks: [ + { + title: 'The Guardian app', + url: 'https://app.adjust.com/16xt6hai', + }, + { + title: 'Video', + url: '/video', + }, + { + title: 'Podcasts', + url: '/podcasts', + }, + { + title: 'Pictures', + url: '/inpictures', + }, + { + title: 'Newsletters', + url: '/email-newsletters', + }, + { + title: "Today's paper", + url: '/theguardian', + children: [ + { + title: 'Obituaries', + url: '/obituaries', + }, + { + title: 'G2', + url: '/theguardian/g2', + }, + { + title: 'Journal', + url: '/theguardian/journal', + }, + { + title: 'Saturday', + url: '/theguardian/saturday', + }, + ], + }, + { + title: 'Inside the Guardian', + url: 'https://www.theguardian.com/insidetheguardian', + }, + { + title: 'Guardian Weekly', + url: 'https://www.theguardian.com/weekly?INTCMP=gdnwb_mawns_editorial_gweekly_GW_TopNav_UK', + }, + { + title: 'Crosswords', + url: '/crosswords', + children: [ + { + title: 'Blog', + url: '/crosswords/crossword-blog', + }, + { + title: 'Quick', + url: '/crosswords/series/quick', + }, + { + title: 'Sunday quick', + url: '/crosswords/series/sunday-quick', + }, + { + title: 'Mini', + url: '/crosswords/series/mini-crossword', + }, + { + title: 'Quick cryptic', + url: '/crosswords/series/quick-cryptic', + }, + { + title: 'Quiptic', + url: '/crosswords/series/quiptic', + }, + { + title: 'Cryptic', + url: '/crosswords/series/cryptic', + }, + { + title: 'Prize', + url: '/crosswords/series/prize', + }, + { + title: 'Genius', + url: '/crosswords/series/genius', + }, + { + title: 'Weekend', + url: '/crosswords/series/weekend-crossword', + }, + { + title: 'Special', + url: '/crosswords/series/special', + }, + ], + }, + { + title: 'Wordiply', + url: 'https://www.wordiply.com', + }, + { + title: 'Corrections', + url: '/theguardian/series/corrections-and-clarifications', + }, + { + title: 'Tips', + url: 'https://www.theguardian.com/tips', + }, + ], + brandExtensions: [ + { + title: 'Search jobs', + url: 'https://jobs.theguardian.com', + }, + { + title: 'Hire with Guardian Jobs', + url: 'https://recruiters.theguardian.com/?utm_source=gdnwb&utm_medium=navbar&utm_campaign=Guardian_Navbar_Recruiters&CMP_TU=trdmkt&CMP_BUNIT=jobs', + }, + { + title: 'Holidays', + url: 'https://holidays.theguardian.com?INTCMP=holidays_uk_web_newheader', + }, + { + title: 'Live events', + url: 'https://www.theguardian.com/guardian-live-events?INTCMP=live_uk_header_dropdown', + }, + { + title: 'About Us', + url: '/about', + }, + { + title: 'Digital Archive', + url: 'https://theguardian.newspapers.com', + }, + { + title: 'Guardian Print Shop', + url: '/artanddesign/series/gnm-print-sales', + }, + { + title: 'Patrons', + url: 'https://patrons.theguardian.com/?INTCMP=header_patrons', + }, + { + title: 'Guardian Licensing', + url: 'https://licensing.theguardian.com/', + }, + ], + readerRevenueLinks: { + header: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=header_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=header_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=header_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=header_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_supporter_cta%22%7D', + }, + footer: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=footer_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=footer_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=footer_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=footer_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_supporter_cta%22%7D', + }, + sideMenu: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=side_menu_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=side_menu_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=side_menu_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=mobilenav_print_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22mobilenav_print_cta%22%7D', + }, + ampHeader: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=header_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=header_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=amp_header_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22amp_header_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=header_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_supporter_cta%22%7D', + }, + ampFooter: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=amp_footer_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=amp_footer_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=footer_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=amp_footer_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_supporter_cta%22%7D', + }, + }, + }, + showBottomSocialButtons: true, + pageFooter: { + footerLinks: [ + [ + { + text: 'About us', + url: '/about', + dataLinkName: 'uk : footer : about us', + extraClasses: '', + }, + { + text: 'Help', + url: 'https://manage.theguardian.com/help-centre', + dataLinkName: 'uk : footer : tech feedback', + extraClasses: 'js-tech-feedback-report', + }, + { + text: 'Complaints & corrections', + url: '/info/complaints-and-corrections', + dataLinkName: 'complaints', + extraClasses: '', + }, + { + text: 'Contact us', + url: '/help/contact-us', + dataLinkName: 'uk : footer : contact us', + extraClasses: '', + }, + { + text: 'Tip us off', + url: 'https://www.theguardian.com/tips', + dataLinkName: 'uk : footer : tips', + extraClasses: '', + }, + { + text: 'SecureDrop', + url: 'https://www.theguardian.com/securedrop', + dataLinkName: 'securedrop', + extraClasses: '', + }, + { + text: 'Privacy policy', + url: '/info/privacy', + dataLinkName: 'privacy', + extraClasses: '', + }, + { + text: 'Cookie policy', + url: '/info/cookies', + dataLinkName: 'cookie', + extraClasses: '', + }, + { + text: 'Modern Slavery Act', + url: 'https://uploads.guim.co.uk/2026/08/10/Modern_Slavery_Statement_2026.pdf', + dataLinkName: 'uk : footer : modern slavery act statement', + extraClasses: '', + }, + { + text: 'Tax strategy', + url: 'https://uploads.guim.co.uk/2026/07/22/STL_Global_Tax_Strategy_for_the_year_to_31_March_2027.pdf', + dataLinkName: 'uk : footer : tax strategy', + extraClasses: '', + }, + { + text: 'Terms & conditions', + url: '/help/terms-of-service', + dataLinkName: 'terms', + extraClasses: '', + }, + ], + [ + { + text: 'All topics', + url: '/index/subjects/a', + dataLinkName: 'uk : footer : all topics', + extraClasses: '', + }, + { + text: 'All writers', + url: '/index/contributors', + dataLinkName: 'uk : footer : all contributors', + extraClasses: '', + }, + { + text: 'Newsletters', + url: '/email-newsletters?INTCMP=DOTCOM_FOOTER_NEWSLETTER_UK', + dataLinkName: 'uk : footer : newsletters', + extraClasses: '', + }, + { + text: 'Digital newspaper archive', + url: 'https://theguardian.newspapers.com', + dataLinkName: 'digital newspaper archive', + extraClasses: '', + }, + { + text: 'Bluesky', + url: 'https://bsky.app/profile/theguardian.com', + dataLinkName: 'uk : footer : Bluesky', + extraClasses: '', + }, + { + text: 'Facebook', + url: 'https://www.facebook.com/theguardian', + dataLinkName: 'uk : footer : Facebook', + extraClasses: '', + }, + { + text: 'Instagram', + url: 'https://www.instagram.com/guardian', + dataLinkName: 'uk : footer : Instagram', + extraClasses: '', + }, + { + text: 'LinkedIn', + url: 'https://www.linkedin.com/company/theguardian', + dataLinkName: 'uk : footer : LinkedIn', + extraClasses: '', + }, + { + text: 'Threads', + url: 'https://www.threads.com/@guardian', + dataLinkName: 'uk : footer : Threads', + extraClasses: '', + }, + { + text: 'TikTok', + url: 'https://www.tiktok.com/@guardian', + dataLinkName: 'uk : footer : TikTok', + extraClasses: '', + }, + { + text: 'YouTube', + url: 'https://www.youtube.com/user/TheGuardian', + dataLinkName: 'uk : footer : YouTube', + extraClasses: '', + }, + ], + [ + { + text: 'Advertise with us', + url: 'https://advertising.theguardian.com', + dataLinkName: 'uk : footer : advertise with us', + extraClasses: '', + }, + { + text: 'Guardian Labs', + url: '/guardian-labs', + dataLinkName: 'uk : footer : guardian labs', + extraClasses: '', + }, + { + text: 'Search jobs', + url: 'https://jobs.theguardian.com', + dataLinkName: 'uk : footer : jobs', + extraClasses: '', + }, + { + text: 'Patrons', + url: 'https://patrons.theguardian.com?INTCMP=footer_patrons', + dataLinkName: 'uk : footer : patrons', + extraClasses: '', + }, + { + text: 'Work with us', + url: 'https://workwithus.theguardian.com/', + dataLinkName: 'uk : footer : work with us', + extraClasses: '', + }, + { + text: 'Accessibility settings', + url: '/help/accessibility-help', + dataLinkName: 'accessibility settings', + extraClasses: '', + }, + ], + ], + }, + publication: 'theguardian.com', + shouldHideReaderRevenue: true, + slotMachineFlags: '', + contributionsServiceUrl: 'https://contributions.guardianapis.com', + isSpecialReport: false, + showTableOfContents: false, + lang: 'en', + isRightToLeftLang: false, +}; diff --git a/dotcom-rendering/scripts/test-data/gen-fixtures.js b/dotcom-rendering/scripts/test-data/gen-fixtures.js index 095f6d55a1a..3dc5b8ec10a 100644 --- a/dotcom-rendering/scripts/test-data/gen-fixtures.js +++ b/dotcom-rendering/scripts/test-data/gen-fixtures.js @@ -111,6 +111,18 @@ const articles = [ name: 'Labs', url: 'https://www.theguardian.com/whats-in-your-blood-/2018/oct/11/royal-ancestry-genetics-things-to-consider', }, + { + name: 'LabsImmersiveParrtjima', + url: 'https://www.theguardian.com/parrtjima-2025-where-ancient-stories-come-to-light/2025/jan/15/ancient-stories-in-a-new-light-what-to-expect-as-parrtjima-returns-to-the-red-centre', + }, + { + name: 'LabsImmersiveVictorianWater', + url: 'https://www.theguardian.com/victorian-government-make-every-drop-count/2026/mar/06/small-changes-big-wins-simple-ways-to-save-water-at-home', + }, + { + name: 'LabsImmersiveGlobalX', + url: 'https://www.theguardian.com/global-x-invest-in-innovation/2026/apr/02/rare-earth-the-commodities-powering-our-ai-future', + }, { name: 'NumberedList', url: 'https://www.theguardian.com/technology/2019/dec/17/best-smartphone-2019-iphone-oneplus-samsung-and-huawei-compared-and-ranked', diff --git a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx index c5791f1cb8a..d16b7504a89 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.stories.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.stories.tsx @@ -9,6 +9,9 @@ import { Analysis as AnalysisStandardNewsFixture } from '../../fixtures/generate import { Comment as CommentStandardOpinionFixture } from '../../fixtures/generated/fe-articles/Comment'; import { Feature as FeatureStandardCultureFixture } from '../../fixtures/generated/fe-articles/Feature'; import { Labs as PhotoEssayImmersiveLabsFixture } from '../../fixtures/generated/fe-articles/Labs'; +import { LabsImmersiveGlobalX as LabsImmersiveGlobalXFixture } from '../../fixtures/generated/fe-articles/LabsImmersiveGlobalX'; +import { LabsImmersiveParrtjima as LabsImmersiveParrtjimaFixture } from '../../fixtures/generated/fe-articles/LabsImmersiveParrtjima'; +import { LabsImmersiveVictorianWater as LabsImmersiveVictorianWaterFixture } from '../../fixtures/generated/fe-articles/LabsImmersiveVictorianWater'; import { Live as LiveBlogStandardNewsFixture } from '../../fixtures/generated/fe-articles/Live'; import { LiveBlogSingleContributor as LiveBlogSingleContributorFixture } from '../../fixtures/generated/fe-articles/LiveBlogSingleContributor'; import { NewsletterSignup as NewsletterSignupStandardSportFixture } from '../../fixtures/generated/fe-articles/NewsletterSignup'; @@ -263,6 +266,24 @@ const photoEssayImmersiveLabsPortraitArticle = enhanceArticleType( 'Web', ); +/** + * Opts an article into the new grid layout regardless of the 0% production + * rollout. + */ +const enableLabsImmersiveGridTest = (article: Article): Article => ({ + ...article, + frontendData: { + ...article.frontendData, + config: { + ...article.frontendData.config, + serverSideABTests: { + ...article.frontendData.config.serverSideABTests, + [LABS_IMMERSIVE_GRID_AB_TEST]: 'enable', + }, + }, + }, +}); + const labsImmersiveArticle = ({ orientation, design, @@ -274,28 +295,19 @@ const labsImmersiveArticle = ({ orientation === 'portrait' ? photoEssayImmersiveLabsPortraitArticle : photoEssayImmersiveLabsArticle; - return { - ...base, - design, - frontendData: { - ...base.frontendData, - config: { - ...base.frontendData.config, - // Opt these stories into the new layout regardless of the - // 0% production rollout, so they keep demonstrating it. - serverSideABTests: { - ...base.frontendData.config.serverSideABTests, - [LABS_IMMERSIVE_GRID_AB_TEST]: 'enable', - }, - }, - }, - }; + return enableLabsImmersiveGridTest({ ...base, design }); }; const immersiveLabsParameters = { ...webParameters, }; +/** Snapshot real Labs immersive articles at mobile in addition to the default width, to catch small-breakpoint-only regressions */ +const immersiveLabsMobileParameters = { + ...immersiveLabsParameters, + chromatic: { viewports: [breakpoints.mobile, breakpoints.wide] }, +}; + export const WebPhotoEssayImmersiveLabsLight: Story = { args: { article: labsImmersiveArticle({ @@ -380,6 +392,36 @@ export const WebFeatureImmersiveLabsPortraitDark: Story = { }, }; +/** + * Real Labs immersive articles tests. + */ +export const WebImmersiveLabsRealParrtjima: Story = { + args: { + article: enableLabsImmersiveGridTest( + enhanceArticleType(LabsImmersiveParrtjimaFixture, 'Web'), + ), + }, + parameters: immersiveLabsMobileParameters, +}; + +export const WebImmersiveLabsRealVictorianWater: Story = { + args: { + article: enableLabsImmersiveGridTest( + enhanceArticleType(LabsImmersiveVictorianWaterFixture, 'Web'), + ), + }, + parameters: immersiveLabsMobileParameters, +}; + +export const WebImmersiveLabsRealGlobalX: Story = { + args: { + article: enableLabsImmersiveGridTest( + enhanceArticleType(LabsImmersiveGlobalXFixture, 'Web'), + ), + }, + parameters: immersiveLabsMobileParameters, +}; + const standardStandardLabsWebFixture: Article = { ...WebStandardStandardNewsLight.args.article, theme: ArticleSpecial.Labs, From 33717496d486286c866a496a9a4e5f6b8dc38730 Mon Sep 17 00:00:00 2001 From: DanielCliftonGuardian <110032454+DanielCliftonGuardian@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:39:19 +0100 Subject: [PATCH 80/80] Fix affiliate disclaimer regression from merge reconciliation --- .../src/layouts/StandardLayoutArticleGrid.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 2ad29f02c13..2a2c70ede75 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -265,8 +265,12 @@ export const StandardLayoutArticleGrid = ({ contentLayout={contentLayoutName} articleArrangement={layoutType} /> - {!!article.affiliateLinksDisclaimer && ( - <AffiliateDisclaimer /> + {article.affiliateLinksDisclaimerRequired && ( + <AffiliateDisclaimer + cssOverrides={css` + margin: ${space[4]}px 0; + `} + /> )} </div> </GridItem>