Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
71c37cf
Merge branch 'latest' of ssh://github.com/bbc/simorgh into latest
louisearchibald Jun 25, 2026
115ee9a
Merge branch 'latest' of ssh://github.com/bbc/simorgh into latest
louisearchibald Jul 2, 2026
4471b23
Merge branch 'latest' of ssh://github.com/bbc/simorgh into latest
louisearchibald Jul 3, 2026
2561fc5
Merge branch 'latest' of ssh://github.com/bbc/simorgh into latest
louisearchibald Jul 10, 2026
7f33e83
Merge branch 'latest' of ssh://github.com/bbc/simorgh into latest
louisearchibald Jul 14, 2026
fe8ac68
Merge branch 'latest' of ssh://github.com/bbc/simorgh into latest
louisearchibald Jul 14, 2026
2c42c87
Merge branch 'latest' of ssh://github.com/bbc/simorgh into latest
louisearchibald Jul 16, 2026
5ad5e36
Merge branch 'latest' of ssh://github.com/bbc/simorgh into latest
louisearchibald Jul 16, 2026
89f9d4e
Merge branch 'latest' of ssh://github.com/bbc/simorgh into latest
louisearchibald Jul 17, 2026
e7d96e5
Merge branch 'latest' of ssh://github.com/bbc/simorgh into latest
louisearchibald Jul 21, 2026
5b39a68
Merge branch 'latest' of ssh://github.com/bbc/simorgh into latest
louisearchibald Jul 24, 2026
1f68fa1
create onward journey component key type
louisearchibald Jul 24, 2026
8c348f7
set up variants with decided oj component ordering
louisearchibald Jul 24, 2026
4a92418
Merge branch 'latest' into WS-2995-support-variants-needed-for-search…
louisearchibald Jul 24, 2026
bef8dae
comments
louisearchibald Jul 24, 2026
fb5b4c3
Merge branch 'WS-2995-support-variants-needed-for-search-experiment' …
louisearchibald Jul 24, 2026
8ca74b9
make variants params to help with testing
louisearchibald Jul 24, 2026
addd268
return variant component order if user is on mobile
louisearchibald Jul 24, 2026
c6603e1
Merge branch 'latest' into WS-2995-support-variants-needed-for-search…
louisearchibald Jul 28, 2026
c08235b
adds oj reordering logic
holchris Jul 28, 2026
efa2edf
adds mobile oj container css
holchris Jul 28, 2026
dabfefd
Merge branch 'WS-2995-support-variants-needed-for-search-experiment' …
louisearchibald Jul 28, 2026
d676ea0
add switch case for showing mid article ojs for each variant
louisearchibald Jul 28, 2026
cd9dfef
add missing brackets to enable reordering to work
louisearchibald Jul 28, 2026
9ebb625
amend switch for variants 1 to 3 as they all return the same component
louisearchibald Jul 28, 2026
29ba715
remove console log
louisearchibald Jul 28, 2026
ea7022f
update mobile OJ order to include searchVariant
louisearchibald Jul 28, 2026
99429fe
small change to get switch case working for midarticle OJs
louisearchibald Jul 28, 2026
f5874fc
fixes midarticle OJ changing at desktop
louisearchibald Jul 29, 2026
de639fa
Merge branch 'latest' into WS-2995-support-variants-needed-for-search…
pvaliani Jul 29, 2026
324577d
hide related topics when topic discovery is in midarticle position
louisearchibald Jul 29, 2026
1702064
Merge branch 'WS-2995-support-variants-needed-for-search-experiment' …
louisearchibald Jul 29, 2026
159eba1
update variant names
holchris Jul 29, 2026
18c506f
update variant names in article page
holchris Jul 29, 2026
e8bfe8e
update search variant params
holchris Jul 29, 2026
11c01d8
Merge branch 'WS-2995-support-variants-needed-for-search-experiment' …
louisearchibald Jul 29, 2026
bdff19c
Merge branch 'latest' into WS-2995-support-variants-needed-for-search…
louisearchibald Jul 29, 2026
1c19189
adds marginBottom to topicDiscovery when in the midarticle position
louisearchibald Jul 29, 2026
0afc61c
Merge branch 'WS-2995-support-variants-needed-for-search-experiment' …
louisearchibald Jul 29, 2026
21ede71
Merge branch 'latest' into WS-2995-support-variants-needed-for-search…
louisearchibald Jul 29, 2026
c266855
Merge branch 'WS-2995-support-variants-needed-for-search-experiment' …
louisearchibald Jul 29, 2026
d663bdd
adds 40px marginBottom for location oj also
louisearchibald Jul 29, 2026
57e4441
remove topicDiscovery from variant 5
louisearchibald Jul 29, 2026
29d0668
remove comment
louisearchibald Jul 29, 2026
173fb7e
reuse variant config for debug variant validation
louisearchibald Jul 29, 2026
c2a1012
remove redundant styling
louisearchibald Jul 29, 2026
3a7b4b1
revert accidental change
louisearchibald Jul 29, 2026
ff416d0
Merge branch 'latest' into WS-2995-support-variants-needed-for-search…
louisearchibald Jul 30, 2026
d0d0eb7
Merge branch 'latest' into WS-2995-support-variants-needed-for-search…
Nabeel1276 Jul 30, 2026
e576f0c
adds tests for search referrer
holchris Jul 30, 2026
8bc155a
Merge branch 'WS-2995-support-variants-needed-for-search-experiment' …
louisearchibald Jul 30, 2026
501f64b
remove commented code and use media query for the test instead of har…
louisearchibald Jul 30, 2026
53eaa17
use searchVariant type
louisearchibald Jul 30, 2026
f7f3ad6
refactor switch case for midarticleOJ
louisearchibald Jul 30, 2026
5de937c
test midarticle OJ to variant mapping
louisearchibald Jul 30, 2026
0aaea8d
Merge branch 'latest' into WS-2995-support-variants-needed-for-search…
louisearchibald Jul 30, 2026
0e45de8
Merge branch 'WS-2995-support-variants-needed-for-search-experiment' …
louisearchibald Jul 30, 2026
c7814e9
refactor condition and remove accidental brackets
louisearchibald Jul 30, 2026
1cd3e69
Merge branch 'latest' into WS-2995-support-variants-needed-for-search…
pvaliani Jul 30, 2026
ebcac79
fallback to most read midarticle oj when location based oj is not pre…
louisearchibald Jul 30, 2026
94b60dd
Merge branch 'WS-2995-support-variants-needed-for-search-experiment' …
louisearchibald Jul 30, 2026
62a816c
update types to be videoOJ to cater for video curations also
louisearchibald Jul 30, 2026
42abb84
add video oj helper to cater for both pvCarousel and video curations
louisearchibald Jul 30, 2026
1bed8dc
rendered mobile variant tests wip copilot
holchris Jul 30, 2026
05de34c
fallback for related content
louisearchibald Jul 30, 2026
2980b75
Merge branch 'WS-2995-support-variants-needed-for-search-experiment' …
louisearchibald Jul 30, 2026
44c9775
temporarily skip wip tests just added
louisearchibald Jul 30, 2026
ca4e4fc
update OJ name
louisearchibald Jul 31, 2026
3ce4988
test change
louisearchibald Jul 31, 2026
02c8712
update describe
louisearchibald Jul 31, 2026
fbb791a
Merge branch 'latest' into WS-2995-support-variants-needed-for-search…
louisearchibald Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/app/pages/ArticlePage/ArticlePage.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,14 @@ export default {
padding: 0,
},
}),
mobileOJContainer: ({ mq }: Theme) =>
css({
[mq.GROUP_4_MIN_WIDTH]: {
display: 'none',
},
}),
midArticleOJ: () =>
css({
marginBottom: `${pixelsToRem(40)}rem`,
}),
};
228 changes: 208 additions & 20 deletions src/app/pages/ArticlePage/ArticlePage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { use, useState, useCallback } from 'react';
import { Fragment, ReactNode, useState, useCallback, use } from 'react';
import { useTheme } from '@emotion/react';
import useToggle from '#hooks/useToggle';
import useMediaQuery from '#hooks/useMediaQuery';
Expand Down Expand Up @@ -59,6 +59,7 @@ import ContinueReadingButton, {
ContinueReadingButtonProps,
} from '#app/components/ContinueReadingButton';
import SaveArticleButton from '#app/components/SaveArticleButton';
import FeaturesAnalysis from '#containers/CpsFeaturesAnalysis';
import AccountPromotionalBannerExperiment from '#app/components/Account/AccountPromotionalBannerExperiment';
import ElectionBanner from './ElectionBanner';
import ArticleMessageBanner from './ArticleMessageBanner';
Expand All @@ -84,6 +85,9 @@ import RelatedContentSection from '../../components/RelatedContentSection';
import TopicDiscovery from '../../components/TopicDiscovery';
import Disclaimer from '../../components/Disclaimer';
import SecondaryColumn from './SecondaryColumn';
import useMobileOJComponentOrder, {
useDebugVariant,
} from './useMobileOJComponentOrder';
import styles from './ArticlePage.styles';
import { ComponentToRenderProps, TimeStampProps } from './types';
import ArticleHeadline from './ArticleHeadline';
Expand All @@ -92,6 +96,12 @@ import {
isPortraitVideoUnderHeadline,
} from '../../components/MediaLoader/utils/isPortraitVideo';
import LocationBasedTopicOJ from '../../components/LocationBasedTopicOJ';
import {
OJComponentKey,
SEARCH_MID_ARTICLE_COMPONENT,
SearchVariant,
} from './searchReferrerComponentOrder';
import TopStoriesSection from './PagePromoSections/TopStoriesSection';

