mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 03:43:28 +10:00
Merge pull request #474 from sharetribe/helmet-followup
Helmet followup
This commit is contained in:
commit
e4de0701c4
12 changed files with 85 additions and 22 deletions
|
|
@ -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">
|
||||
|
|
|
|||
14
public/static/manifest.json
Normal file
14
public/static/manifest.json
Normal 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"
|
||||
}
|
||||
BIN
public/static/webapp-icon-192x192.png
Normal file
BIN
public/static/webapp-icon-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
`
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue