diff --git a/components/StructuredData.js b/components/StructuredData.js index b85fc5dc..c65fae11 100644 --- a/components/StructuredData.js +++ b/components/StructuredData.js @@ -1,10 +1,10 @@ import { structuredData } from 'lib/structuredData' -const StructuredData = () => ( +const StructuredData = ({ data = structuredData }) => ( ) diff --git a/lib/findingStructuredData.js b/lib/findingStructuredData.js new file mode 100644 index 00000000..c80c4708 --- /dev/null +++ b/lib/findingStructuredData.js @@ -0,0 +1,82 @@ +import { EXPLORER_BASE_URL, ORGANIZATION_ID } from 'lib/structuredData' + +const getAuthor = (reportedBy) => { + if (!reportedBy) { + return { '@id': ORGANIZATION_ID } + } + + const authors = (reportedBy.match(/[^,(]+(?:\([^)]*\))?/g) ?? []).reduce( + (names, part) => { + const trimmed = part.trim() + + if (trimmed === '') { + return names + } + + if (trimmed.toLowerCase() === 'ooni') { + names.push({ '@id': ORGANIZATION_ID }) + } else { + names.push({ '@type': 'Person', name: trimmed }) + } + + return names + }, + [], + ) + + if (!authors.length) { + return { '@id': ORGANIZATION_ID } + } + + return authors +} + +const getKeywords = (incident) => { + const keywords = new Set() + + for (const tag of incident.tags ?? []) { + keywords.add(tag.startsWith('theme-') ? tag.replace('theme-', '') : tag) + } + + for (const theme of incident.themes ?? []) { + keywords.add(theme.replace('_', ' ')) + } + + return [...keywords] +} + +export const getFindingStructuredData = (incident, canonicalUrl) => { + const keywords = getKeywords(incident) + const schema = { + '@context': 'https://schema.org', + '@type': 'Article', + '@id': `${canonicalUrl}#article`, + headline: incident.title, + description: incident.short_description, + url: canonicalUrl, + datePublished: incident.create_time, + author: getAuthor(incident.reported_by), + publisher: { + '@id': ORGANIZATION_ID, + }, + mainEntityOfPage: { + '@type': 'WebPage', + '@id': canonicalUrl, + }, + isPartOf: { + '@id': `${EXPLORER_BASE_URL}/#website`, + }, + inLanguage: 'en', + } + + + if (incident.update_time) { + schema.dateModified = incident.update_time + } + + if (keywords.length) { + schema.keywords = keywords.join(', ') + } + + return schema +} diff --git a/lib/structuredData.js b/lib/structuredData.js index f3bfc28b..2183429e 100644 --- a/lib/structuredData.js +++ b/lib/structuredData.js @@ -1,5 +1,5 @@ -const EXPLORER_BASE_URL = 'https://explorer.ooni.org' -const ORGANIZATION_ID = 'https://ooni.org/#organization' +export const EXPLORER_BASE_URL = 'https://explorer.ooni.org' +export const ORGANIZATION_ID = 'https://ooni.org/#organization' const SUPPORTED_LANGUAGES = [ 'en', diff --git a/pages/findings/[id].js b/pages/findings/[id].js index de5339e8..6caada43 100644 --- a/pages/findings/[id].js +++ b/pages/findings/[id].js @@ -3,6 +3,8 @@ import { apiEndpoints, fetcher } from 'lib/api' import NotFound from 'components/NotFound' import FindingDisplay from 'components/findings/FindingDisplay' +import StructuredData from 'components/StructuredData' +import { getFindingStructuredData } from 'lib/findingStructuredData' import { useMemo } from 'react' import { useIntl } from 'react-intl' @@ -26,6 +28,14 @@ const ReportView = ({ data, canonicalUrl }) => { const metaTitle = `${!!data?.incident?.title && `${data?.incident?.title} | `}${intl.formatMessage({ id: 'General.OoniExplorer' })}` const metaDescription = data?.incident?.short_description + const findingStructuredData = useMemo(() => { + if (!data?.incident) { + return null + } + + return getFindingStructuredData(data.incident, canonicalUrl) + }, [canonicalUrl, data?.incident]) + return ( <>
@@ -44,6 +54,9 @@ const ReportView = ({ data, canonicalUrl }) => { content={metaDescription} /> + {findingStructuredData && ( +