Merge pull request #474 from sharetribe/helmet-followup

Helmet followup
This commit is contained in:
Vesa Luusua 2017-10-06 15:04:11 +03:00 committed by GitHub
commit e4de0701c4
12 changed files with 85 additions and 22 deletions

View file

@ -6,7 +6,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--!meta-->
<link rel="shortcut icon" href="%PUBLIC_URL%/static/favicon.ico">
<link rel="manifest" href="%PUBLIC_URL%/static/manifest.json">
<link rel="icon" sizes="32x32" type="image/x-icon" href="%PUBLIC_URL%/static/favicon.ico">
<link rel="icon" sizes="192x192" type="image/png" href="%PUBLIC_URL%/static/webapp-icon-192x192.png" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/static/touch-icon-iphone-60x60.png">
<link rel="apple-touch-icon" sizes="57x57" href="%PUBLIC_URL%/static/apple-touch-icon-ipad-57x57.png">
<link rel="apple-touch-icon" sizes="114x114" href="%PUBLIC_URL%/static/apple-touch-icon-ipad-57x57@2x.png">

View file

@ -0,0 +1,14 @@
{
"name": "Saunatime",
"icons": [
{
"src": "/static/webapp-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"start_url": "/",
"display": "standalone",
"theme_color": "#C0392B",
"background_color": "#FFFFFF"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -34,8 +34,8 @@ describe('Application', () => {
it('server renders pages that do not require authentication', () => {
const urlTitles = {
'/': 'Book saunas everywhere',
'/s': 'Saunatime | search saunas everywhere',
'/': 'Book saunas everywhere | Saunatime',
'/s': 'Search results for map search | Saunatime',
'/l/listing-title-slug/1234': 'Loading listing…',
'/u/1234': 'Profile page with display name: 1234',
'/login': 'Login',

View file

@ -3,6 +3,7 @@ import Helmet from 'react-helmet';
import { withRouter } from 'react-router-dom';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
import config from '../../config';
import { canonicalURL, metaTagProps } from '../../util/seo';
import facebookImage from '../../assets/saunatimeFacebook-1200x630.jpg';
@ -48,6 +49,7 @@ class PageComponent extends Component {
intl,
logoutError,
scrollingDisabled,
author,
contentType,
description,
facebookImages,
@ -78,18 +80,20 @@ class PageComponent extends Component {
const { pathname, search = '' } = history.location;
const pathWithSearch = `${pathname}${search}`;
const schemaTitle = intl.formatMessage({ id: 'Page.schemaTitle' });
const siteTitle = config.siteTitle;
const schemaTitle = intl.formatMessage({ id: 'Page.schemaTitle' }, { siteTitle });
const schemaDescription = intl.formatMessage({ id: 'Page.schemaDescription' });
const metaTitle = title || schemaTitle;
const metaDescription = description || schemaDescription;
const facebookImgs = facebookImages || [
{ name: 'facebook', url: facebookImage, width: 1200, height: 630 },
{ name: 'facebook', url: `${config.canonicalRootURL}${facebookImage}`, width: 1200, height: 630 },
];
const twitterImgs = twitterImages || [
{ name: 'twitter', url: twitterImage, width: 600, height: 314 },
{ name: 'twitter', url: `${config.canonicalRootURL}${twitterImage}`, width: 600, height: 314 },
];
const metaToHead = metaTagProps({
author,
contentType,
description: metaDescription,
facebookImages: facebookImgs,
@ -100,6 +104,7 @@ class PageComponent extends Component {
twitterHandle,
updated,
url: canonicalURL(pathWithSearch),
locale: intl.locale,
});
// eslint-disable-next-line react/no-array-index-key
@ -125,6 +130,8 @@ class PageComponent extends Component {
>
<title>{title}</title>
<link rel="canonical" href={canonicalURL(pathWithSearch)} />
<meta httpEquiv="Content-Type" content="text/html; charset=UTF-8" />
<meta httpEquiv="Content-Language" content={intl.locale} />
{metaTags}
<script type="application/ld+json">
{schemaJSONString}
@ -158,6 +165,7 @@ PageComponent.defaultProps = {
authInfoError: null,
logoutError: null,
scrollingDisabled: false,
author: null,
contentType: 'website',
description: null,
facebookImages: null,
@ -178,6 +186,7 @@ PageComponent.propTypes = {
scrollingDisabled: bool,
// SEO related props
author: string,
contentType: string, // og:type
description: string, // page description
facebookImages: arrayOf(

View file

@ -40,7 +40,8 @@ export const LandingPageComponent = props => {
// Schema for search engines (helps them to understand what this page is about)
// http://schema.org
// We are using JSON-LD format
const schemaTitle = intl.formatMessage({ id: 'LandingPage.schemaTitle' });
const siteTitle = config.siteTitle;
const schemaTitle = intl.formatMessage({ id: 'LandingPage.schemaTitle' }, { siteTitle });
const schemaDescription = intl.formatMessage({ id: 'LandingPage.schemaDescription' });
const schemaImage = `${config.canonicalRootURL}/${facebookImage}`;

View file

@ -335,13 +335,19 @@ export class ListingPageComponent extends Component {
})
: [];
const schemaImages = JSON.stringify(facebookImages.map(img => img.url));
const siteTitle = config.siteTitle;
const schemaTitle = intl.formatMessage(
{ id: 'ListingPage.schemaTitle' },
{ title, price: formattedPrice, siteTitle }
);
return (
<Page
authInfoError={authInfoError}
logoutError={logoutError}
title={`${title} ${formattedPrice}`}
title={schemaTitle}
scrollingDisabled={scrollingDisabled}
author={currentAuthorDisplayName}
contentType="website"
description={description}
facebookImages={facebookImages}
@ -352,7 +358,7 @@ export class ListingPageComponent extends Component {
"@context": "http://schema.org",
"@type": "ItemPage",
"description": "${description}",
"name": "${title}",
"name": "${schemaTitle}",
"image": ${schemaImages}
}
`

View file

@ -1,6 +1,7 @@
exports[`ListingPage matches snapshot 1`] = `
<Page
authInfoError={null}
author="user-1 display name"
contentType="website"
description="listing1 description"
facebookImages={Array []}
@ -10,12 +11,12 @@ exports[`ListingPage matches snapshot 1`] = `
\"@context\": \"http://schema.org\",
\"@type\": \"ItemPage\",
\"description\": \"listing1 description\",
\"name\": \"listing1 title\",
\"name\": \"ListingPage.schemaTitle\",
\"image\": []
}
"
scrollingDisabled={false}
title="listing1 title 55"
title="ListingPage.schemaTitle"
twitterImages={Array []}>
<Topbar
authInProgress={false}

View file

@ -236,7 +236,12 @@ export class SearchPageComponent extends Component {
// Schema for search engines (helps them to understand what this page is about)
// http://schema.org
// We are using JSON-LD format
const schemaTitle = intl.formatMessage({ id: 'SearchPage.schemaTitle' });
const siteTitle = config.siteTitle;
const searchAddress = address || intl.formatMessage({ id: 'SearchPage.schemaMapSearch' });
const schemaTitle = intl.formatMessage(
{ id: 'SearchPage.schemaTitle' },
{ searchAddress, siteTitle }
);
const schemaDescription = intl.formatMessage({ id: 'SearchPage.schemaDescription' });
const schemaListings = listings.map((l, i) => {
const title = l.attributes.title;
@ -253,7 +258,7 @@ export class SearchPageComponent extends Component {
});
const schemaMainEntity = JSON.stringify({
'@type': 'ItemList',
name: address,
name: searchAddress,
itemListOrder: 'http://schema.org/ItemListOrderAscending',
itemListElement: schemaListings,
});

View file

@ -9,7 +9,7 @@ exports[`SearchPageComponent matches snapshot 1`] = `
\"@type\": \"SearchResultsPage\",
\"description\": \"SearchPage.schemaDescription\",
\"name\": \"SearchPage.schemaTitle\",
\"mainEntity\": [{\"@type\":\"ItemList\",\"itemListOrder\":\"http://schema.org/ItemListOrderAscending\",\"itemListElement\":[]}]
\"mainEntity\": [{\"@type\":\"ItemList\",\"name\":\"SearchPage.schemaMapSearch\",\"itemListOrder\":\"http://schema.org/ItemListOrderAscending\",\"itemListElement\":[]}]
}
"
scrollingDisabled={false}

View file

@ -171,7 +171,7 @@
"InboxPage.stateRequested": "Requested",
"InboxPage.title": "Inbox",
"LandingPage.schemaDescription": "You can book a sauna from Saunatime or get some income by sharing your own sauna",
"LandingPage.schemaTitle": "Book saunas everywhere",
"LandingPage.schemaTitle": "Book saunas everywhere | {siteTitle}",
"ListingCard.hostedBy": "Hosted by {authorName}.",
"ListingCard.perNight": "per night",
"ListingCard.unsupportedPrice": "({currency})",
@ -193,6 +193,7 @@
"ListingPage.ownClosedListing": "Your listing has been closed and can't be booked.",
"ListingPage.ownListing": "This is your own listing.",
"ListingPage.perNight": "per night",
"ListingPage.schemaTitle": "{title} - {price} | {siteTitle}",
"ListingPage.viewImagesButton": "View photos ({count})",
"LocationSearchForm.placeholder": "Search saunas…",
"LoginForm.emailInvalid": "A valid email address is required",
@ -253,7 +254,7 @@
"Page.authInfoFailed": "Could not get authentication information.",
"Page.logoutFailed": "Logout failed. Please try again.",
"Page.schemaDescription": "You can book a sauna from Saunatime or get some income by sharing your own sauna",
"Page.schemaTitle": "Book saunas everywhere",
"Page.schemaTitle": "Book saunas everywhere | {siteTitle}",
"PaginationLinks.next": "Next page",
"PaginationLinks.previous": "Previous page",
"PaginationLinks.toPage": "Go to page {page}",
@ -407,8 +408,9 @@
"SearchPage.loadingResults": "Loading search results…",
"SearchPage.noResults": "Could not find any listings.",
"SearchPage.openMapView": "Map view",
"SearchPage.schemaMapSearch": "map search",
"SearchPage.schemaDescription": "Showing search results",
"SearchPage.schemaTitle": "Saunatime | search saunas everywhere",
"SearchPage.schemaTitle": "Search results for {searchAddress} | {siteTitle}",
"SearchPage.searchError": "Search failed. Please try again.",
"SearchResultsPanel.nextPage": "Next page",
"SearchResultsPanel.previousPage": "Previous page",

View file

@ -2,6 +2,15 @@ import config from '../config';
export const canonicalURL = path => `${config.canonicalRootURL}${path}`;
const ensureOpenGraphLocale = locale => {
switch (locale) {
case 'en':
return 'en_US';
default:
return locale;
}
};
/**
* These will be used with Helmet <meta {...openGraphMetaProps} />
*/
@ -12,6 +21,7 @@ export const openGraphMetaProps = data => {
description,
facebookAppId,
facebookImages,
locale,
published,
siteTitle,
tags,
@ -45,12 +55,13 @@ export const openGraphMetaProps = data => {
{ property: 'og:title', content: title },
{ property: 'og:type', content: contentType },
{ property: 'og:url', content: url },
{ property: 'og:locale', content: ensureOpenGraphLocale(locale) },
];
facebookImages.forEach(i => {
openGraphMeta.push({
property: 'og:image',
content: `${canonicalRootURL}${i.url}`,
content: i.url,
});
if (i.width && i.height) {
@ -93,14 +104,15 @@ export const twitterMetaProps = data => {
title,
twitterHandle,
twitterImages,
url,
} = data;
if (!(title && description && siteTwitterHandle)) {
if (!(title && description && siteTwitterHandle && url)) {
/* eslint-disable no-console */
if (console && console.warning) {
console.warning(
`Can't create twitter card meta tags:
title, description, and siteTwitterHandle are needed.`
title, description, siteTwitterHandle, and url are needed.`
);
}
/* eslint-enable no-console */
@ -112,13 +124,14 @@ export const twitterMetaProps = data => {
{ name: 'twitter:title', content: title },
{ name: 'twitter:description', content: description },
{ name: 'twitter:site', content: siteTwitterHandle },
{ name: 'twitter:url', content: url },
];
if (canonicalRootURL && twitterImages && twitterImages.length > 0) {
twitterImages.forEach(i => {
twitterMeta.push({
name: 'twitter:image:src',
content: `${canonicalRootURL}${i.url}`,
content: i.url,
});
});
}
@ -129,6 +142,10 @@ export const twitterMetaProps = data => {
twitterMeta.push({ name: 'twitter:creator', content: twitterHandle });
}
if (canonicalRootURL) {
twitterMeta.push({ name: 'twitter:domain', content: canonicalRootURL });
}
return twitterMeta;
};
@ -144,6 +161,12 @@ export const metaTagProps = tagData => {
siteTwitterHandle,
} = config;
const author = tagData.author || siteTitle;
const defaultMeta = [
{ name: 'description', content: tagData.description },
{ name: 'author', content: author },
];
const openGraphMeta = openGraphMetaProps({
...tagData,
canonicalRootURL,
@ -157,5 +180,5 @@ export const metaTagProps = tagData => {
siteTwitterHandle,
});
return [...openGraphMeta, ...twitterMeta];
return [...defaultMeta, ...openGraphMeta, ...twitterMeta];
};