diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx
index 3b0c141fc86..c06bb27a5e2 100644
--- a/dotcom-rendering/src/components/ArticleHeadline.tsx
+++ b/dotcom-rendering/src/components/ArticleHeadline.tsx
@@ -874,10 +874,15 @@ export const ArticleHeadline = ({
case ArticleDesign.Picture:
return (
{
const count = format.design === ArticleDesign.Comment ? 8 : 4;
- switch (format.theme) {
- case Pillar.Sport:
- return ;
- default:
- return (
-
- );
+ if (
+ format.theme === Pillar.Sport &&
+ format.design !== ArticleDesign.Picture
+ ) {
+ return ;
}
+
+ return (
+
+ );
};
diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx
index e912f480019..5a31ce8480a 100644
--- a/dotcom-rendering/src/components/MainMedia.tsx
+++ b/dotcom-rendering/src/components/MainMedia.tsx
@@ -75,6 +75,13 @@ const chooseWrapper = (format: ArticleFormat) => {
return noGutters;
}
}
+ case ArticleDisplay.Showcase:
+ switch (format.design) {
+ case ArticleDesign.Picture:
+ return '';
+ default:
+ return noGutters;
+ }
default:
return noGutters;
}
diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx
index b6dd7f4a27c..091adfd22be 100644
--- a/dotcom-rendering/src/layouts/DecideLayout.tsx
+++ b/dotcom-rendering/src/layouts/DecideLayout.tsx
@@ -14,7 +14,6 @@ import { ImmersiveLayout } from './ImmersiveLayout';
import { InteractiveLayout } from './InteractiveLayout';
import { LiveLayout } from './LiveLayout';
import { NewsletterSignupLayout } from './NewsletterSignupLayout';
-import { PictureLayout } from './PictureLayout';
import { StandardLayout } from './StandardLayout';
interface BaseProps {
@@ -91,15 +90,6 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => {
serverTime={serverTime}
/>
);
- case ArticleDesign.Picture:
- return (
-
- );
default:
return (
{
serverTime={serverTime}
/>
);
- case ArticleDesign.Picture:
- return (
-
- );
default:
return (
(
-
- {children}
-
-);
-
-const maxWidth = css`
- ${from.desktop} {
- max-width: 620px;
- }
-`;
-
-const stretchLines = css`
- ${until.phablet} {
- margin-left: -20px;
- margin-right: -20px;
- }
- ${until.mobileLandscape} {
- margin-left: -10px;
- margin-right: -10px;
- }
-`;
-
-const mainMediaWrapper = (displayAvatarUrl: boolean) => css`
- position: relative;
- ${until.phablet} {
- margin-left: 20px;
- margin-right: 20px;
- }
- ${until.mobileLandscape} {
- margin-left: 10px;
- margin-right: 10px;
- }
- ${displayAvatarUrl
- ? css`
- margin-top: 8px;
- `
- : ``}
-`;
-
-const avatarHeadlineWrapper = css`
- display: flex;
- flex-direction: column;
- justify-content: space-between;
-`;
-
-// This styling taken from the similar approach in CommentLayout.tsx
-// If in mobile increase the margin top and margin right deficit
-const avatarPositionStyles = css`
- display: flex;
- justify-content: flex-end;
- position: relative;
- margin-bottom: -29px;
- pointer-events: none;
- ${from.desktop} {
- margin-top: -50px;
- }
- ${until.tablet} {
- overflow: hidden;
- }
-
- /* Why target img element?
-
- Because only in this context, where we have overflow: hidden
- and the margin-bottom and margin-top of avatarPositionStyles
- do we also want to apply our margin-right. These styles
- are tightly coupled in this context, and so it does not
- make sense to move them to the avatar component.
-
- It's imperfect from the perspective of DCR, the alternative is to bust
- the combined elements into a separate component (with the
- relevant stories) and couple them that way, which might be what
- you want to do if you find yourself adding more styles
- to this section. For now, this works without making me 🤢.
- */
-
- ${from.mobile} {
- img {
- margin-right: -1.85rem;
- }
- }
- ${from.mobileLandscape} {
- img {
- margin-right: -1.25rem;
- }
- }
-`;
-
-const LeftColLines = (displayAvatarUrl: boolean) => css`
- margin-bottom: 4px;
- ${displayAvatarUrl
- ? css`
- margin-top: -29px;
- `
- : ''}
-`;
-
-interface CommonProps {
- article: ArticleDeprecated;
- format: ArticleFormat;
- renderingTarget: RenderingTarget;
- serverTime?: number;
-}
-
-interface WebProps extends CommonProps {
- NAV: NavType;
- renderingTarget: 'Web';
-}
-
-interface AppsProps extends CommonProps {
- renderingTarget: 'Apps';
-}
-
-export const PictureLayout = (props: WebProps | AppsProps) => {
- const { article, format, renderingTarget, serverTime } = props;
-
- const {
- config: { isPaidContent, host, hasSurveyAd },
- } = article;
-
- const isWeb = renderingTarget === 'Web';
- const isApps = renderingTarget === 'Apps';
-
- // 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 { branding } = article.commercialProperties[article.editionId];
-
- const contributionsServiceUrl = getContributionsServiceUrl(article);
-
- const renderAds = canRenderAds(article);
-
- const isWorldCup2026 = article.tags.some((tag) => tag.id === worldCupTagId);
-
- const avatarUrl = getSoleContributor(
- article.tags,
- article.byline,
- )?.bylineLargeImageUrl;
-
- const displayAvatarUrl = avatarUrl ? true : false;
-
- return (
- <>
- {isWeb && (
-
- {renderAds && (
-
-
-
- )}
- tag.id)}
- sectionId={article.config.section}
- contentType={article.contentType}
- />
-
- )}
-
- {isWeb && renderAds && hasSurveyAd && (
-
- )}
-
-
- {isApps && renderAds && (
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
- {displayAvatarUrl ? (
-
-
-
-
-
- {!!avatarUrl && (
-
-
-
- )}
-
-
-
-
- ) : (
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {isApps ? (
- <>
-
-
-
-
-
-
- >
- ) : (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- {isWeb && renderAds && (
-
- )}
-
- {article.storyPackage && (
-
- )}
-
- {isWeb && (
-
-
-
- )}
- {showComments && (
-
- )}
-
- {!isPaidContent && (
-
- )}
-
- {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 8d24f6d3b8f..6125d6f2e05 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 { ContributorAvatar } from '../components/ContributorAvatar';
import { CricketMatchHeaderWrapper } from '../components/CricketMatchHeaderWrapper.island';
import { DecideLines } from '../components/DecideLines';
import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland';
@@ -52,6 +53,7 @@ import {
type ArticleFormat,
ArticleSpecial,
} from '../lib/articleFormat';
+import { getSoleContributor } from '../lib/byline';
import { canRenderAds } from '../lib/canRenderAds';
import { getContributionsServiceUrl } from '../lib/contributions';
import { decideStoryPackageTrails } from '../lib/decideTrail';
@@ -81,6 +83,54 @@ const stretchLines = css`
}
`;
+const avatarHeadlineWrapper = css`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+`;
+
+// This styling taken from the similar approach in CommentLayout.tsx
+// If in mobile increase the margin top and margin right deficit
+const avatarPositionStyles = css`
+ display: flex;
+ justify-content: flex-end;
+ position: relative;
+ margin-bottom: -29px;
+ pointer-events: none;
+ ${from.desktop} {
+ margin-top: -50px;
+ }
+ ${until.tablet} {
+ overflow: hidden;
+ }
+
+ /* Why target img element?
+
+ Because only in this context, where we have overflow: hidden
+ and the margin-bottom and margin-top of avatarPositionStyles
+ do we also want to apply our margin-right. These styles
+ are tightly coupled in this context, and so it does not
+ make sense to move them to the avatar component.
+
+ It's imperfect from the perspective of DCR, the alternative is to bust
+ the combined elements into a separate component (with the
+ relevant stories) and couple them that way, which might be what
+ you want to do if you find yourself adding more styles
+ to this section. For now, this works without making me 🤢.
+ */
+
+ ${from.mobile} {
+ img {
+ margin-right: -1.85rem;
+ }
+ }
+ ${from.mobileLandscape} {
+ img {
+ margin-right: -1.25rem;
+ }
+ }
+`;
+
interface GridItemProps {
area: Area;
layoutType: LayoutType;
@@ -168,6 +218,7 @@ export const StandardLayout = (props: WebProps | AppProps) => {
const isVideo = format.design === ArticleDesign.Video;
const isShowcase = format.display === ArticleDisplay.Showcase;
+ const isPicture = format.design === ArticleDesign.Picture;
const showComments = article.isCommentable && !isPaidContent;
@@ -183,9 +234,18 @@ export const StandardLayout = (props: WebProps | AppProps) => {
const layoutType: LayoutType = isMedia
? 'media'
- : isShowcase
- ? 'showcase'
- : 'standard';
+ : isPicture
+ ? 'picture'
+ : isShowcase
+ ? 'showcase'
+ : 'standard';
+
+ const avatarUrl = getSoleContributor(
+ article.tags,
+ article.byline,
+ )?.bylineLargeImageUrl;
+
+ const displayAvatarUrl = avatarUrl ? true : false;
return (
<>
@@ -282,7 +342,17 @@ export const StandardLayout = (props: WebProps | AppProps) => {
`,
]}
>
-
+
{
area="title"
layoutType={layoutType}
element="aside"
+ css={css`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ `}
>
{
guardianBaseURL={article.guardianBaseURL}
isMatch={!!footballMatchUrl}
/>
+ {displayAvatarUrl && (
+
+
+
+ )}
-
-
-
+ {displayAvatarUrl ? (
+
+
+
+
+
+ {!!avatarUrl && (
+
+
+
+ )}
+
+
+
+
+ ) : (
+
+
+
+ )}
{
format.theme === ArticleSpecial.Labs &&
format.design !== ArticleDesign.Video ? (
- ) : (
+ ) : !displayAvatarUrl ? (
- )}
+ ) : null}
{isApps ? (
<>
@@ -595,30 +721,32 @@ export const StandardLayout = (props: WebProps | AppProps) => {
}
`}
>
-
-
-
-
-
+ {!isPicture && (
+
+
+
+
+
+ )}
diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts
index 2cf5fb6843f..00e4ec032cc 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' | 'picture';
export type Area =
| 'title'
@@ -149,10 +149,48 @@ const mediaCss: LayoutCssMap = {
},
};
+const pictureCss: LayoutCssMap = {
+ title: {
+ mobile: 'grid-row: 1;',
+ tablet: 'grid-row: 1;',
+ leftCol: `grid-row: 1; ${grid.column.left}`,
+ },
+ headline: {
+ mobile: 'grid-row: 2;',
+ tablet: 'grid-row: 2;',
+ desktop: `grid-row: 2; ${grid.between('centre-column-start', 'right-column-end')};`,
+ leftCol: 'grid-row: 1;',
+ },
+ standfirst: {
+ mobile: 'grid-row: 4;',
+ tablet: 'grid-row: 4;',
+ desktop: 'grid-row: 4;',
+ leftCol: 'grid-row: 2;',
+ },
+ media: {
+ mobile: 'grid-row: 5;',
+ tablet: 'grid-row: 5;',
+ desktop: `grid-row: 5; ${grid.between('centre-column-start', 'right-column-end')};`,
+ leftCol: `grid-row: 3; ${grid.between('centre-column-start', 'right-column-end')};`,
+ },
+ meta: {
+ mobile: 'grid-row: 3;',
+ tablet: 'grid-row: 3;',
+ desktop: `grid-row: 3; ${grid.between('centre-column-start', 'right-column-end')};`,
+ leftCol: `grid-row: 3 / span 2; ${grid.column.left};`,
+ },
+ body: {
+ tablet: 'grid-row: 6;',
+ desktop: `grid-row: 6; ${grid.between('centre-column-start', 'right-column-end')};`,
+ leftCol: `grid-row: 5; ${grid.between('centre-column-start', 'right-column-end')};`,
+ },
+};
+
const layoutCssMaps: Record = {
standard: standardCss,
showcase: showcaseCss,
media: mediaCss,
+ picture: pictureCss,
};
/**