Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4db20f7
INI: first bit (not working)
eagerterrier Aug 15, 2025
40bb70e
import curation and use useEffect
LilyL0u Aug 15, 2025
e8a6cb0
WIP: move data fetch to server
eagerterrier Aug 18, 2025
f426a5f
prettier
LilyL0u Aug 18, 2025
8ab0b2e
show data in curation grid
LilyL0u Aug 18, 2025
3376aac
do not add personalosed content object if the topic has no summaries
LilyL0u Aug 18, 2025
8c14fe2
clean up of ts and other errors like unused variables
LilyL0u Aug 18, 2025
3c03a7d
name of experiment
LilyL0u Aug 29, 2025
1ab78a6
Merge branch 'latest' into wip-personalised-rail
LilyL0u Aug 29, 2025
75c456b
Merge branch 'latest' into wip-personalised-rail
LilyL0u Aug 29, 2025
92f1ff8
wip optimizely setup
LilyL0u Sep 2, 2025
0433c7f
Merge branch 'latest' of github.com:bbc/simorgh into wip-personalised…
LilyL0u Sep 12, 2025
851448c
Merge branch 'latest' of github.com:bbc/simorgh into wip-personalised…
LilyL0u Sep 12, 2025
db74cc1
Merge branch 'latest' of github.com:bbc/simorgh into wip-personalised…
LilyL0u Sep 12, 2025
d8e5f7b
wip with internal server error
LilyL0u Sep 17, 2025
1d37b8a
tracking
LilyL0u Sep 26, 2025
232f06a
add variation between default and personalised
LilyL0u Sep 26, 2025
c815b24
fix data fetching
LilyL0u Oct 13, 2025
5a91432
change articles to summaries for consistency
LilyL0u Oct 24, 2025
4733f24
Merge branch 'latest' into wip-personalised-rail
LilyL0u Oct 24, 2025
1d45fb3
resolving conflicts was easier when removing and putting back
LilyL0u Oct 24, 2025
0d94379
read time location is no longer on latest
LilyL0u Oct 24, 2025
7a1ee40
used same styles as related content
LilyL0u Oct 24, 2025
5afca4b
move to cocomponents folder
LilyL0u Oct 24, 2025
0c8f538
unit tests
LilyL0u Oct 24, 2025
46717da
log and unused code
LilyL0u Nov 10, 2025
abd250e
update with agreed spike topics and associated ids
pvaliani Nov 25, 2025
e8c5ec9
update uruguay country code
pvaliani Nov 25, 2025
c4c71c9
only run location personalised content code in server when on article…
LilyL0u Nov 25, 2025
076fbd6
if there isnt a secondaryv column, add an empty one to stop failure w…
LilyL0u Nov 25, 2025
00b11c1
remove default topic logic and cleanup
pvaliani Nov 27, 2025
6096ffa
update express server fetch before moving to nextjs - include try cat…
pvaliani Nov 27, 2025
72b8f01
align experiment name to newswb_ws_location_based_topics
pvaliani Nov 27, 2025
c3bf85b
port over express logic to nextapp for personalised topic experience
pvaliani Nov 27, 2025
10ba456
restore express server changes to latest
pvaliani Nov 27, 2025
cee1b8f
fix uncommented article test for when data is present - passing
pvaliani Nov 27, 2025
7db8ecb
add link tracking on subheading for topic
pvaliani Nov 27, 2025
079ab1f
Merge remote-tracking branch 'origin/latest' into wip-personalised-rail
pvaliani Nov 27, 2025
42fd596
Merge branch 'latest' into wip-personalised-rail
pvaliani Nov 27, 2025
8c66db2
Merge branch 'latest' into wip-personalised-rail
pvaliani Nov 27, 2025
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
75 changes: 75 additions & 0 deletions src/app/components/PersonalisedContent/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import { render, screen } from '../react-testing-library-with-providers';
import PersonalisedContent from '.';

const mockPersonalisedContent = [
{
title: 'Personalised Title',
summaries: [
{
type: 'promo',
title: 'Promo Title',
description: 'Promo Description',
link: '/promo-link',
imageUrl: 'promo-image.jpg',
imageAlt: 'Promo Image',
isLive: false,
},
],
id: 'personalised-content',
link: '/personalised-link',
isFirstCuration: true,
topicId: 'topic-1',
},
];

const basePageData = {
secondaryColumn: {
PersonalisedContent: mockPersonalisedContent,
topStories: [],
features: [],
},
};

describe('PersonalisedContent', () => {
it('renders nothing if there is no personalised content data', () => {
render(
<PersonalisedContent
// @ts-expect-error: Test fixture data does not need to match Article type exactly
pageData={{ secondaryColumn: { topStories: [], features: [] } }}
personalisedTopicCurationExperimentVariant="personalised"
/>,
);
expect(screen.queryByRole('region')).not.toBeInTheDocument();
});

it('renders personalised content when variant is "personalised"', () => {
render(
<PersonalisedContent
// @ts-expect-error: Test fixture data does not need to match Article type exactly
pageData={basePageData}
personalisedTopicCurationExperimentVariant="personalised"
/>,
);
expect(screen.getByRole('region')).toHaveAttribute(
'aria-labelledby',
'personalised-content',
);
expect(screen.getByText('Personalised Title')).toBeInTheDocument();
expect(screen.getByText('Promo Title')).toBeInTheDocument();
});

it('renders the subheading as a link if link is provided', () => {
render(
<PersonalisedContent
// @ts-expect-error: Test fixture data does not need to match Article type exactly
pageData={basePageData}
personalisedTopicCurationExperimentVariant="personalised"
/>,
);
const subheadingLink = screen.getByRole('link', {
name: 'Personalised Title',
});
expect(subheadingLink).toHaveAttribute('href', '/personalised-link');
});
});
101 changes: 101 additions & 0 deletions src/app/components/PersonalisedContent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/** @jsx jsx */
import { jsx } from '@emotion/react';

import { Article } from '#app/models/types/optimo';
import CurationGrid from '#app/components/Curation/CurationGrid';
import Subheading from '#app/components/Curation/Subhead';
import { Summary } from '#app/models/types/curationData';
import { EventTrackingData } from '#app/lib/analyticsUtils/types';
import useViewTracker from '#app/hooks/useViewTracker';
import useClickTrackerHandler from '#app/hooks/useClickTrackerHandler';
import styles from '#app/components/RelatedContentSection/index.styles';

const PersonalisedContent = ({
pageData,
personalisedTopicCurationExperimentVariant,
}: {
pageData: Article;
personalisedTopicCurationExperimentVariant: string;
}) => {
type PersonalisedContentType = {
title?: string;
summaries?: Summary[];
curationLength?: number;
id?: string;
link?: string;
renderVisuallyHiddenH2Title?: boolean;
curationSubheading?: string;
isFirstCuration?: boolean;
topicId?: string;
};

const personalisedContentArray = pageData.secondaryColumn
?.PersonalisedContent as PersonalisedContentType[] | undefined;

const getPersonalisedContentData = () => {
if (personalisedTopicCurationExperimentVariant !== 'personalised') {
return undefined;
}
if (
!Array.isArray(personalisedContentArray) ||
personalisedContentArray.length === 0
) {
return undefined;
}
// Country-specific data is always first
return personalisedContentArray[0];
};

const personalisedContentData = getPersonalisedContentData();
const {
title,
summaries = [],
id = 'personalised-content',
link = '',
isFirstCuration = false,
topicId = '',
} = personalisedContentData || {};

const eventTrackingData: EventTrackingData = {
componentName: 'personalised-topic-curation',
sendOptimizelyEvents: true,
experimentName: 'newswb_ws_location_based_topics',
experimentVariant: personalisedTopicCurationExperimentVariant,
groupTracker: {
name: title,
type: 'personalised-topic-curation',
...(link && { link }),
...(topicId && { resourceId: topicId }),
...(summaries?.length > 0 && { itemCount: summaries.length }),
},
};
const viewTracker = useViewTracker(eventTrackingData);
const subheadingClickTracker = useClickTrackerHandler(eventTrackingData);

if (!personalisedContentData) {
return null;
}

return (
<section
aria-labelledby={id}
role="region"
{...viewTracker}
css={styles.relatedContentSection} // use the same style as related content for padding unless we want to make it look different
>
{title && (
<Subheading id={id} link={link} {...subheadingClickTracker}>
{title}
</Subheading>
)}
<CurationGrid
summaries={summaries}
headingLevel={3}
isFirstCuration={isFirstCuration}
eventTrackingData={eventTrackingData}
/>
</section>
);
};

export default PersonalisedContent;
1 change: 1 addition & 0 deletions src/app/models/types/optimo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export type SecondaryColumn = {
mediaCuration?: Curation;
topStories: TopStoryItem[];
features: object[];
PersonalisedContent?: object[];
latestMedia?: LatestMedia[];
};

Expand Down
31 changes: 31 additions & 0 deletions src/app/pages/ArticlePage/ArticlePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { Recommendation } from '#app/models/types/onwardJourney';
import ScrollablePromo from '#components/ScrollablePromo';
import Recommendations from '#app/components/Recommendations';
import { ReadTimeArticleExperiment as ReadTime } from '#app/components/ReadTime';
import PersonalisedContent from '../../components/PersonalisedContent';
import ElectionBanner from './ElectionBanner';
import ImageWithCaption from '../../components/ImageWithCaption';
import AdContainer from '../../components/Ad';
Expand Down Expand Up @@ -246,6 +247,27 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
experimentType: ExperimentType.CLIENT_SIDE,
});

// EXPERIMENT: Personalised Content Rail
const personalisedContentExperimentName = 'newswb_ws_location_based_topics';
let personalisedContentExperimentVariant = useOptimizelyVariation({
experimentName: personalisedContentExperimentName,
experimentType: ExperimentType.CLIENT_SIDE,
});

// temp overrides for testing
const personalisedContentOverride = true;
personalisedContentExperimentVariant = 'personalised';

const showPersonalisedContent = personalisedContentOverride
? true
: Boolean(
!isAmp &&
!isLite &&
!isApp &&
personalisedContentExperimentVariant &&
personalisedContentExperimentVariant === 'personalised',
);

const allowAdvertising = pageData?.metadata?.allowAdvertising ?? false;
const adcampaign = pageData?.metadata?.adCampaignKeyword;

Expand Down Expand Up @@ -470,6 +492,15 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
},
})}
/>
{/* EXPERIMENT: Personalised Content */}
{showPersonalisedContent && (
<PersonalisedContent
pageData={pageData}
personalisedTopicCurationExperimentVariant={
personalisedContentExperimentVariant ?? ''
}
/>
)}
</div>
{!isApp && !isPGL && (
<SecondaryColumn
Expand Down
70 changes: 69 additions & 1 deletion src/app/pages/ArticlePage/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ import {
import { ARTICLE_PAGE } from '#app/routes/utils/pageTypes';
import { suppressPropWarnings } from '#app/legacy/psammead/psammead-test-helpers/src';
import { Services } from '#app/models/types/global';

import { Article } from '#app/models/types/optimo';
import * as clickTracking from '#app/hooks/useClickTrackerHandler';
import * as viewTracking from '#app/hooks/useViewTracker';
import useOptimizelyVariation from '#app/hooks/useOptimizelyVariation';
import PersonalisedContent from '../../components/PersonalisedContent';
import {
render,
screen,
Expand Down Expand Up @@ -1208,4 +1208,72 @@ describe('Article Page', () => {
},
);
});

describe('Personalised topic curation', () => {
it('renders nothing if personalisedContentData is undefined', () => {
const pageData = {
...articleDataNews,
secondaryColumn: {
topStories: [],
features: [],
},
};
const { container } = render(
<PersonalisedContent
pageData={pageData}
personalisedTopicCurationExperimentVariant="variantA"
/>,
);
expect(container).toBeEmptyDOMElement();
});
it('renders section and subheading when personalisedContentData is present', () => {
const personalisedContent = [
{
title: 'Recommended for you',
summaries: [
{
type: 'promo',
title: 'Article 1',
description: 'Description 1',
link: '/article-1',
imageUrl: 'image-1.jpg',
imageAlt: 'Image 1',
isLive: false,
id: 'article-1',
},
{
type: 'promo',
title: 'Article 2',
description: 'Description 2',
link: '/article-2',
imageUrl: 'image-2.jpg',
imageAlt: 'Image 2',
isLive: false,
id: 'article-2',
},
],
id: 'personalised-content',
topicId: 'topic-1',
},
];
const pageData = {
...articleDataNews,
secondaryColumn: {
topStories: [],
features: [],
PersonalisedContent: personalisedContent,
},
};
render(
<PersonalisedContent
pageData={pageData}
personalisedTopicCurationExperimentVariant="personalised"
/>,
);
expect(screen.getByRole('region')).toBeInTheDocument();
expect(screen.getByText('Recommended for you')).toBeInTheDocument();
expect(screen.getByText('Article 1')).toBeInTheDocument();
expect(screen.getByText('Article 2')).toBeInTheDocument();
});
});
});
Loading
Loading