diff --git a/config/speechConfig.js.example b/config/speechConfig.js.example index 04c29d27..0d7e9ab0 100644 --- a/config/speechConfig.js.example +++ b/config/speechConfig.js.example @@ -26,6 +26,7 @@ module.exports = { title: 'Spee.ch', name : 'Spee.ch', host : 'https://spee.ch', + description: 'Open-source, decentralized image and video sharing.' }, claim: { defaultTitle : 'Spee.ch', diff --git a/helpers/handlePageRender.jsx b/helpers/handlePageRender.jsx index 5ec46c3b..206303c4 100644 --- a/helpers/handlePageRender.jsx +++ b/helpers/handlePageRender.jsx @@ -7,6 +7,7 @@ import { StaticRouter } from 'react-router-dom'; import GAListener from '../react/components/GAListener'; import App from '../react/app'; import renderFullPage from './renderFullPage.js'; +import Helmet from 'react-helmet'; module.exports = (req, res) => { let context = {}; @@ -25,6 +26,9 @@ module.exports = (req, res) => { ); + // get head tags from helmet + const helmet = Helmet.renderStatic(); + // check for a redirect if (context.url) { // Somewhere a `` was rendered @@ -37,5 +41,5 @@ module.exports = (req, res) => { const preloadedState = store.getState(); // send the rendered page back to the client - res.send(renderFullPage(html, preloadedState)); + res.send(renderFullPage(helmet, html, preloadedState)); }; diff --git a/react/api/channelApi.js b/react/api/channelApi.js index 67ac5259..e8a45d4c 100644 --- a/react/api/channelApi.js +++ b/react/api/channelApi.js @@ -11,6 +11,6 @@ export function getChannelData (name, id) { export function getChannelClaims (name, longId, page) { console.log('getting channel claims for channel:', name, longId); if (!page) page = 1; - const url = `/api/channel/claims/${name}/${longId}/${page}`; + const url = `${host}/api/channel/claims/${name}/${longId}/${page}`; return Request(url); }; diff --git a/react/components/AboutPage/index.js b/react/components/AboutPage/index.js index 566dd94c..9b589617 100644 --- a/react/components/AboutPage/index.js +++ b/react/components/AboutPage/index.js @@ -1,17 +1,18 @@ import React from 'react'; import NavBar from 'containers/NavBar'; -import Helmet from 'react-helmet'; - -const { site: { title, host } } = require('../../../config/speechConfig.js'); +import SEO from 'components/SEO'; +import { createPageTitle } from 'utils/pageTitle'; +import { createBasicCanonicalLink } from 'utils/canonicalLink'; +import { createBasicMetaTags } from 'utils/metaTags'; class AboutPage extends React.Component { render () { + const pageTitle = createPageTitle('About'); + const canonicalLink = createBasicCanonicalLink('about'); + const metaTags = createBasicMetaTags(); return (
- - {title} - About - - +
diff --git a/react/components/HomePage/index.js b/react/components/HomePage/index.js index 57331257..bd156842 100644 --- a/react/components/HomePage/index.js +++ b/react/components/HomePage/index.js @@ -1,18 +1,19 @@ import React from 'react'; -import Helmet from 'react-helmet'; +import SEO from 'components/SEO'; import NavBar from 'containers/NavBar'; import PublishTool from 'containers/PublishTool'; - -const { site: { title, host } } = require('../../../config/speechConfig.js'); +import { createPageTitle } from 'utils/pageTitle'; +import { createBasicCanonicalLink } from 'utils/canonicalLink'; +import { createBasicMetaTags } from 'utils/metaTags'; class HomePage extends React.Component { render () { + const pageTitle = createPageTitle(); + const canonicalLink = createBasicCanonicalLink(); + const metaTags = createBasicMetaTags(); return (
- - {title} - - +
diff --git a/react/components/OpenGraphTags/view.jsx b/react/components/OpenGraphTags/view.jsx index 85a53cfe..5ca05a12 100644 --- a/react/components/OpenGraphTags/view.jsx +++ b/react/components/OpenGraphTags/view.jsx @@ -74,7 +74,7 @@ class OpenGraphTags extends React.Component { {/* image twitter tags */} - )}; + )}
); } diff --git a/react/components/SEO/index.jsx b/react/components/SEO/index.jsx new file mode 100644 index 00000000..064528bc --- /dev/null +++ b/react/components/SEO/index.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import Helmet from 'react-helmet'; +import PropTypes from 'prop-types'; + +class SEO extends React.Component { + render () { + const { pageTitle, metaTags, canonicalLink } = this.props; + return ( + + ); + } +}; + +SEO.propTypes = { + pageTitle : PropTypes.string.isRequired, + metaTags : PropTypes.array.isRequired, + canonicalLink: PropTypes.string.isRequired, +}; + +export default SEO; diff --git a/react/components/ShowAssetDetails/view.jsx b/react/components/ShowAssetDetails/view.jsx index 1f3b3ff5..8d74d500 100644 --- a/react/components/ShowAssetDetails/view.jsx +++ b/react/components/ShowAssetDetails/view.jsx @@ -1,33 +1,25 @@ import React from 'react'; -import Helmet from 'react-helmet'; -import OpenGraphTags from 'components/OpenGraphTags'; +import SEO from 'components/SEO'; import NavBar from 'containers/NavBar'; import ErrorPage from 'components/ErrorPage'; import AssetTitle from 'components/AssetTitle'; import AssetDisplay from 'components/AssetDisplay'; import AssetInfo from 'components/AssetInfo'; - -const { site: { title, host } } = require('../../../config/speechConfig.js'); +import { createPageTitle } from 'utils/pageTitle'; +import { createAssetCanonicalLink } from 'utils/canonicalLink'; +import { createAssetMetaTags } from 'utils/metaTags'; class ShowAssetDetails extends React.Component { render () { const { asset } = this.props; if (asset) { - let channelName, certificateId, name, claimId; - if (asset.claimData) { - ({ channelName, certificateId, name, claimId } = asset.claimData); - }; + const { name } = asset.claimData; + const pageTitle = createPageTitle(`${name} - details`); + const canonicalLink = createAssetCanonicalLink(asset); + const metaTags = createAssetMetaTags(asset); return (
- - {title} - {name} - details - {channelName ? ( - - ) : ( - - )} - - +
diff --git a/react/components/ShowAssetLite/view.jsx b/react/components/ShowAssetLite/view.jsx index 25b0220a..64144279 100644 --- a/react/components/ShowAssetLite/view.jsx +++ b/react/components/ShowAssetLite/view.jsx @@ -1,30 +1,24 @@ import React from 'react'; -import Helmet from 'react-helmet'; +import SEO from 'components/SEO'; import OpenGraphTags from 'components/OpenGraphTags'; import { Link } from 'react-router-dom'; import AssetDisplay from 'components/AssetDisplay'; - -const { site: { title, host } } = require('../../../config/speechConfig.js'); +import { createPageTitle } from 'utils/pageTitle'; +import { createAssetCanonicalLink } from 'utils/canonicalLink'; +import { createAssetMetaTags } from 'utils/metaTags'; class ShowLite extends React.Component { render () { const { asset } = this.props; if (asset) { - let channelName, certificateId, name, claimId, fileExt; - if (asset.claimData) { - ({ channelName, certificateId, name, claimId, fileExt } = asset.claimData); - }; + const { name, claimId } = asset.claimData; + const pageTitle = createPageTitle(name); + const canonicalLink = createAssetCanonicalLink(asset); + const metaTags = createAssetMetaTags(asset); return (
- - {title} - {name} - {channelName ? ( - - ) : ( - - )} - - + +
hosted diff --git a/react/components/ShowChannel/view.jsx b/react/components/ShowChannel/view.jsx index a328cc13..4735aa8d 100644 --- a/react/components/ShowChannel/view.jsx +++ b/react/components/ShowChannel/view.jsx @@ -1,22 +1,23 @@ import React from 'react'; -import Helmet from 'react-helmet'; +import SEO from 'components/SEO'; import ErrorPage from 'components/ErrorPage'; import NavBar from 'containers/NavBar'; import ChannelClaimsDisplay from 'containers/ChannelClaimsDisplay'; - -const { site: { title, host } } = require('../../../config/speechConfig.js'); +import { createPageTitle } from 'utils/pageTitle'; +import { createChannelCanonicalLink } from 'utils/canonicalLink'; +import { createChannelMetaTags } from 'utils/metaTags'; class ShowChannel extends React.Component { render () { const { channel } = this.props; if (channel) { const { name, longId, shortId } = channel; + const pageTitle = createPageTitle(`${name}`); + const canonicalLink = createChannelCanonicalLink(channel); + const metaTags = createChannelMetaTags(channel); return (
- - {title} - {name} - - +
diff --git a/react/containers/LoginPage/view.jsx b/react/containers/LoginPage/view.jsx index b984265f..bf25662b 100644 --- a/react/containers/LoginPage/view.jsx +++ b/react/containers/LoginPage/view.jsx @@ -1,11 +1,12 @@ import React from 'react'; import { withRouter } from 'react-router-dom'; -import Helmet from 'react-helmet'; +import SEO from 'components/SEO'; import NavBar from 'containers/NavBar'; import ChannelLoginForm from 'containers/ChannelLoginForm'; import ChannelCreateForm from 'containers/ChannelCreateForm'; - -const { site: { title, host } } = require('../../../config/speechConfig.js'); +import { createPageTitle } from 'utils/pageTitle'; +import { createBasicCanonicalLink } from 'utils/canonicalLink'; +import { createBasicMetaTags } from 'utils/metaTags'; class LoginPage extends React.Component { componentWillReceiveProps (newProps) { @@ -16,12 +17,12 @@ class LoginPage extends React.Component { } } render () { + const pageTitle = createPageTitle('Login'); + const canonicalLink = createBasicCanonicalLink('login'); + const metaTags = createBasicMetaTags(); return (
- - {title} - Login - - +
diff --git a/react/sagas/show_uri.js b/react/sagas/show_uri.js index ddb5b5d2..ad3021ba 100644 --- a/react/sagas/show_uri.js +++ b/react/sagas/show_uri.js @@ -50,7 +50,6 @@ function * parseAndUpdateClaimOnly (claim) { export function * handleShowPageUri (action) { console.log('handleShowPageUri'); - console.log('action:', action); const { identifier, claim } = action.data; if (identifier) { return yield call(parseAndUpdateIdentifierAndClaim, identifier, claim); diff --git a/react/utils/canonicalLink.js b/react/utils/canonicalLink.js new file mode 100644 index 00000000..53507180 --- /dev/null +++ b/react/utils/canonicalLink.js @@ -0,0 +1,24 @@ +const { site: { host } } = require('../../config/speechConfig.js'); + +export const createBasicCanonicalLink = (page) => { + if (!page) { + return `${host}`; + }; + return `${host}/${page}`; +}; + +export const createAssetCanonicalLink = (asset) => { + let channelName, certificateId, name, claimId; + if (asset.claimData) { + ({ channelName, certificateId, name, claimId } = asset.claimData); + }; + if (channelName) { + return `${host}/${channelName}:${certificateId}/${name}`; + }; + return `${host}/${claimId}/${name}`; +}; + +export const createChannelCanonicalLink = (channel) => { + const { name, longId } = channel; + return `${host}/${name}:${longId}`; +}; diff --git a/react/utils/metaTags.js b/react/utils/metaTags.js new file mode 100644 index 00000000..5bcf67ad --- /dev/null +++ b/react/utils/metaTags.js @@ -0,0 +1,86 @@ +const { site: { title, host, description }, claim: { defaultThumbnail, defaultDescription } } = require('../../config/speechConfig.js'); + +const determineOgThumbnailContentType = (thumbnail) => { + if (thumbnail) { + const fileExt = thumbnail.substring(thumbnail.lastIndexOf('.')); + switch (fileExt) { + case 'jpeg': + case 'jpg': + return 'image/jpeg'; + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + case 'mp4': + return 'video/mp4'; + default: + return 'image/jpeg'; + } + } + return ''; +}; + +export const createBasicMetaTags = () => { + return [ + {property: 'og:title', content: title}, + {property: 'og:url', content: host}, + {property: 'og:site_name', content: title}, + {property: 'og:description', content: description}, + {property: 'twitter:site', content: '@spee_ch'}, + {property: 'twitter:card', content: 'summary'}, + ]; +}; + +export const createChannelMetaTags = (channel) => { + const { name, longId } = channel; + return [ + {property: 'og:title', content: `${name} on ${title}`}, + {property: 'og:url', content: `${host}/${name}:${longId}`}, + {property: 'og:site_name', content: title}, + {property: 'og:description', content: `${name}, a channel on ${title}`}, + {property: 'twitter:site', content: '@spee_ch'}, + {property: 'twitter:card', content: 'summary'}, + ]; +}; + +export const createAssetMetaTags = (asset) => { + const { claimData } = asset; + const { contentType } = claimData; + const embedUrl = `${host}/${claimData.claimId}/${claimData.name}`; + const showUrl = `${host}/${claimData.claimId}/${claimData.name}`; + const source = `${host}/${claimData.claimId}/${claimData.name}.${claimData.fileExt}`; + const ogTitle = claimData.title || claimData.name; + const ogDescription = claimData.description || defaultDescription; + const ogThumbnailContentType = determineOgThumbnailContentType(claimData.thumbnail); + const ogThumbnail = claimData.thumbnail || defaultThumbnail; + const metaTags = [ + {property: 'og:title', content: ogTitle}, + {property: 'og:url', content: showUrl}, + {property: 'og:site_name', content: title}, + {property: 'og:description', content: ogDescription}, + {property: 'og:image:width', content: 600}, + {property: 'og:image:height', content: 315}, + {property: 'twitter:site', content: '@spee_ch'}, + ]; + if (contentType === 'video/mp4' || contentType === 'video/webm') { + metaTags.push({property: 'og:video', content: source}); + metaTags.push({property: 'og:video:secure_url', content: source}); + metaTags.push({property: 'og:video:type', content: contentType}); + metaTags.push({property: 'og:image', content: ogThumbnail}); + metaTags.push({property: 'og:image:type', content: ogThumbnailContentType}); + metaTags.push({property: 'og:type', content: 'video'}); + metaTags.push({property: 'twitter:card', content: 'player'}); + metaTags.push({property: 'twitter:player', content: embedUrl}); + metaTags.push({property: 'twitter:player:width', content: 600}); + metaTags.push({property: 'twitter:text:player_width', content: 600}); + metaTags.push({property: 'twitter:player:height', content: 337}); + metaTags.push({property: 'twitter:player:stream', content: source}); + metaTags.push({property: 'twitter:player:stream:content_type', content: contentType}); + } else { + metaTags.push({property: 'og:image', content: source}); + metaTags.push({property: 'og:image:type', content: contentType}); + metaTags.push({property: 'og:type', content: 'article'}); + metaTags.push({property: 'twitter:card', content: 'summary_large_image'}); + } + return metaTags; +}; diff --git a/react/utils/pageTitle.js b/react/utils/pageTitle.js new file mode 100644 index 00000000..430f6c2a --- /dev/null +++ b/react/utils/pageTitle.js @@ -0,0 +1,8 @@ +const { site: { title: siteTitle } } = require('../../config/speechConfig.js'); + +export const createPageTitle = (pageTitle) => { + if (!pageTitle) { + return `${siteTitle}`; + } + return `${siteTitle} - ${pageTitle}`; +};