diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx
index 8d24f6d3b8f..e20954cca52 100644
--- a/dotcom-rendering/src/layouts/StandardLayout.tsx
+++ b/dotcom-rendering/src/layouts/StandardLayout.tsx
@@ -1,51 +1,24 @@
-import { css } from '@emotion/react';
-import { log } from '@guardian/libs';
-import {
- from,
- palette as sourcePalette,
- space,
- until,
-} from '@guardian/source/foundations';
-import { Hide } from '@guardian/source/react-components';
-import { StraightLines } from '@guardian/source-development-kitchen/react-components';
+import { palette as sourcePalette } from '@guardian/source/foundations';
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';
-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 { 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';
import { FootballMatchHeaderWrapper } from '../components/FootballMatchHeaderWrapper.island';
-import { FootballMatchInfoWrapper } from '../components/FootballMatchInfoWrapper.island';
import { Footer } from '../components/Footer';
-import { GuardianLabsLines } from '../components/GuardianLabsLines';
import { HeaderAdSlot } from '../components/HeaderAdSlot';
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 { MatchHeaderFallback } from '../components/MatchHeaderFallback';
import { MostViewedFooterData } from '../components/MostViewedFooterData.island';
import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout';
-import { MostViewedRightWithAd } from '../components/MostViewedRightWithAd.island';
import { OnwardsUpper } from '../components/OnwardsUpper.island';
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 { grid } from '../grid';
import {
ArticleDesign,
ArticleDisplay,
@@ -55,55 +28,14 @@ import {
import { canRenderAds } from '../lib/canRenderAds';
import { getContributionsServiceUrl } from '../lib/contributions';
import { decideStoryPackageTrails } from '../lib/decideTrail';
-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';
import type { ArticleDeprecated } from '../types/article';
import type { RenderingTarget } from '../types/renderingTarget';
-import {
- type Area,
- gridItemCss,
- type LayoutType,
-} from './lib/articleArrangements';
import { BannerWrapper, Stuck } from './lib/stickiness';
-
-const stretchLines = css`
- ${until.phablet} {
- margin-left: -20px;
- margin-right: -20px;
- }
- ${until.mobileLandscape} {
- margin-left: -10px;
- margin-right: -10px;
- }
-`;
-
-interface GridItemProps {
- area: Area;
- layoutType: LayoutType;
- element?: 'div' | 'aside';
- className?: string;
- children: React.ReactNode;
-}
-
-const GridItem = ({
- area,
- layoutType,
- element: Element = 'div',
- className,
- children,
-}: GridItemProps) => (
-
- {children}
-
-);
+import { StandardLayoutArticleGrid } from './StandardLayoutArticleGrid';
interface Props {
article: ArticleDeprecated;
@@ -112,12 +44,12 @@ interface Props {
serverTime?: number;
}
-interface WebProps extends Props {
+export interface WebProps extends Props {
NAV: NavType;
renderingTarget: 'Web';
}
-interface AppProps extends Props {
+export interface AppProps extends Props {
renderingTarget: 'Apps';
}
@@ -131,11 +63,6 @@ export const StandardLayout = (props: WebProps | AppProps) => {
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.
@@ -145,11 +72,6 @@ export const StandardLayout = (props: WebProps | AppProps) => {
? article.matchUrl
: undefined;
- const footballMatchStatsUrl =
- article.matchType === 'FootballMatchType'
- ? article.matchStatsUrl
- : undefined;
-
const isFootballMatchReport =
format.design === ArticleDesign.MatchReport && !!footballMatchUrl;
@@ -161,18 +83,8 @@ export const StandardLayout = (props: WebProps | AppProps) => {
const isCricketMatchReport =
format.design === ArticleDesign.MatchReport && !!cricketMatchUrl;
- const isMedia =
- format.design === ArticleDesign.Video ||
- format.design === ArticleDesign.Audio;
-
- const isVideo = format.design === ArticleDesign.Video;
-
- const isShowcase = format.display === ArticleDisplay.Showcase;
-
const showComments = article.isCommentable && !isPaidContent;
- const { branding } = article.commercialProperties[article.editionId];
-
const contributionsServiceUrl = getContributionsServiceUrl(article);
const isLabs = format.theme === ArticleSpecial.Labs;
@@ -181,12 +93,6 @@ export const StandardLayout = (props: WebProps | AppProps) => {
const renderAds = canRenderAds(article);
- const layoutType: LayoutType = isMedia
- ? 'media'
- : isShowcase
- ? 'showcase'
- : 'standard';
-
return (
<>
{isWeb && (
@@ -261,366 +167,13 @@ 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 */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {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 && (
-
-
-
-
-
- )}
-
- )}
-
-
-
-
- {isApps && (
-
-
-
- )}
-
- {showBodyEndSlot && (
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
+
{isWeb && renderAds && !isLabs && (
{
- if (isMatchReport && !!footballMatchStatsUrl) {
- const parsedUrl = safeParseURL(footballMatchStatsUrl);
- if (!parsedUrl.ok) {
- log(
- 'dotcom',
- new Error(
- `Failed to parse match stats URL: ${footballMatchStatsUrl}`,
- ),
- );
-
- return null;
- }
- return (
-
-
-
- );
- }
-
- return null;
-};
diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx
new file mode 100644
index 00000000000..53c6d6d0371
--- /dev/null
+++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx
@@ -0,0 +1,463 @@
+import { css } from '@emotion/react';
+import { log } from '@guardian/libs';
+import { from, space, until } from '@guardian/source/foundations';
+import { Hide } from '@guardian/source/react-components';
+import { StraightLines } from '@guardian/source-development-kitchen/react-components';
+import { AffiliateDisclaimer } from '../components/AffiliateDisclaimer';
+import { AppsEpic } from '../components/AppsEpic.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 { DecideLines } from '../components/DecideLines';
+import { FootballMatchInfoWrapper } from '../components/FootballMatchInfoWrapper.island';
+import { GuardianLabsLines } from '../components/GuardianLabsLines';
+import { Island } from '../components/Island';
+import { ListenToArticle } from '../components/ListenToArticle.island';
+import { MainMedia } from '../components/MainMedia';
+import { MostViewedRightWithAd } from '../components/MostViewedRightWithAd.island';
+import { SlotBodyEnd } from '../components/SlotBodyEnd.island';
+import { Standfirst } from '../components/Standfirst';
+import { SubMeta } from '../components/SubMeta';
+import { grid } from '../grid';
+import {
+ ArticleDesign,
+ ArticleDisplay,
+ type ArticleFormat,
+ ArticleSpecial,
+} from '../lib/articleFormat';
+import { getContributionsServiceUrl } from '../lib/contributions';
+import { safeParseURL } from '../lib/parse';
+import { parse } from '../lib/slot-machine-flags';
+import { palette as themePalette } from '../palette';
+import type { ArticleDeprecated } from '../types/article';
+import type { RenderingTarget } from '../types/renderingTarget';
+import {
+ type Area,
+ gridItemCss,
+ type LayoutType,
+} from './lib/articleArrangements';
+
+const stretchLines = css`
+ ${until.phablet} {
+ margin-left: -20px;
+ margin-right: -20px;
+ }
+ ${until.mobileLandscape} {
+ margin-left: -10px;
+ margin-right: -10px;
+ }
+`;
+
+interface GridItemProps {
+ area: Area;
+ layoutType: LayoutType;
+ element?: 'div' | 'aside';
+ className?: string;
+ children: React.ReactNode;
+}
+
+const GridItem = ({
+ area,
+ layoutType,
+ element: Element = 'div',
+ className,
+ children,
+}: GridItemProps) => (
+
+ {children}
+
+);
+
+export const StandardLayoutArticleGrid = ({
+ article,
+ format,
+ renderingTarget,
+}: {
+ article: ArticleDeprecated;
+ format: ArticleFormat;
+ renderingTarget: RenderingTarget;
+}) => {
+ const {
+ config: { host },
+ } = article;
+ const isWeb = renderingTarget === 'Web';
+ const isApps = renderingTarget === 'Apps';
+ const renderAds = isWeb && !article.shouldHideAds;
+
+ const contributionsServiceUrl = getContributionsServiceUrl(article);
+
+ const showBodyEndSlot =
+ isWeb &&
+ (parse(article.slotMachineFlags ?? '').showBodyEnd ||
+ article.config.switches.slotBodyEnd);
+
+ const { branding } = article.commercialProperties[article.editionId];
+
+ const footballMatchStatsUrl =
+ article.matchType === 'FootballMatchType'
+ ? article.matchStatsUrl
+ : undefined;
+
+ const isLabs = format.theme === ArticleSpecial.Labs;
+ const isMedia =
+ format.design === ArticleDesign.Video ||
+ format.design === ArticleDesign.Audio;
+ const isShowcase = format.display === ArticleDisplay.Showcase;
+
+ const isVideo = format.design === ArticleDesign.Video;
+
+ const footballMatchUrl =
+ article.matchType === 'FootballMatchType'
+ ? article.matchUrl
+ : undefined;
+
+ const isFootballMatchReport =
+ format.design === ArticleDesign.MatchReport && !!footballMatchUrl;
+
+ const layoutType: LayoutType = isMedia
+ ? 'media'
+ : isShowcase
+ ? 'showcase'
+ : 'standard';
+
+ return (
+
+ {/* GridItem order matters — mobile layout relies on DOM order for grid placement.
+ See furnitureArrangements.ts if reordering. */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {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 && (
+
+
+
+
+
+ )}
+
+ )}
+
+
+
+
+ {isApps && (
+
+
+
+ )}
+
+ {showBodyEndSlot && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const MatchInfoContainer = ({
+ isMatchReport,
+ footballMatchStatsUrl,
+}: {
+ isMatchReport: boolean;
+ footballMatchStatsUrl: string | undefined;
+}) => {
+ if (isMatchReport && !!footballMatchStatsUrl) {
+ const parsedUrl = safeParseURL(footballMatchStatsUrl);
+ if (!parsedUrl.ok) {
+ log(
+ 'dotcom',
+ new Error(
+ `Failed to parse match stats URL: ${footballMatchStatsUrl}`,
+ ),
+ );
+
+ return null;
+ }
+ return (
+
+
+
+ );
+ }
+
+ return null;
+};