const getImageComponent =
(preloadLeadImageToggle: boolean) => (props: ComponentToRenderProps) => (
Expand Down Expand Up @@ -161,6 +171,7 @@ const getWsojComponent = ({
}) => (
<Recommendations data={data} {...(experimentProps && { experimentProps })} />
);

const DisclaimerWithPaddingOverride = (props: ComponentToRenderProps) => (
<Disclaimer {...props} increasePaddingOnDesktop={false} />
);
Expand Down Expand Up @@ -292,6 +303,60 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
?.split(':')
?.includes('topcat');

const showCountryCuration = Boolean(
!isAmp &&
!isLite &&
!isApp &&
countryCurationEnabled &&
pageData?.countryCuration?.summaries?.length,
);

const hasRelatedContent = blocks.some(
block => block.type === 'relatedContent',
);

const getSearchMidArticleOJ = ({
data,
experimentProps,
searchVariant,
}: {
data: Recommendation[];
experimentProps?: ComponentExperimentProps | null;
searchVariant: SearchVariant | null;
}) => {
const midarticleOJ = searchVariant
? SEARCH_MID_ARTICLE_COMPONENT[searchVariant]
: null;
switch (midarticleOJ) {
case 'mostRead':
return <Recommendations data={data} />;

case 'relatedContent':
return hasRelatedContent ? (
<div css={styles.midArticleOJ}>
<RelatedContentSection content={blocks} />
</div>
) : (
<Recommendations data={data} />
);

case 'topicDiscovery':

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The footer versions use the existing checks for whether Topic Discovery and Location OJ (like showTopicDiscovery and showCountryCuration) are enabled, but the mid-article versions bypass those checks I think. This might display a component that is disabled for a service, or leave the mid-article position empty when it is unavailable. If the response is empty it might handle gracefully but thought it might be worth mentioning

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The country curation not being on a service could be a problem here for variant 6. I will ask Gavin.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answer is to fallback to most read in the mid-article spot (same as control) when the variant defined component is unavailable. This will happen with Location OJ and Related Content.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take it Topic Discovery is guaranteed to be available for every article in scope, as this mid-article path does not use showTopicDiscovery? If it is guaranteed then this should be fine otherwise it may need the same fallback so was jw

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not encountered any articles where it isn't present, however that's not to say that it couldn't happen? The drawback to having the same fallback is the ordering of that particular variant as you would then have mostRead content showing in the Recommendations component in the mid-article slot and then directly followed by mostRead as the first OJ underneath. I can query that tomorrow with product.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think topic discovery is on every service. Let's try not to let it be toggled off for the duration of the experiment! (not sure why it would be. Getting the dedupe fix in as well!)

return <TopicDiscovery topics={topics} css={styles.midArticleOJ} />;

case 'locationBasedOJ':
return showCountryCuration ? (
<div css={styles.midArticleOJ}>
<LocationBasedTopicOJ pageData={pageData} />
</div>
) : (
<Recommendations data={data} />
Comment thread
louisearchibald marked this conversation as resolved.
);

default:
return getWsojComponent({ data, experimentProps });
}
};

const showPortraitVideoCarousel = Boolean(
pageData?.portraitVideoItems?.portraitVideo?.blocks?.length &&
articlePortraitVideoEnabled,
Expand Down Expand Up @@ -350,6 +415,9 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
promoImageRawBlock?.model as { locator?: string } | undefined
)?.locator;

const searchVariant = useDebugVariant();
const mobileOJOrder = useMobileOJComponentOrder(searchVariant);

const componentsToRender = {
visuallyHiddenHeadline,
headline: getHeadlineComponent,
Expand All @@ -376,10 +444,11 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
group: gist,
links: ArticleLinksBlock,
mpu: getMpuComponent(allowAdvertising),
// renders wsoj if user is on desktop, otherwise renders a chosen OJ based on search referrer experiment on mobile
wsoj: ({ data }: { data: Recommendation[] }) =>
getWsojComponent({
data,
}),
!isDesktopViewport
? getSearchMidArticleOJ({ data, searchVariant })
: getWsojComponent({ data }),
disclaimer: DisclaimerWithPaddingOverride,
podcastPromo: getPodcastPromoComponent(podcastPromoEnabled),
...(showContinueReadingButton && {
Expand All @@ -404,8 +473,16 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {

const showTopicDiscovery = topicDiscoveryEnabled && !isAmp && !isLite;

// Topic Discovery shows in the midarticle position for one variant
// We want to hide RelatedTopics when this happens
const topicDiscoveryInMidArticlePosition =
!isDesktopViewport && searchVariant === 'variant_5_recommended_mid';

const showRelatedTopicsComponent = Boolean(
showRelatedTopics && topics.length > 0 && !showTopicDiscovery,
showRelatedTopics &&
topics.length > 0 &&
!showTopicDiscovery &&
!topicDiscoveryInMidArticlePosition,
);

const showMediaCuration = Boolean(
Expand All @@ -417,13 +494,112 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
articleVideoCurationEnabled,
);

const showCountryCuration = Boolean(
!isAmp &&
!isLite &&
!isApp &&
countryCurationEnabled &&
pageData?.countryCuration?.summaries?.length,
);
const topStoriesContent = pageData?.secondaryColumn?.topStories;
const featuresContent = pageData?.secondaryColumn?.features;

const getTopicDiscoverySlot = () => {
if (showTopicDiscovery) {
return (
<TopicDiscovery
css={[
...(showContinueReadingButton
? [!showAllContent && styles.hideTopicDiscovery]
: []),
]}
topics={topics}
/>
);
}
if (showRelatedTopicsComponent) {
return (
<RelatedTopics
css={[
styles.relatedTopics,
...(showContinueReadingButton
? [!showAllContent && styles.hideRelatedTopics]
: []),
]}
topics={topics}
mobileDivider={false}
/>
);
}
return null;
};

const topicDiscoverySlot = getTopicDiscoverySlot();

const getVideoOJComponent = (): ReactNode => {
if (showPortraitVideoCarousel) {
return (
<PortraitVideoCarousel
{...portraitVideoCarouselProps}
css={styles.portraitVideoCarousel}
/>
);
}
if (showMediaCuration) {
return (
<div css={styles.mediaCurationRow}>
<div data-testid="media-curation">
<Curation
visualStyle={VISUAL_STYLE.FEED}
visualProminence={VISUAL_PROMINENCE.NORMAL}
summaries={mediaCurationContent?.summaries}
title={mediaCurationContent?.title}
position={mediaCurationContent?.position || 0}
curationId={mediaCurationContent?.curationId}
curationLength={1}
link={mediaCurationContent?.link}
curationContentType="video"
pageType={pageType}
/>
</div>
</div>
);
}
return null;
};

const mobileOJComponents: Record<OJComponentKey, ReactNode> = {
mostRead:
!isApp && !isPGL ? (
<MostRead
css={styles.mostReadSection}
data={mostReadInitialData}
columnLayout="twoColumn"
size="default"
headingBackgroundColour={GREY_2}
mobileDivider={showRelatedTopicsComponent}
/>
) : null,
topicDiscovery: topicDiscoverySlot,
relatedContent: <RelatedContentSection content={blocks} />,
videoOJ: getVideoOJComponent(),
topStories:
!isApp && !isPGL && topStoriesContent ? (
<div
css={styles.topStoriesSection}
data-testid="top-stories"
data-experiment-position="secondaryColumn"
>
<TopStoriesSection content={topStoriesContent} />
</div>
) : null,
featuredArticles:
!isApp && !isPGL && featuresContent ? (
<div css={styles.featuresSection} data-testid="features">
<FeaturesAnalysis
content={featuresContent}
parentColumns={{}}
sectionLabelBackground={GREY_2}
/>
</div>
) : null,
locationBasedOJ: showCountryCuration ? (
<LocationBasedTopicOJ pageData={pageData} />
) : null,
};

const shouldApplyCollapsedArticleSpacing =
showContinueReadingButton && !showAllContent;
Expand Down Expand Up @@ -500,7 +676,7 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
<OptimizelyPageMetrics trackPageComplete />
</main>
<OptimizelyPageMetrics trackPageDepth />
{showTopicDiscovery && (
{!mobileOJOrder && showTopicDiscovery && (
<TopicDiscovery
css={[
...(showContinueReadingButton
Expand All @@ -510,7 +686,7 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
topics={topics}
/>
)}
{showRelatedTopicsComponent && (
{!mobileOJOrder && showRelatedTopicsComponent && (
<RelatedTopics
css={[
styles.relatedTopics,
Expand All @@ -522,15 +698,17 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
mobileDivider={false}
/>
)}
{showCountryCuration && <LocationBasedTopicOJ pageData={pageData} />}
{showPortraitVideoCarousel && (
{!mobileOJOrder && showCountryCuration && (
<LocationBasedTopicOJ pageData={pageData} />
)}
{!mobileOJOrder && showPortraitVideoCarousel && (
<PortraitVideoCarousel
{...portraitVideoCarouselProps}
css={styles.portraitVideoCarousel}
/>
)}
<RelatedContentSection content={blocks} />
{showMediaCuration && (
{!mobileOJOrder && <RelatedContentSection content={blocks} />}
{!mobileOJOrder && showMediaCuration && (
<div css={styles.mediaCurationRow}>
<div data-testid="media-curation">
<Curation
Expand All @@ -550,10 +728,20 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
)}
</div>

{!isApp && !isPGL && <SecondaryColumn pageData={pageData} />}
{!isApp && !isPGL && !mobileOJOrder && (
<SecondaryColumn pageData={pageData} />
)}
</div>

{!isApp && !isPGL && (
{mobileOJOrder && (
<div css={styles.mobileOJContainer} data-testid="mobile-oj-container">
{mobileOJOrder.map(key => (
<Fragment key={key}>{mobileOJComponents[key]}</Fragment>
))}
</div>
)}

{!isApp && !isPGL && !mobileOJOrder && (
<MostRead
css={styles.mostReadSection}
data={mostReadInitialData}
Expand Down
Loading
Loading