Skip to content

Commit

Permalink
feat(project): add SEO for screens and update translations
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristiaanScheermeijer committed Jun 15, 2021
1 parent 59a17eb commit de5ddc8
Show file tree
Hide file tree
Showing 17 changed files with 272 additions and 152 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
"pullstate": "^1.22.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^11.10.0",
"react-helmet": "^6.1.0",
"react-i18next": "^11.10.0",
"react-query": "^3.13.10",
"react-router-dom": "^5.2.0",
"react-virtualized": "^9.22.3",
Expand Down
30 changes: 9 additions & 21 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
<title>JW OTT Webapp</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />

<meta
name="description"
content="JW OTT Webapp is an open-source, dynamically generated video website built around JW Player and JW Platform services. It enables you to easily publish your JW Player-hosted video content with no coding and minimal configuration."
/>
<meta name="twitter:card" content="summary" />
<meta property="og:title" content="JW OTT Webapp">
<meta property="og:description" content="JW OTT Webapp is an open-source, dynamically generated video website built around JW Player and JW Platform services. It enables you to easily publish your JW Player-hosted video content with no coding and minimal configuration.">
<meta property="og:image" content="/images/icon-256x256.png">
<meta name="description" content="JW OTT Webapp is an open-source, dynamically generated video website." data-react-helmet="true" />
<meta name="twitter:card" content="summary" data-react-helmet="true" />
<meta property="og:title" content="JW OTT Webapp" data-react-helmet="true" >
<meta property="og:description" content="JW OTT Webapp is an open-source, dynamically generated video website." data-react-helmet="true">
<meta property="og:image" content="/images/icon-256x256.png" data-react-helmet="true">

<link rel="icon" href="favicon.ico" type="image/x-icon" />

<!-- Pre-connect JW Player origins -->
Expand All @@ -26,11 +24,11 @@
<link rel="apple-touch-icon" href="/images/icon-152x152.png">

<!-- Chrome, Firefox OS and Opera -->
<meta name="theme-color" content="#fff">
<meta name="theme-color" content="#000000">
<!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#fff">
<meta name="msapplication-navbutton-color" content="#000000">
<!-- iOS Safari -->
<meta name="apple-mobile-web-app-status-bar-style" content="#fff">
<meta name="apple-mobile-web-app-status-bar-style" content="#000000">
</head>
<body>
<div id="root"></div>
Expand All @@ -52,15 +50,5 @@
}
</script>
<script type="module" src="/dist/index.js"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
4 changes: 3 additions & 1 deletion src/components/Shelf/Shelf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ export type ShelfProps = {
featured?: boolean;
loading?: boolean;
error?: unknown;
title?: string;
};

