mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-25 22:37:18 +10:00
Render more Helmet stuff
This commit is contained in:
parent
02ef54c898
commit
82f73a8016
26 changed files with 390 additions and 67 deletions
|
|
@ -1,10 +1,13 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html data-htmlattr="htmlAttributes">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--!title-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!--!meta-->
|
||||
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/static/favicon.ico">
|
||||
<!--!link-->
|
||||
|
||||
<style>
|
||||
/**
|
||||
|
|
@ -68,5 +71,6 @@
|
|||
<script src="https://js.stripe.com/v2/"></script>
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<!--!preloadedStateScript-->
|
||||
<!--!script-->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -13,30 +13,60 @@ const buildPath = path.resolve(__dirname, '..', 'build');
|
|||
const indexHtml = fs.readFileSync(path.join(buildPath, 'index.html'), 'utf-8');
|
||||
|
||||
const reNoMatch = /($^)/;
|
||||
const template = _.template(indexHtml, {
|
||||
// Interpolate variables in the HTML template with the following
|
||||
// syntax: <!--!variableName-->
|
||||
|
||||
// Not all the Helmet provided data is tags to be added inside <head> or <body>
|
||||
// <html> tag's attributes need separate interpolation functionality
|
||||
const templateWithHtmlAttributes = _.template(indexHtml, {
|
||||
// Interpolate htmlAttributes (Helmet data) in the HTML template with the following
|
||||
// syntax: data-htmlattr="variableName"
|
||||
//
|
||||
// This syntax is very intentional: it works as a HTML comment and
|
||||
// doesn't render anything visual in the dev mode, and in the
|
||||
// production mode, HtmlWebpackPlugin strips out comments using
|
||||
// HTMLMinifier except those that aren't explicitly marked as custom
|
||||
// comments. By default, custom comments are those that begin with a
|
||||
// ! character.
|
||||
// This syntax is very intentional: it works as a data attribute and
|
||||
// doesn't render attributes that have special meaning in HTML renderig
|
||||
// (except containing some data).
|
||||
//
|
||||
// Note that the variables are _not_ escaped since we only inject
|
||||
// HTML content.
|
||||
//
|
||||
// See:
|
||||
// - https://github.com/ampedandwired/html-webpack-plugin
|
||||
// - https://github.com/kangax/html-minifier
|
||||
// - Plugin options in the production Webpack configuration file
|
||||
interpolate: /<!--!([\s\S]+?)-->/g,
|
||||
// This special attribute should be added to <html> tag
|
||||
// It may contain attributes like lang, itemscope, and itemtype
|
||||
interpolate: /data-htmlattr=\"([\s\S]+?)\"/g,
|
||||
// Disable evaluated and escaped variables in the template
|
||||
evaluate: reNoMatch,
|
||||
escape: reNoMatch,
|
||||
});
|
||||
|
||||
// Template tags inside given template string (templatedWithHtmlAttributes),
|
||||
// which cantains <html> attributes already.
|
||||
const templateTags = templatedWithHtmlAttributes =>
|
||||
_.template(templatedWithHtmlAttributes, {
|
||||
// Interpolate variables in the HTML template with the following
|
||||
// syntax: <!--!variableName-->
|
||||
//
|
||||
// This syntax is very intentional: it works as a HTML comment and
|
||||
// doesn't render anything visual in the dev mode, and in the
|
||||
// production mode, HtmlWebpackPlugin strips out comments using
|
||||
// HTMLMinifier except those that aren't explicitly marked as custom
|
||||
// comments. By default, custom comments are those that begin with a
|
||||
// ! character.
|
||||
//
|
||||
// Note that the variables are _not_ escaped since we only inject
|
||||
// HTML content.
|
||||
//
|
||||
// See:
|
||||
// - https://github.com/ampedandwired/html-webpack-plugin
|
||||
// - https://github.com/kangax/html-minifier
|
||||
// - Plugin options in the production Webpack configuration file
|
||||
interpolate: /<!--!([\s\S]+?)-->/g,
|
||||
// Disable evaluated and escaped variables in the template
|
||||
evaluate: reNoMatch,
|
||||
escape: reNoMatch,
|
||||
});
|
||||
|
||||
// Interpolate htmlAttributes and other helmet data into the template
|
||||
const template = params => {
|
||||
const htmlAttributes = params.htmlAttributes;
|
||||
const tags = _.omit(params, ['htmlAttributes']);
|
||||
const templatedWithHtmlAttributes = templateWithHtmlAttributes({ htmlAttributes });
|
||||
return templateTags(templatedWithHtmlAttributes)(tags);
|
||||
};
|
||||
|
||||
exports.render = function(requestUrl, context, preloadedState) {
|
||||
const { head, body } = renderApp(requestUrl, context, preloadedState);
|
||||
|
||||
|
|
@ -54,5 +84,13 @@ exports.render = function(requestUrl, context, preloadedState) {
|
|||
<script>window.__PRELOADED_STATE__ = ${JSON.stringify(serializedState)};</script>
|
||||
`;
|
||||
|
||||
return template({ title: head.title.toString(), preloadedStateScript, body });
|
||||
return template({
|
||||
htmlAttributes: head.htmlAttributes.toString(),
|
||||
title: head.title.toString(),
|
||||
link: head.link.toString(),
|
||||
meta: head.meta.toString(),
|
||||
script: head.script.toString(),
|
||||
preloadedStateScript,
|
||||
body,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
BIN
src/assets/saunatimeFacebook-1200x630.jpg
Normal file
BIN
src/assets/saunatimeFacebook-1200x630.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
BIN
src/assets/saunatimeTwitter-600x314.jpg
Normal file
BIN
src/assets/saunatimeTwitter-600x314.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
|
|
@ -1,9 +1,12 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { canonicalURL, metaTagProps } from '../../util/seo';
|
||||
|
||||
import facebookImage from '../../assets/saunatimeFacebook-1200x630.jpg';
|
||||
import twitterImage from '../../assets/saunatimeTwitter-600x314.jpg';
|
||||
import css from './PageLayout.css';
|
||||
|
||||
const scrollToTop = () => {
|
||||
|
|
@ -11,7 +14,7 @@ const scrollToTop = () => {
|
|||
window.scrollTo(0, 0);
|
||||
};
|
||||
|
||||
class PageLayout extends Component {
|
||||
class PageLayoutComponent extends Component {
|
||||
componentDidMount() {
|
||||
this.historyUnlisten = this.props.history.listen(() => scrollToTop());
|
||||
}
|
||||
|
|
@ -26,11 +29,22 @@ class PageLayout extends Component {
|
|||
const {
|
||||
className,
|
||||
rootClassName,
|
||||
title,
|
||||
children,
|
||||
authInfoError,
|
||||
children,
|
||||
history,
|
||||
intl,
|
||||
logoutError,
|
||||
scrollingDisabled,
|
||||
contentType,
|
||||
description,
|
||||
facebookImages,
|
||||
published,
|
||||
schema,
|
||||
tags,
|
||||
title,
|
||||
twitterHandle,
|
||||
twitterImages,
|
||||
updated,
|
||||
} = this.props;
|
||||
|
||||
// TODO: use FlashMessages for auth errors
|
||||
|
|
@ -48,10 +62,61 @@ class PageLayout extends Component {
|
|||
[css.scrollingDisabled]: scrollingDisabled,
|
||||
});
|
||||
|
||||
const { pathname, search = '' } = history.location;
|
||||
const pathWithSearch = `${pathname}${search}`;
|
||||
|
||||
const schemaTitle = intl.formatMessage({ id: 'PageLayout.schemaTitle' });
|
||||
const schemaDescription = intl.formatMessage({ id: 'PageLayout.schemaDescription' });
|
||||
const metaTitle = title || schemaTitle;
|
||||
const metaDescription = description || schemaDescription;
|
||||
const facebookImgs = facebookImages || [
|
||||
{ name: 'facebook', url: facebookImage, width: 1200, height: 630 },
|
||||
];
|
||||
const twitterImgs = twitterImages || [
|
||||
{ name: 'twitter', url: twitterImage, width: 600, height: 314 },
|
||||
];
|
||||
|
||||
const metaToHead = metaTagProps({
|
||||
contentType,
|
||||
description: metaDescription,
|
||||
facebookImages: facebookImgs,
|
||||
twitterImages: twitterImgs,
|
||||
published,
|
||||
tags,
|
||||
title: metaTitle,
|
||||
twitterHandle,
|
||||
updated,
|
||||
url: canonicalURL(pathWithSearch),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
const metaTags = metaToHead.map((metaProps, i) => <meta key={i} {...metaProps} />);
|
||||
|
||||
// Schema for search engines (helps them to understand what this page is about)
|
||||
// http://schema.org
|
||||
// We are using JSON-LD format
|
||||
const schemaJSONString = schema ||
|
||||
JSON.stringify({
|
||||
'@context': 'http://schema.org',
|
||||
'@type': 'WebPage',
|
||||
description: schemaDescription,
|
||||
name: schemaTitle,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<Helmet>
|
||||
<Helmet
|
||||
htmlAttributes={{
|
||||
lang: intl.locale,
|
||||
}}
|
||||
>
|
||||
<title>{title}</title>
|
||||
<link rel="canonical" href={canonicalURL(pathWithSearch)} />
|
||||
{metaTags}
|
||||
<script type="application/ld+json">
|
||||
{schemaJSONString}
|
||||
</script>
|
||||
|
||||
</Helmet>
|
||||
{authInfoError
|
||||
? <div style={{ color: 'red' }}>
|
||||
|
|
@ -71,30 +136,68 @@ class PageLayout extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const { any, bool, func, instanceOf, shape, string } = PropTypes;
|
||||
const { any, arrayOf, bool, func, instanceOf, number, shape, string } = PropTypes;
|
||||
|
||||
PageLayout.defaultProps = {
|
||||
PageLayoutComponent.defaultProps = {
|
||||
className: null,
|
||||
rootClassName: null,
|
||||
children: null,
|
||||
authInfoError: null,
|
||||
logoutError: null,
|
||||
scrollingDisabled: false,
|
||||
contentType: 'website',
|
||||
description: null,
|
||||
facebookImages: null,
|
||||
twitterImages: null,
|
||||
published: null,
|
||||
schema: null,
|
||||
tags: null,
|
||||
twitterHandle: null,
|
||||
updated: null,
|
||||
};
|
||||
|
||||
PageLayout.propTypes = {
|
||||
PageLayoutComponent.propTypes = {
|
||||
className: string,
|
||||
rootClassName: string,
|
||||
title: string.isRequired,
|
||||
children: any,
|
||||
authInfoError: instanceOf(Error),
|
||||
logoutError: instanceOf(Error),
|
||||
scrollingDisabled: bool,
|
||||
|
||||
// SEO related props
|
||||
contentType: string, // og:type
|
||||
description: string, // page description
|
||||
facebookImages: arrayOf(
|
||||
shape({
|
||||
width: number.isRequired,
|
||||
height: number.isRequired,
|
||||
url: string.isRequired,
|
||||
})
|
||||
),
|
||||
twitterImages: arrayOf(
|
||||
shape({
|
||||
width: number.isRequired,
|
||||
height: number.isRequired,
|
||||
url: string.isRequired,
|
||||
})
|
||||
),
|
||||
published: string, // article:published_time
|
||||
schema: string, // http://schema.org
|
||||
tags: string, // article:tag
|
||||
title: string.isRequired, // page title
|
||||
twitterHandle: string, // twitter handle
|
||||
updated: string, // article:modified_time
|
||||
|
||||
// from withRouter
|
||||
history: shape({
|
||||
listen: func.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default withRouter(PageLayout);
|
||||
const PageLayout = injectIntl(withRouter(PageLayoutComponent));
|
||||
PageLayout.displayName = 'PageLayout';
|
||||
|
||||
export default PageLayout;
|
||||
|
|
|
|||
|
|
@ -181,6 +181,18 @@ const stripeSupportedCountries = [
|
|||
},
|
||||
];
|
||||
|
||||
// Canonical root url is needed in social media sharing and SEO optimization purposes.
|
||||
const canonicalRootURL = process.env.REACT_APP_CANONICAL_ROOT_URL || 'https://localhost:3000';
|
||||
|
||||
// Site title is needed in meta tags (bots and social media sharing reads those)
|
||||
const siteTitle = 'Saunatime';
|
||||
|
||||
// Twitter handle is needed in meta tags (twitter:site)
|
||||
const siteTwitterHandle = '@sharetribe';
|
||||
|
||||
// Facebook counts shares with app or page associated by this id
|
||||
const facebookAppId = null;
|
||||
|
||||
// NOTE: only expose configuration that should be visible in the
|
||||
// client side, don't add any server secrets in this file.
|
||||
const config = {
|
||||
|
|
@ -190,6 +202,10 @@ const config = {
|
|||
currency,
|
||||
currencyConfig,
|
||||
stripe: { publishableKey: stripePublishableKey, supportedCountries: stripeSupportedCountries },
|
||||
canonicalRootURL,
|
||||
siteTitle,
|
||||
siteTwitterHandle,
|
||||
facebookAppId,
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`AuthenticationPageComponent matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
scrollingDisabled={false}
|
||||
|
|
@ -78,5 +78,5 @@ exports[`AuthenticationPageComponent matches snapshot 1`] = `
|
|||
onSubmit={[Function]} />
|
||||
</div>
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`CheckoutPage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
title="CheckoutPage.title">
|
||||
|
|
@ -156,5 +156,5 @@ exports[`CheckoutPage matches snapshot 1`] = `
|
|||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`ContactDetailsPage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
title="Contact details">
|
||||
|
|
@ -81,5 +81,5 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
|
|||
</div>
|
||||
</ContentWrapper>
|
||||
</LayoutSideNavigation>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`EditListingPageComponent matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
scrollingDisabled={false}
|
||||
|
|
@ -65,5 +65,5 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
|
|||
"type": "new",
|
||||
}
|
||||
} />
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`InboxPage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
scrollingDisabled={false}
|
||||
|
|
@ -253,7 +253,7 @@ exports[`InboxPage matches snapshot 1`] = `
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
||||
exports[`InboxPage matches snapshot 2`] = `
|
||||
|
|
@ -309,7 +309,7 @@ exports[`InboxPage matches snapshot 2`] = `
|
|||
`;
|
||||
|
||||
exports[`InboxPage matches snapshot 3`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
scrollingDisabled={false}
|
||||
|
|
@ -563,7 +563,7 @@ exports[`InboxPage matches snapshot 3`] = `
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
||||
exports[`InboxPage matches snapshot 4`] = `
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`LandingPage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
scrollingDisabled={false}
|
||||
|
|
@ -42,5 +42,5 @@ exports[`LandingPage matches snapshot 1`] = `
|
|||
}
|
||||
rootClassName={null} />
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`ListingPage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
scrollingDisabled={false}
|
||||
|
|
@ -269,5 +269,5 @@ exports[`ListingPage matches snapshot 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`ContactDetailsPage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
scrollingDisabled={false}
|
||||
|
|
@ -35,5 +35,5 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
|
|||
<div>
|
||||
<div />
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`NotFoundPageComponent matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
title="NotFoundPage.title">
|
||||
|
|
@ -32,5 +32,5 @@ exports[`NotFoundPageComponent matches snapshot 1`] = `
|
|||
values={Object {}} />
|
||||
</h1>
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`OrderPage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
scrollingDisabled={false}
|
||||
|
|
@ -151,5 +151,5 @@ exports[`OrderPage matches snapshot 1`] = `
|
|||
"type": "transaction",
|
||||
}
|
||||
} />
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`PasswordChangePage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
title="Contact details">
|
||||
|
|
@ -81,5 +81,5 @@ exports[`PasswordChangePage matches snapshot 1`] = `
|
|||
</div>
|
||||
</ContentWrapper>
|
||||
</LayoutSideNavigation>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`ContactDetailsPage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
title="PasswordRecoveryPage.title">
|
||||
|
|
@ -51,5 +51,5 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
|
|||
recoveryError={null} />
|
||||
</div>
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`PayoutPreferencesPage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
title="Payout preferences">
|
||||
|
|
@ -25,5 +25,5 @@ exports[`PayoutPreferencesPage matches snapshot 1`] = `
|
|||
onResendVerificationEmail={[Function]}
|
||||
sendVerificationEmailError={null}
|
||||
sendVerificationEmailInProgress={false} />
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`ProfilePage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
title="Profile page with display name: most-awesome-shop">
|
||||
|
|
@ -25,5 +25,5 @@ exports[`ProfilePage matches snapshot 1`] = `
|
|||
onResendVerificationEmail={[Function]}
|
||||
sendVerificationEmailError={null}
|
||||
sendVerificationEmailInProgress={false} />
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`ContactDetailsPage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
scrollingDisabled={false}
|
||||
|
|
@ -38,5 +38,5 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
|
|||
values={Object {}} />
|
||||
</h1>
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`SalePage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
scrollingDisabled={false}
|
||||
|
|
@ -159,5 +159,5 @@ exports[`SalePage matches snapshot 1`] = `
|
|||
}
|
||||
} />
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`SearchPageComponent matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
scrollingDisabled={false}
|
||||
|
|
@ -92,5 +92,5 @@ exports[`SearchPageComponent matches snapshot 1`] = `
|
|||
</div>
|
||||
</withViewport(ModalInMobileComponent)>
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
exports[`SecurityPage matches snapshot 1`] = `
|
||||
<withRouter(PageLayout)
|
||||
<PageLayout
|
||||
authInfoError={null}
|
||||
logoutError={null}
|
||||
title="Security">
|
||||
|
|
@ -25,5 +25,5 @@ exports[`SecurityPage matches snapshot 1`] = `
|
|||
onResendVerificationEmail={[Function]}
|
||||
sendVerificationEmailError={null}
|
||||
sendVerificationEmailInProgress={false} />
|
||||
</withRouter(PageLayout)>
|
||||
</PageLayout>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -241,6 +241,8 @@
|
|||
"OrderPage.title": "Order details: {listingTitle}",
|
||||
"PageLayout.authInfoFailed": "Could not get authentication information.",
|
||||
"PageLayout.logoutFailed": "Logout failed. Please try again.",
|
||||
"PageLayout.schemaDescription": "You can book a sauna from Saunatime or get some income by sharing your own sauna",
|
||||
"PageLayout.schemaTitle": "Book saunas everywhere",
|
||||
"PaginationLinks.next": "Next page",
|
||||
"PaginationLinks.previous": "Previous page",
|
||||
"PaginationLinks.toPage": "Go to page {page}",
|
||||
|
|
|
|||
160
src/util/seo.js
Normal file
160
src/util/seo.js
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import config from '../config';
|
||||
|
||||
export const canonicalURL = path => `${config.canonicalRootURL}${path}`;
|
||||
|
||||
/**
|
||||
* These will be used with Helmet <meta {...openGraphMetaProps} />
|
||||
*/
|
||||
export const openGraphMetaProps = data => {
|
||||
const {
|
||||
canonicalRootURL,
|
||||
contentType,
|
||||
description,
|
||||
facebookAppId,
|
||||
facebookImages,
|
||||
published,
|
||||
siteTitle,
|
||||
tags,
|
||||
title,
|
||||
updated,
|
||||
url,
|
||||
} = data;
|
||||
|
||||
if (
|
||||
!(title &&
|
||||
description &&
|
||||
contentType &&
|
||||
url &&
|
||||
facebookImages &&
|
||||
facebookImages.length > 0 &&
|
||||
canonicalRootURL)
|
||||
) {
|
||||
/* eslint-disable no-console */
|
||||
if (console && console.warning) {
|
||||
console.warning(
|
||||
`Can't create Openg Graph meta tags:
|
||||
title, description, contentType, url, facebookImages, and canonicalRootURL are needed.`
|
||||
);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
return [];
|
||||
}
|
||||
|
||||
const openGraphMeta = [
|
||||
{ property: 'og:description', content: description },
|
||||
{ property: 'og:title', content: title },
|
||||
{ property: 'og:type', content: contentType },
|
||||
{ property: 'og:url', content: url },
|
||||
];
|
||||
|
||||
facebookImages.forEach(i => {
|
||||
openGraphMeta.push({
|
||||
property: 'og:image',
|
||||
content: `${canonicalRootURL}${i.url}`,
|
||||
});
|
||||
|
||||
if (i.width && i.height) {
|
||||
openGraphMeta.push({ property: 'og:image:width', content: i.width });
|
||||
openGraphMeta.push({ property: 'og:image:height', content: i.height });
|
||||
}
|
||||
});
|
||||
|
||||
if (siteTitle) {
|
||||
openGraphMeta.push({ property: 'og:site_name', content: siteTitle });
|
||||
}
|
||||
|
||||
if (facebookAppId) {
|
||||
openGraphMeta.push({ property: 'fb:app_id', content: facebookAppId });
|
||||
}
|
||||
|
||||
if (published) {
|
||||
openGraphMeta.push({ property: 'article:published_time', content: published });
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
openGraphMeta.push({ property: 'article:modified_time', content: updated });
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
openGraphMeta.push({ property: 'article:tag', content: tags });
|
||||
}
|
||||
|
||||
return openGraphMeta;
|
||||
};
|
||||
|
||||
/**
|
||||
* These will be used with Helmet <meta {...twitterMetaProps} />
|
||||
*/
|
||||
export const twitterMetaProps = data => {
|
||||
const {
|
||||
canonicalRootURL,
|
||||
description,
|
||||
siteTwitterHandle,
|
||||
title,
|
||||
twitterHandle,
|
||||
twitterImages,
|
||||
} = data;
|
||||
|
||||
if (!(title && description && siteTwitterHandle)) {
|
||||
/* eslint-disable no-console */
|
||||
if (console && console.warning) {
|
||||
console.warning(
|
||||
`Can't create twitter card meta tags:
|
||||
title, description, and siteTwitterHandle are needed.`
|
||||
);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
return [];
|
||||
}
|
||||
|
||||
const twitterMeta = [
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'twitter:title', content: title },
|
||||
{ name: 'twitter:description', content: description },
|
||||
{ name: 'twitter:site', content: siteTwitterHandle },
|
||||
];
|
||||
|
||||
if (canonicalRootURL && twitterImages && twitterImages.length > 0) {
|
||||
twitterImages.forEach(i => {
|
||||
twitterMeta.push({
|
||||
name: 'twitter:image:src',
|
||||
content: `${canonicalRootURL}${i.url}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (twitterHandle) {
|
||||
// TODO: this needs API support for listings
|
||||
twitterMeta.push({ name: 'twitter:creator', content: twitterHandle });
|
||||
}
|
||||
|
||||
return twitterMeta;
|
||||
};
|
||||
|
||||
/**
|
||||
* These will be used with Helmet <meta {...metaTagProps} />
|
||||
* Creates data for Open Graph and Twitter meta tags.
|
||||
*/
|
||||
export const metaTagProps = tagData => {
|
||||
const {
|
||||
canonicalRootURL,
|
||||
facebookAppId,
|
||||
siteTitle,
|
||||
siteTwitterHandle,
|
||||
} = config;
|
||||
|
||||
const openGraphMeta = openGraphMetaProps({
|
||||
...tagData,
|
||||
canonicalRootURL,
|
||||
facebookAppId,
|
||||
siteTitle,
|
||||
});
|
||||
|
||||
const twitterMeta = twitterMetaProps({
|
||||
...tagData,
|
||||
canonicalRootURL,
|
||||
siteTwitterHandle,
|
||||
});
|
||||
|
||||
return [...openGraphMeta, ...twitterMeta];
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue