diff --git a/src/app/components/PersonalisedContent/index.test.tsx b/src/app/components/PersonalisedContent/index.test.tsx
new file mode 100644
index 00000000000..9a9fd84641e
--- /dev/null
+++ b/src/app/components/PersonalisedContent/index.test.tsx
@@ -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(
+ ,
+ );
+ expect(screen.queryByRole('region')).not.toBeInTheDocument();
+ });
+
+ it('renders personalised content when variant is "personalised"', () => {
+ render(
+ ,
+ );
+ 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(
+ ,
+ );
+ const subheadingLink = screen.getByRole('link', {
+ name: 'Personalised Title',
+ });
+ expect(subheadingLink).toHaveAttribute('href', '/personalised-link');
+ });
+});
diff --git a/src/app/components/PersonalisedContent/index.tsx b/src/app/components/PersonalisedContent/index.tsx
new file mode 100644
index 00000000000..b3ac5e599ec
--- /dev/null
+++ b/src/app/components/PersonalisedContent/index.tsx
@@ -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 (
+
+ {title && (
+
+ {title}
+
+ )}
+
+
+ );
+};
+
+export default PersonalisedContent;
diff --git a/src/app/models/types/optimo.ts b/src/app/models/types/optimo.ts
index 19b8fd3de51..8f24a59541b 100644
--- a/src/app/models/types/optimo.ts
+++ b/src/app/models/types/optimo.ts
@@ -135,6 +135,7 @@ export type SecondaryColumn = {
mediaCuration?: Curation;
topStories: TopStoryItem[];
features: object[];
+ PersonalisedContent?: object[];
latestMedia?: LatestMedia[];
};
diff --git a/src/app/pages/ArticlePage/ArticlePage.tsx b/src/app/pages/ArticlePage/ArticlePage.tsx
index 050ef04bf8f..f63dbc86ba8 100644
--- a/src/app/pages/ArticlePage/ArticlePage.tsx
+++ b/src/app/pages/ArticlePage/ArticlePage.tsx
@@ -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';
@@ -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;
@@ -470,6 +492,15 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
},
})}
/>
+ {/* EXPERIMENT: Personalised Content */}
+ {showPersonalisedContent && (
+
+ )}
{!isApp && !isPGL && (
{
},
);
});
+
+ describe('Personalised topic curation', () => {
+ it('renders nothing if personalisedContentData is undefined', () => {
+ const pageData = {
+ ...articleDataNews,
+ secondaryColumn: {
+ topStories: [],
+ features: [],
+ },
+ };
+ const { container } = render(
+ ,
+ );
+ 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(
+ ,
+ );
+ expect(screen.getByRole('region')).toBeInTheDocument();
+ expect(screen.getByText('Recommended for you')).toBeInTheDocument();
+ expect(screen.getByText('Article 1')).toBeInTheDocument();
+ expect(screen.getByText('Article 2')).toBeInTheDocument();
+ });
+ });
});
diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts
index 458d4418ec7..cf6ec0a0d85 100644
--- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts
+++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts
@@ -13,6 +13,8 @@ import augmentWithDisclaimer from '#app/routes/article/utils/augmentWithDisclaim
import shouldRender from '#app/legacy/containers/PageHandlers/withData/shouldRender';
import { ArticleMetadata } from '#app/models/types/optimo';
import { getServerExperiments } from '#server/utilities/experimentHeader';
+import fetchDataFromBFF from '#app/routes/utils/fetchDataFromBFF';
+import getAgent from '#server/utilities/getAgent';
import getPageData from '../../../utilities/pageRequests/getPageData';
const logger = nodeLogger(__filename);
@@ -93,8 +95,14 @@ export default async (context: GetServerSidePropsContext) => {
throw handleError('Article data is malformed', 500);
}
- const { article, secondaryData } = data?.pageData || {};
+ const country = (reqHeaders['x-country'] || reqHeaders['x-bbc-edge-country'])
+ ?.toString()
+ .toLowerCase();
+
+ const shouldAttemptPersonalisedTopicExperience =
+ service === 'mundo' && !isAmp && Boolean(country);
+ const { article, secondaryData } = data?.pageData || {};
const isArticleOlderThanSixHours =
Date.now() - article.metadata.lastPublished > 21600000;
const maxAge = isArticleOlderThanSixHours ? 90 : 45;
@@ -111,7 +119,62 @@ export default async (context: GetServerSidePropsContext) => {
mostRead = null,
billboardCuration = null,
mediaCuration = null,
- } = secondaryData;
+ } = secondaryData || {};
+
+ let personalisedContent;
+
+ if (shouldAttemptPersonalisedTopicExperience) {
+ const countrySpecificTopics: Record = {
+ ar: 'c7zp57yy6dzt',
+ cl: 'c340qyppkk8t',
+ mx: 'c340qyp6yggt',
+ co: 'c404v5gz1rkt',
+ es: 'c6vzy3wd189t',
+ ve: 'cpzd49v9rd1t',
+ us: 'cdr5613yzwqt',
+ uy: 'cpzd498zwj6t',
+ do: 'cr50y7pykkdt',
+ };
+
+ const countrySpecificId =
+ country && countrySpecificTopics[country.toString()];
+
+ if (countrySpecificId) {
+ try {
+ const countrySpecificData = await fetchDataFromBFF({
+ pathname: `/${service}/topics/${countrySpecificId}?renderer_env=live`,
+ pageType: 'topic',
+ service,
+ variant: variant || undefined,
+ isAmp,
+ getAgent,
+ });
+
+ const countryArticles =
+ countrySpecificData?.json?.data?.curations?.[0]?.summaries || [];
+
+ if (countrySpecificData?.json?.data) {
+ personalisedContent = [
+ {
+ title: countrySpecificData.json.data.title,
+ description: countrySpecificData.json.data.description,
+ link: `/${service}/topics/${countrySpecificId}`,
+ summaries: Array.isArray(countryArticles)
+ ? countryArticles.slice(0, 4)
+ : [],
+ topicId: countrySpecificId,
+ },
+ ];
+ }
+ } catch (error) {
+ logger.warn('PERSONALISED_CONTENT_TOPIC_FETCH_FAILED', {
+ message: (error as Error)?.message,
+ country,
+ topicId: countrySpecificId,
+ });
+ }
+ }
+ }
const transformedArticleData = transformPageData(toggles)(article);
@@ -145,6 +208,9 @@ export default async (context: GetServerSidePropsContext) => {
latestMedia,
mediaCuration,
billboardCuration,
+ ...(personalisedContent && {
+ PersonalisedContent: personalisedContent,
+ }),
},
mostRead,
},