const Shelf: React.FC<ShelfProps> = ({
playlist,
onCardClick,
onCardHover,
title,
featured = false,
loading = false,
error = null,
Expand All @@ -61,7 +63,7 @@ const Shelf: React.FC<ShelfProps> = ({

return (
<div className={classNames(styles.shelf, { [styles.featured]: featured })} data-mediaid={playlist.feedid}>
{!featured && <h2 className={classNames(styles.title, { [styles.loading]: loading })}>{playlist.title}</h2>}
{!featured && <h2 className={classNames(styles.title, { [styles.loading]: loading })}>{title || playlist.title}</h2>}
<TileDock<PlaylistItem>
items={playlist.playlist}
tilesToShow={tilesToShow}
Expand Down
4 changes: 3 additions & 1 deletion src/containers/Shelf/Shelf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ type ShelfProps = {
onCardHover: (playlistItem: PlaylistItem) => void;
relatedMediaId?: string;
featured?: boolean;
title?: string;
};

const alternativeShelves = ['favorites'];
const Shelf = ({ playlistId, onCardClick, onCardHover, relatedMediaId, featured = false }: ShelfProps): JSX.Element | null => {
const Shelf = ({ playlistId, onCardClick, onCardHover, relatedMediaId, featured = false, title }: ShelfProps): JSX.Element | null => {
const isAlternativeShelf = alternativeShelves.includes(playlistId);
const {
isLoading,
Expand All @@ -36,6 +37,7 @@ const Shelf = ({ playlistId, onCardClick, onCardHover, relatedMediaId, featured
playlist={shelfPlaylist}
onCardClick={onCardClick}
onCardHover={onCardHover}
title={title}
featured={featured}
/>
);
Expand Down
77 changes: 0 additions & 77 deletions src/containers/Video/Video.tsx

This file was deleted.

14 changes: 6 additions & 8 deletions src/hooks/useBlurImageUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ import { useEffect } from 'react';
import { UIStore } from '../stores/UIStore';
import type { PlaylistItem } from '../../types/playlist';

const useBlurImageUpdater = (playlist: PlaylistItem[]) => {
const useBlurImageUpdater = (playlist: PlaylistItem[] | null, item?: PlaylistItem | null) => {
useEffect(() => {
if (!playlist.length) return;
const targetItem = playlist?.[0] || item;

const { image } = playlist[0];
if (!targetItem?.image) return;

if (image) {
UIStore.update((state) => {
state.blurImage = image;
});
}
UIStore.update((state) => {
state.blurImage = targetItem.image;
});
}, [playlist]);

return (image: string) =>
Expand Down
12 changes: 12 additions & 0 deletions src/hooks/useMedia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { UseBaseQueryResult, useQuery } from 'react-query';
import type { PlaylistItem } from 'types/playlist';

import { getMediaById } from '../services/api.service';

export type UseMediaResult<TData = PlaylistItem, TError = unknown> = UseBaseQueryResult<TData, TError>;

export default function useMedia (mediaId: string, enabled: boolean = true): UseMediaResult {
return useQuery(['media', mediaId], () => getMediaById(mediaId), {
enabled: !!mediaId && enabled,
});
}
4 changes: 2 additions & 2 deletions src/hooks/usePlaylist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ const placeholderData = generatePlaylistPlaceholder(30);

export type UsePlaylistResult<TData = Playlist, TError = unknown> = UseBaseQueryResult<TData, TError>;

export default function usePlaylist (playlistId: string, relatedMediaId?: string, enabled: boolean = true): UsePlaylistResult {
export default function usePlaylist (playlistId: string, relatedMediaId?: string, enabled: boolean = true, usePlaceholderData: boolean = true): UsePlaylistResult {
return useQuery(['playlist', playlistId, relatedMediaId], () => getPlaylistById(playlistId, relatedMediaId), {
enabled: !!playlistId && enabled,
placeholderData,
placeholderData: usePlaceholderData ? placeholderData : undefined,
});
}
1 change: 1 addition & 0 deletions src/i18n/locales/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

export { default as common } from './en_US/common.json';
export { default as menu } from './en_US/menu.json';
export { default as search } from './en_US/search.json';
export { default as video } from './en_US/video.json';
14 changes: 14 additions & 0 deletions src/i18n/locales/en_US/search.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"error_description": "Reload this page or try again later.",
"error_heading": "Something went wrong",
"error_subheading": "It looks like we had an issue loading this page...",
"heading": "Search results",
"no_results_heading": "No results found for \"{{query}}\"",
"start_typing": "Type something in the search box to start searching",
"suggestions": "Suggestions:",
"tip_one": "Make sure all words are spelled correctly",
"tip_three": "Try different search terms",
"tip_two": "Make search terms more general",
"title": "{{results}} result for \"{{query}}\"",
"title_plural": "{{results}} results for \"{{query}}\""
}
1 change: 1 addition & 0 deletions src/i18n/locales/nl_NL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

export { default as common } from './nl_NL/common.json';
export { default as menu } from './nl_NL/menu.json';
export { default as search } from './nl_NL/search.json';
export { default as video } from './nl_NL/video.json';
13 changes: 13 additions & 0 deletions src/i18n/locales/nl_NL/search.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"error_description": "",
"error_heading": "",
"error_subheading": "",
"heading": "",
"no_results_heading": "",
"start_typing": "",
"suggestions": "",
"tip_one": "",
"tip_three": "",
"tip_two": "",
"title": ""
}
93 changes: 82 additions & 11 deletions src/screens/Movie/Movie.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,93 @@
import React from 'react';
import { RouteComponentProps, useLocation } from 'react-router-dom';
import React, { useContext } from 'react';
import type { RouteComponentProps } from 'react-router-dom';
import { useHistory } from 'react-router';
import { Helmet } from 'react-helmet';

import Video from '../../containers/Video/Video';
import { useFavorites } from '../../stores/FavoritesStore';
import { ConfigContext } from '../../providers/ConfigProvider';
import useBlurImageUpdater from '../../hooks/useBlurImageUpdater';
import { cardUrl, 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';

type MovieRouteParams = {
id: string;
};

const Movie = ({
match: {
params: { id },
},
}: RouteComponentProps<MovieRouteParams>): JSX.Element => {
const listId: string | null = new URLSearchParams(useLocation().search).get('list');
const Movie = (
{
match: {
params: { id },
},
location,
}: RouteComponentProps<MovieRouteParams>): JSX.Element => {
const config = useContext(ConfigContext);
const searchParams = new URLSearchParams(location.search);
const { isLoading, error, data: item } = useMedia(id);

if (!listId) return <p>No playlist id</p>;
const history = useHistory();
const { hasItem, saveItem, removeItem } = useFavorites();
const play = searchParams.get('play') === '1';
const posterFading: boolean = config ? config.options.posterFading === true : false;

return <Video videoType={'movie'} playlistId={listId} mediaId={id} />;
const updateBlurImage = useBlurImageUpdater(null, item);
const isFavorited = !!item && hasItem(item);

const startPlay = () => item && history.push(videoUrl(item, searchParams.get('r'), true));
const goBack = () => item && history.push(videoUrl(item, searchParams.get('r'), false));

const onCardClick = (item: PlaylistItem) => history.push(cardUrl(item));
const onCardHover = (item: PlaylistItem) => updateBlurImage(item.image);

if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error loading list</p>;
if (!item) return <p>Can not find medium</p>;

return (
<React.Fragment>
<Helmet>
<title>{item.title} - {config.siteName}</title>
<meta name="description" content={item.description} />
<meta property="og:description" content={item.description} />
<meta property="og:title" content={`${item.title} - ${config.siteName}`} />
<meta property="og:type" content="video.other" />
{item.image && <meta property="og:image" content={item.image?.replace(/^https:/, 'http:')} />}
{item.image && <meta property="og:image:secure_url" content={item.image?.replace(/^http:/, 'https:')} />}
<meta property="og:image:width" content={item.image ? '720' : ''} />
<meta property="og:image:height" content={item.image ? '406' : ''} />
<meta name="twitter:title" content={`${item.title} - ${config.siteName}`} />
<meta name="twitter:description" content={item.description} />
<meta name="twitter:image" content={item.image} />
<meta property="og:video" content={window.location.href} />
<meta property="og:video:secure_url" content={window.location.href} />
<meta property="og:video:type" content="text/html" />
<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} />)}
</Helmet>
<VideoComponent
item={item}
play={play}
startPlay={startPlay}
goBack={goBack}
poster={posterFading ? 'fading' : 'normal'}
isFavorited={isFavorited}
onFavoriteButtonClick={() => isFavorited ? removeItem(item) : saveItem(item)}
relatedShelf={
config.recommendationsPlaylist ? (
<Shelf
playlistId={config.recommendationsPlaylist}
onCardClick={onCardClick}
onCardHover={onCardHover}
relatedMediaId={item.mediaid}
/>
) : undefined
}
/>
</React.Fragment>
);
};

export default Movie;
4 changes: 4 additions & 0 deletions src/screens/Playlist/Playlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useContext, useMemo, useState } from 'react';
import { RouteComponentProps, useHistory } from 'react-router-dom';
import type { PlaylistItem } from 'types/playlist';
import type { Config } from 'types/Config';
import { Helmet } from 'react-helmet';

import { ConfigContext } from '../../providers/ConfigProvider';
import { cardUrl } from '../../utils/formatting';
Expand Down Expand Up @@ -42,6 +43,9 @@ function Playlist({

return (
<div className={styles.playlist}>
<Helmet>
<title>{title} - {config.siteName}</title>
</Helmet>
<header className={styles.header}>
<h2>{title}</h2>
<Filter name="categories" value={filter} defaultLabel="All" options={categories} setValue={setFilter} />
Expand Down
Loading

0 comments on commit de5ddc8

Please sign in to comment.