Merge pull request #677 from lbryio/canonical-urls

Canonical urls
This commit is contained in:
Travis Eden 2018-11-05 11:53:13 -05:00 committed by GitHub
commit e46533046f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 158 additions and 80 deletions

View file

@ -1,9 +1,11 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import createCanonicalLink from '../../../../utils/createCanonicalLink';
const AssetPreview = ({ defaultThumbnail, claimData: { name, claimId, fileExt, contentType, thumbnail, title } }) => { const AssetPreview = ({ defaultThumbnail, claimData }) => {
const embedUrl = `/${claimId}/${name}.${fileExt}`; const { name, fileExt, contentType, thumbnail, title } = claimData;
const showUrl = `/${claimId}/${name}`; const showUrl = createCanonicalLink({ asset: { ...claimData }});
const embedUrl = `${showUrl}.${fileExt}`;
return ( return (
<Link to={showUrl} className='asset-preview'> <Link to={showUrl} className='asset-preview'>
{(() => { {(() => {

View file

@ -1,48 +1,48 @@
import React from 'react'; import React from 'react';
import SocialShareLink from '@components/SocialShareLink'; import SocialShareLink from '@components/SocialShareLink';
const AssetShareButtons = ({ host, name, shortId }) => { const AssetShareButtons = ({ assetUrl, name }) => {
return ( return (
<SocialShareLink > <SocialShareLink >
<a <a
className='link--primary' className='link--primary'
target='_blank' target='_blank'
href={`https://twitter.com/intent/tweet?text=${host}/${shortId}/${name}`} href={`https://twitter.com/intent/tweet?text=${assetUrl}`}
> >
twitter twitter
</a> </a>
<a <a
className='link--primary' className='link--primary'
target='_blank' target='_blank'
href={`https://www.facebook.com/sharer/sharer.php?u=${host}/${shortId}/${name}`} href={`https://www.facebook.com/sharer/sharer.php?u=${assetUrl}`}
> >
facebook facebook
</a> </a>
<a <a
className='link--primary' className='link--primary'
target='_blank' target='_blank'
href={`https://tumblr.com/widgets/share/tool?canonicalUrl=${host}/${shortId}/${name}`} href={`https://tumblr.com/widgets/share/tool?canonicalUrl=${assetUrl}`}
> >
tumblr tumblr
</a> </a>
<a <a
className='link--primary' className='link--primary'
target='_blank' target='_blank'
href={`https://www.reddit.com/submit?url=${host}/${shortId}/${name}&title=${name}`} href={`https://www.reddit.com/submit?url=${assetUrl}&title=${name}`}
> >
reddit reddit
</a> </a>
<a <a
className='link--primary' className='link--primary'
target='_blank' target='_blank'
href={`https://sharetomastodon.github.io/?title=${name}&url=${host}/${shortId}/${name}`} href={`https://sharetomastodon.github.io/?title=${name}&url=${assetUrl}`}
> >
mastodon mastodon
</a> </a>
<a <a
className='link--primary' className='link--primary'
target='_blank' target='_blank'
href={`https://share.diasporafoundation.org/?title=${name}&url=${host}/${shortId}/${name}`} href={`https://share.diasporafoundation.org/?title=${name}&url=${assetUrl}`}
> >
diaspora diaspora
</a> </a>

View file

@ -7,18 +7,26 @@ import SpaceBetween from '@components/SpaceBetween';
import AssetShareButtons from '@components/AssetShareButtons'; import AssetShareButtons from '@components/AssetShareButtons';
import ClickToCopy from '@components/ClickToCopy'; import ClickToCopy from '@components/ClickToCopy';
import siteConfig from '@config/siteConfig.json';
const { details: { host } } = siteConfig;
import createCanonicalLink from '../../../../utils/createCanonicalLink';
class AssetInfo extends React.Component { class AssetInfo extends React.Component {
render () { render () {
const { const { asset } = this.props;
asset: { const { claimViews, claimData: { channelName, channelShortId, description, name, fileExt, contentType, thumbnail, host } } = asset;
shortId,
claimData : {
channelName, certificateId, description, name, claimId, fileExt, contentType, thumbnail, host
},
claimViews,
}
} = this.props;
const canonicalUrl = createCanonicalLink({ asset: { ...asset.claimData, shortId: asset.shortId }});
const assetCanonicalUrl = `${host}${canonicalUrl}`;
let channelCanonicalUrl;
if (channelName) {
const channel = {
name: channelName,
shortId: channelShortId,
};
channelCanonicalUrl = `${createCanonicalLink({channel})}`;
}
return ( return (
<div> <div>
{channelName && ( {channelName && (
@ -29,7 +37,7 @@ class AssetInfo extends React.Component {
} }
content={ content={
<span className='text'> <span className='text'>
<Link to={`/${channelName}:${certificateId}`}>{channelName}</Link> <Link to={channelCanonicalUrl}>{channelName}</Link>
</span> </span>
} }
/> />
@ -58,9 +66,8 @@ class AssetInfo extends React.Component {
} }
content={ content={
<AssetShareButtons <AssetShareButtons
host={host}
name={name} name={name}
shortId={shortId} assetUrl={assetCanonicalUrl}
/> />
} }
/> />
@ -74,7 +81,7 @@ class AssetInfo extends React.Component {
content={ content={
<ClickToCopy <ClickToCopy
id={'short-link'} id={'short-link'}
value={`${host}/${shortId}/${name}`} value={assetCanonicalUrl}
/> />
} }
/> />
@ -90,12 +97,12 @@ class AssetInfo extends React.Component {
{(contentType === 'video/mp4') ? ( {(contentType === 'video/mp4') ? (
<ClickToCopy <ClickToCopy
id={'embed-text-video'} id={'embed-text-video'}
value={`<iframe src="${host}/video-embed/${name}/${claimId}" allowfullscreen="true" style="border:0" /></iframe>`} value={`<iframe src="${host}/video-embed${canonicalUrl}" allowfullscreen="true" style="border:0" /></iframe>`}
/> />
) : ( ) : (
<ClickToCopy <ClickToCopy
id={'embed-text-image'} id={'embed-text-image'}
value={`<img src="${host}/${claimId}/${name}.${fileExt}"/>`} value={`<img src="${assetCanonicalUrl}.${fileExt}"/>`}
/> />
)} )}
</div> </div>
@ -107,13 +114,13 @@ class AssetInfo extends React.Component {
<SpaceBetween> <SpaceBetween>
<a <a
className='link--primary' className='link--primary'
href={`${host}/${claimId}/${name}.${fileExt}`} href={`${assetCanonicalUrl}.${fileExt}`}
> >
Direct Link Direct Link
</a> </a>
<a <a
className={'link--primary'} className={'link--primary'}
href={`${host}/${claimId}/${name}.${fileExt}`} href={`${assetCanonicalUrl}.${fileExt}`}
download={name} download={name}
> >
Download Download

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { NavLink, withRouter } from 'react-router-dom'; import { NavLink, withRouter } from 'react-router-dom';
import NavBarChannelOptionsDropdown from '@components/NavBarChannelOptionsDropdown'; import NavBarChannelOptionsDropdown from '@components/NavBarChannelOptionsDropdown';
import createCanonicalLink from '../../../../utils/createCanonicalLink';
const VIEW = 'VIEW'; const VIEW = 'VIEW';
const LOGOUT = 'LOGOUT'; const LOGOUT = 'LOGOUT';
@ -14,6 +15,7 @@ class NavigationLinks extends React.Component {
this.props.checkForLoggedInChannel(); this.props.checkForLoggedInChannel();
} }
handleSelection (event) { handleSelection (event) {
const { history, channelName: name, channelShortId: shortId } = this.props;
const value = event.target.selectedOptions[0].value; const value = event.target.selectedOptions[0].value;
switch (value) { switch (value) {
case LOGOUT: case LOGOUT:
@ -21,7 +23,7 @@ class NavigationLinks extends React.Component {
break; break;
case VIEW: case VIEW:
// redirect to channel page // redirect to channel page
this.props.history.push(`/${this.props.channelName}:${this.props.channelLongId}`); history.push(createCanonicalLink({ channel: { name, shortId } }));
break; break;
default: default:
break; break;

View file

@ -2,12 +2,12 @@ import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import siteConfig from '@config/siteConfig.json';
import createPageTitle from '../../utils/createPageTitle'; import createPageTitle from '../../utils/createPageTitle';
import createMetaTags from '../../utils/createMetaTags'; import createMetaTags from '../../utils/createMetaTags';
import oEmbed from '../../utils/oEmbed.js'; import oEmbed from '../../utils/oEmbed.js';
import createCanonicalLink from '../../utils/createCanonicalLink'; import createCanonicalLink from '../../../../utils/createCanonicalLink';
import siteConfig from '@config/siteConfig.json';
const { details: { host } } = siteConfig; const { details: { host } } = siteConfig;
class SEO extends React.Component { class SEO extends React.Component {
@ -21,7 +21,11 @@ class SEO extends React.Component {
asset, asset,
channel, channel,
}); });
const cannonicalLink = createCanonicalLink(asset, channel, pageUri); const canonicalLink = `${host}${createCanonicalLink({
asset: asset ? { ...asset.claimData, shortId: asset.shortId } : undefined,
channel,
page: pageUri,
})}`;
// render results // render results
return ( return (
<Helmet <Helmet
@ -30,9 +34,9 @@ class SEO extends React.Component {
link={[ link={[
{ {
rel : 'canonical', rel : 'canonical',
href: cannonicalLink, href: canonicalLink,
}, },
oEmbed.json(host, cannonicalLink), oEmbed.json(host, canonicalLink),
]} ]}
/> />
); );

View file

@ -1,6 +1,7 @@
import siteConfig from '@config/siteConfig.json'; import siteConfig from '@config/siteConfig.json';
import determineContentTypeFromExtension from './determineContentTypeFromExtension'; import determineContentTypeFromExtension from './determineContentTypeFromExtension';
import createMetaTagsArray from './createMetaTagsArray'; import createMetaTagsArray from './createMetaTagsArray';
import createCanonicalLink from '../../../utils/createCanonicalLink';
const { const {
details: { details: {
@ -37,8 +38,10 @@ const determineMediaType = (contentType) => {
const createAssetMetaTags = (asset) => { const createAssetMetaTags = (asset) => {
const { claimData } = asset; const { claimData } = asset;
const { contentType } = claimData; const { contentType } = claimData;
const showUrl = `${host}/${claimData.claimId}/${claimData.name}`; const canonicalLink = createCanonicalLink({ asset: { ...asset.claimData, shortId: asset.shortId }});
const serveUrl = `${host}/${claimData.claimId}/${claimData.name}.${claimData.fileExt}`; const showUrl = `${host}${canonicalLink}`;
const serveUrl = `${showUrl}.${claimData.fileExt}`;
const ogTitle = claimData.title || claimData.name; const ogTitle = claimData.title || claimData.name;
const ogDescription = claimData.description || defaultDescription; const ogDescription = claimData.description || defaultDescription;
const ogThumbnailContentType = determineContentTypeFromExtension(claimData.thumbnail); const ogThumbnailContentType = determineContentTypeFromExtension(claimData.thumbnail);
@ -55,7 +58,7 @@ const createAssetMetaTags = (asset) => {
'fb:app_id' : '1371961932852223', 'fb:app_id' : '1371961932852223',
}; };
if (determineMediaType(contentType) === VIDEO) { if (determineMediaType(contentType) === VIDEO) {
const videoEmbedUrl = `${host}/video-embed/${claimData.name}/${claimData.claimId}`; const videoEmbedUrl = `${host}/video-embed${canonicalLink}`;
// card type tags // card type tags
metaTags['og:type'] = 'video.other'; metaTags['og:type'] = 'video.other';
metaTags['twitter:card'] = 'player'; metaTags['twitter:card'] = 'player';

View file

@ -1,39 +0,0 @@
import siteConfig from '@config/siteConfig.json';
const {
details: {
host,
},
} = siteConfig;
const createBasicCanonicalLink = (page) => {
return `${host}/${page}`;
};
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}`;
};
const createChannelCanonicalLink = (channel) => {
const { name, longId } = channel;
return `${host}/${name}:${longId}`;
};
const createCanonicalLink = (asset, channel, page) => {
if (asset) {
return createAssetCanonicalLink(asset);
}
if (channel) {
return createChannelCanonicalLink(channel);
}
return createBasicCanonicalLink(page);
};
export default createCanonicalLink;

View file

@ -1,6 +1,7 @@
import siteConfig from '@config/siteConfig.json'; import siteConfig from '@config/siteConfig.json';
import determineContentTypeFromExtension from './determineContentTypeFromExtension'; import determineContentTypeFromExtension from './determineContentTypeFromExtension';
import createMetaTagsArray from './createMetaTagsArray'; import createMetaTagsArray from './createMetaTagsArray';
import createCanonicalLink from '../../../utils/createCanonicalLink';
const { const {
details: { details: {
@ -14,7 +15,7 @@ const {
} = siteConfig; } = siteConfig;
export const createChannelMetaTags = (channel) => { export const createChannelMetaTags = (channel) => {
const { name, longId } = channel; const { name, shortId } = channel;
const metaTags = { const metaTags = {
// page detail tags // page detail tags
'og:title' : `${name} on ${siteTitle}`, 'og:title' : `${name} on ${siteTitle}`,
@ -22,7 +23,7 @@ export const createChannelMetaTags = (channel) => {
'og:description' : `${name}, a channel on ${siteTitle}`, 'og:description' : `${name}, a channel on ${siteTitle}`,
'twitter:description': `${name}, a channel on ${siteTitle}`, 'twitter:description': `${name}, a channel on ${siteTitle}`,
// url // url
'og:url' : `${host}/${name}:${longId}`, 'og:url' : `${host}/${createCanonicalLink({ channel })}`,
// site info // site info
'og:site_name' : siteTitle, 'og:site_name' : siteTitle,
'twitter:site' : twitter, 'twitter:site' : twitter,

View file

@ -69,12 +69,19 @@ const parseLogoConfigParam = async (rawConfig) => {
} }
const sendVideoEmbedPage = async ({ params }, res) => { const sendVideoEmbedPage = async ({ params }, res) => {
const { let {
claimId, claimId,
config, config,
name, name,
} = params; } = params;
// if channel then swap name and claimId for order
if (name[0] === '@' && name.includes(':')) {
const temp = name;
name = claimId;
claimId = temp;
}
const logoConfig = await parseLogoConfigParam(config); const logoConfig = await parseLogoConfigParam(config);
// test setting response headers // test setting response headers

View file

@ -34,6 +34,35 @@ function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var createCanonicalLink = require('../../../utils/createCanonicalLink');
var getCanonicalUrlFromShow = function getCanonicalUrlFromShow(show) {
var requestId = show.requestList[show.request.id];
var requestType = show.request.type;
switch (requestType) {
case 'ASSET_DETAILS':
var asset = show.assetList[requestId.key];
return createCanonicalLink({
asset: _objectSpread({}, asset.claimData, {
shortId: asset.shortId
})
});
case 'CHANNEL':
return createCanonicalLink({
channel: show.channelList[requestId.key]
});
default:
return null;
}
};
var returnSagaWithParams = function returnSagaWithParams(saga, params) { var returnSagaWithParams = function returnSagaWithParams(saga, params) {
return ( return (
/*#__PURE__*/ /*#__PURE__*/
@ -115,6 +144,14 @@ module.exports = function (req, res) {
var boundSaga = returnSagaWithParams(saga, boundAction); // run the saga middleware with the saga call var boundSaga = returnSagaWithParams(saga, boundAction); // run the saga middleware with the saga call
sagaMiddleware.run(boundSaga).done.then(function () { sagaMiddleware.run(boundSaga).done.then(function () {
// redirect if request does not use canonical url
var canonicalUrl = getCanonicalUrlFromShow(store.getState().show);
if (canonicalUrl && canonicalUrl !== req.originalUrl) {
console.log("redirecting ".concat(req.originalUrl, " to ").concat(canonicalUrl));
res.redirect(canonicalUrl);
}
return renderPage(store); return renderPage(store);
}); });
} else { } else {

View file

@ -15,12 +15,27 @@ import App from '@app';
import Sagas from '@sagas'; import Sagas from '@sagas';
import Actions from '@actions'; import Actions from '@actions';
const createCanonicalLink = require('../../../utils/createCanonicalLink');
const getCanonicalUrlFromShow = show => {
const requestId = show.requestList[show.request.id];
const requestType = show.request.type;
switch (requestType) {
case 'ASSET_DETAILS':
const asset = show.assetList[requestId.key];
return createCanonicalLink({ asset: { ...asset.claimData, shortId: asset.shortId }});
case 'CHANNEL':
return createCanonicalLink({ channel: show.channelList[requestId.key] });
default:
return null;
}
};
const returnSagaWithParams = (saga, params) => { const returnSagaWithParams = (saga, params) => {
return function * () { return function * () {
yield call(saga, params); yield call(saga, params);
}; };
}; };
module.exports = (req, res) => { module.exports = (req, res) => {
let context = {}; let context = {};
@ -90,7 +105,15 @@ module.exports = (req, res) => {
sagaMiddleware sagaMiddleware
.run(boundSaga) .run(boundSaga)
.done .done
.then(() => renderPage(store) ); .then(() => {
// redirect if request does not use canonical url
const canonicalUrl = getCanonicalUrlFromShow(store.getState().show);
if (canonicalUrl && canonicalUrl !== req.originalUrl) {
console.log(`redirecting ${req.originalUrl} to ${canonicalUrl}`);
res.redirect(canonicalUrl);
}
return renderPage(store)
});
} else { } else {
const store = createStore(Reducers); const store = createStore(Reducers);
renderPage(store); renderPage(store);

View file

@ -10,11 +10,17 @@ module.exports = async (data) => {
channelName = await chainquery.claim.queries.getClaimChannelName(certificateId).catch(()=>{}); channelName = await chainquery.claim.queries.getClaimChannelName(certificateId).catch(()=>{});
} }
let channelShortId = null;
if (certificateId && channelName) {
channelShortId = await chainquery.claim.queries.getShortClaimIdFromLongClaimId(certificateId, channelName).catch(() => null);
}
return ({ return ({
name: data.name, name: data.name,
title: data.title, title: data.title,
certificateId, certificateId,
channelName, channelName,
channelShortId,
contentType: data.content_type || data.contentType, contentType: data.content_type || data.contentType,
claimId: data.claim_id || data.claimId, claimId: data.claim_id || data.claimId,
fileExt: data.generated_extension || data.fileExt, fileExt: data.generated_extension || data.fileExt,

View file

@ -0,0 +1,25 @@
const createBasicCanonicalLink = (page) => {
return `/${page}`;
};
const createAssetCanonicalLink = (asset) => {
const { channelName, channelShortId, name, claimId, shortId } = asset;
return channelName ? `/${channelName}:${channelShortId}/${name}` : `/${shortId || claimId}/${name}`;
};
const createChannelCanonicalLink = (channel) => {
const { name, shortId } = channel;
return `/${name}:${shortId}`;
};
const createCanonicalLink = ({asset, channel, page}) => {
if (asset) {
return createAssetCanonicalLink(asset);
}
if (channel) {
return createChannelCanonicalLink(channel);
}
return createBasicCanonicalLink(page);
};
module.exports = createCanonicalLink;