Skip to content

Commit

Permalink
feat(project): add structured data for movie and series screens
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristiaanScheermeijer committed Jun 16, 2021
1 parent a4ba9d4 commit 2a6df70
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 2 deletions.
1 change: 0 additions & 1 deletion src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const Layout: FC<LayoutProps> = ({ children }) => {
<meta name="description" content={description} />
<meta property="og:description" content={description} />
<meta property="og:title" content={siteName} />
<meta property="og:type" content="video.other" />
{banner && <meta property="og:image" content={banner?.replace(/^https:/, 'http:')} />}
{banner && <meta property="og:image:secure_url" content={banner?.replace(/^http:/, 'https:')} />}
<meta name="twitter:title" content={siteName} />
Expand Down
5 changes: 4 additions & 1 deletion src/screens/Movie/Movie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { Helmet } from 'react-helmet';
import { useFavorites } from '../../stores/FavoritesStore';
import { ConfigContext } from '../../providers/ConfigProvider';
import useBlurImageUpdater from '../../hooks/useBlurImageUpdater';
import { cardUrl, videoUrl } from '../../utils/formatting';
import { cardUrl, movieURL, videoUrl } from '../../utils/formatting';
import type { PlaylistItem } from '../../../types/playlist';
import VideoComponent from '../../components/Video/Video';
import Shelf from '../../containers/Shelf/Shelf';
import useMedia from '../../hooks/useMedia';
import { generateMovieJSONLD } from '../../utils/structuredData';

type MovieRouteParams = {
id: string;
Expand Down Expand Up @@ -49,6 +50,7 @@ const Movie = (
<React.Fragment>
<Helmet>
<title>{item.title} - {config.siteName}</title>
{item ? <link rel="canonical" href={`${window.location.origin}${movieURL(item)}`} /> : null}
<meta name="description" content={item.description} />
<meta property="og:description" content={item.description} />
<meta property="og:title" content={`${item.title} - ${config.siteName}`} />
Expand All @@ -66,6 +68,7 @@ const Movie = (
<meta property="og:video:width" content="1280" />
<meta property="og:video:height" content="720" />
{item.tags.split(',').map(tag => <meta property="og:video:tag" content={tag} key={tag} />)}
{item ? <script type="application/ld+json">{generateMovieJSONLD(item)}</script> : null}
</Helmet>
<VideoComponent
item={item}
Expand Down
3 changes: 3 additions & 0 deletions src/screens/Series/Series.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import VideoComponent from '../../components/Video/Video';
import Shelf from '../../containers/Shelf/Shelf';
import useMedia from '../../hooks/useMedia';
import usePlaylist from '../../hooks/usePlaylist';
import { generateEpisodeJSONLD } from '../../utils/structuredData';

type SeriesRouteParams = {
id: string;
Expand Down Expand Up @@ -60,6 +61,7 @@ const Series = (
<React.Fragment>
<Helmet>
<title>{item.title} - {config.siteName}</title>
{seriesPlaylist && item ? <link rel="canonical" href={`${window.location.origin}${episodeURL(seriesPlaylist, item.mediaid)}`} /> : null}
<meta name="description" content={item.description} />
<meta property="og:description" content={item.description} />
<meta property="og:title" content={`${item.title} - ${config.siteName}`} />
Expand All @@ -77,6 +79,7 @@ const Series = (
<meta property="og:video:width" content="1280" />
<meta property="og:video:height" content="720" />
{item.tags.split(',').map(tag => <meta property="og:video:tag" content={tag} key={tag} />)}
{seriesPlaylist && item ? <script type="application/ld+json">{generateEpisodeJSONLD(seriesPlaylist, item)}</script> : null}
</Helmet>
<VideoComponent
item={item}
Expand Down
24 changes: 24 additions & 0 deletions src/utils/datetime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Seconds to ISO8601 duration or date string
*/
export function secondsToISO8601 (input: number, timeOnly: boolean = false): string {
if (!input) {
return '';
}

const date = new Date(input ? input * 1000 : 0);
const hours = date.getUTCHours();
const minutes = date.getUTCMinutes();
const seconds = date.getUTCSeconds();

if (!timeOnly) {
return date.toISOString();
}

let isoString = 'PT';
if (hours > 0) isoString += hours + 'H';
if (minutes > 0) isoString += minutes + 'M';
if (seconds > 0) isoString += seconds + 'S';

return isoString;
}
49 changes: 49 additions & 0 deletions src/utils/structuredData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Playlist, PlaylistItem } from '../../types/playlist';

import { episodeURL, movieURL } from './formatting';
import { secondsToISO8601 } from './datetime';

export const generateSeriesMetadata = (seriesPlaylist: Playlist) => {
const seriesCanonical = `${window.location.origin}/${episodeURL(seriesPlaylist)}`;

return {
'@type': 'TVSeries',
'@id': seriesCanonical,
name: seriesPlaylist.title,
numberOfEpisodes: seriesPlaylist.playlist.length,
numberOfSeasons: seriesPlaylist.playlist.reduce(function (list, playlistItem) {
return !playlistItem.seasonNumber || list.includes(playlistItem.seasonNumber) ? list : list.concat(playlistItem.seasonNumber);
}, [] as string[]).length
};
}

export const generateEpisodeJSONLD = (seriesPlaylist: Playlist, episode: PlaylistItem) => {
const episodeCanonical = `${window.location.origin}/${episodeURL(seriesPlaylist, episode.mediaid)}`;
const seriesMetadata = generateSeriesMetadata(seriesPlaylist);

return JSON.stringify({
'@context': 'http://schema.org/',
'@type': 'TVEpisode',
'@id': episodeCanonical,
episodeNumber: episode.episodeNumber,
seasonNumber: episode.seasonNumber,
name: episode.title,
uploadDate: secondsToISO8601(episode.pubdate),
partOfSeries: seriesMetadata,
});
}

export const generateMovieJSONLD = (item: PlaylistItem) => {
const movieCanonical = `${window.location.origin}/${movieURL(item)}`;

return JSON.stringify({
'@context': 'http://schema.org/',
'@type': 'VideoObject',
'@id': movieCanonical,
name: item.title,
description: item.description,
duration: secondsToISO8601(item.duration, true),
thumbnailUrl: item.image,
uploadDate: secondsToISO8601(item.pubdate)
});
}
2 changes: 2 additions & 0 deletions types/playlist.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export type PlaylistItem = {
rating: string;
sources: Source[];
seriesId?: string;
episodeNumber?: string;
seasonNumber?: string;
tags: string;
title: string;
tracks: Track[];
Expand Down

0 comments on commit 2a6df70

Please sign in to comment.