Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 38 additions & 0 deletions src/app/components/LinkedData/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,44 @@ describe('LinkedData', () => {
],
};

describe('SpeakableSpecification schema', () => {
const baseProps = {
type: 'WebPage',
seoTitle: 'Hindi Most Read Title',
datePublished: '2024-01-01T12:00:00.000Z',
dateModified: '2024-01-01T13:00:00.000Z',
};

it('includes SpeakableSpecification for Hindi service with title only', () => {
render(
<Context service="hindi">
<LinkedData {...baseProps} />
</Context>,
);
const output = getLinkedDataOutput();
const speakable = output['@graph'].find(
(item: Record<string, unknown>) => item.speakable,
);
expect(speakable).toBeTruthy();
expect(speakable.speakable).toEqual([
{ '@type': 'SpeakableSpecification', xpath: ['/html/head/title'] },
]);
});

it('does not include SpeakableSpecification for non-enabled service', () => {
render(
<Context service="news">
<LinkedData {...baseProps} />
</Context>,
);
const output = getLinkedDataOutput();
const speakable = output['@graph'].find(
(item: Record<string, unknown>) => item.speakable,
);
expect(speakable).toBeUndefined();
});
});

it('should correctly render linked data for Ondemand Radio page', () => {
render(
<Context>
Expand Down
41 changes: 38 additions & 3 deletions src/app/components/LinkedData/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Helmet } from 'react-helmet';
import { RequestContext } from '#contexts/RequestContext';
import serialiseForScript from '#lib/utilities/serialiseForScript';
import getBrandedImage from '#lib/utilities/getBrandedImage';
import { Services } from '#app/models/types/global';
import { ServiceContext } from '../../contexts/ServiceContext';
import getAboutTagsContent from './getAboutTagsContent';
import { BylineLinkedData, LinkedDataProps } from './types';
Expand All @@ -28,6 +29,34 @@ type AuthorStructure = {

type Author = AuthorStructure | AuthorStructure[];

type SpeakableSpecification = {
'@type': 'SpeakableSpecification';
xpath: string[];
};

const SPEAKABLE_ENABLED_SERVICES = ['hindi']; // TODO: to be extended
const SUPPORTED_SPEAKABLE_TYPES = ['WebPage'];

const getSpeakableXpaths = ({
service,
seoTitle,
type,
}: {
service: Services;
seoTitle?: string;
type: string;
}): SpeakableSpecification[] | null => {
if (!SUPPORTED_SPEAKABLE_TYPES.includes(type)) return null;
if (!SPEAKABLE_ENABLED_SERVICES.includes(service)) return null;
if (!seoTitle) return null;
return [
{
'@type': 'SpeakableSpecification',
xpath: ['/html/head/title'],
},
];
};

const LinkedData = ({
showAuthor = false,
type,
Expand Down Expand Up @@ -177,6 +206,13 @@ const LinkedData = ({
if (hasByline && bylineAuthors && bylineAuthors.length > 0) {
author = bylineAuthors.length === 1 ? bylineAuthors[0] : bylineAuthors;
}

const speakableXpaths = getSpeakableXpaths({
service,
seoTitle,
type,
});

const linkedData = {
'@type': type,
url: canonicalNonUkLink,
Expand All @@ -191,10 +227,9 @@ const LinkedData = ({
coverageEndTime,
inLanguage,
...(aboutTags && { about: getAboutTagsContent(aboutTags) }),
...(showAuthor && {
author,
}),
...(showAuthor && { author }),
...(hasByline && places.length > 0 && { locationCreated }),
...(speakableXpaths && { speakable: speakableXpaths }),
};

return (
Expand Down
Loading