diff --git a/src/app/components/LinkedData/index.test.tsx b/src/app/components/LinkedData/index.test.tsx
index 469ea758784..b9116e8c977 100644
--- a/src/app/components/LinkedData/index.test.tsx
+++ b/src/app/components/LinkedData/index.test.tsx
@@ -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(
+
+
+ ,
+ );
+ const output = getLinkedDataOutput();
+ const speakable = output['@graph'].find(
+ (item: Record) => 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(
+
+
+ ,
+ );
+ const output = getLinkedDataOutput();
+ const speakable = output['@graph'].find(
+ (item: Record) => item.speakable,
+ );
+ expect(speakable).toBeUndefined();
+ });
+ });
+
it('should correctly render linked data for Ondemand Radio page', () => {
render(
diff --git a/src/app/components/LinkedData/index.tsx b/src/app/components/LinkedData/index.tsx
index 2625928b4bc..c61463a880b 100644
--- a/src/app/components/LinkedData/index.tsx
+++ b/src/app/components/LinkedData/index.tsx
@@ -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';
@@ -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,
@@ -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,
@@ -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 (