LandingPage, SearchPage and ListingPage specify schema

This commit is contained in:
Vesa Luusua 2017-09-28 20:49:43 +03:00
parent 94f0bc126c
commit 5a32ac350d
10 changed files with 177 additions and 14 deletions

View file

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

View file

@ -2,13 +2,17 @@ import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { injectIntl, intlShape } from 'react-intl';
import { sendVerificationEmail } from '../../ducks/user.duck';
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
import { PageLayout, HeroSection, Topbar } from '../../components';
import * as propTypes from '../../util/propTypes';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import config from '../../config';
import facebookImage from '../../assets/saunatimeFacebook-1200x630.jpg';
import twitterImage from '../../assets/saunatimeTwitter-600x314.jpg';
import css from './LandingPage.css';
export const LandingPageComponent = props => {
@ -20,6 +24,7 @@ export const LandingPageComponent = props => {
currentUserHasOrders,
flattenedRoutes,
history,
intl,
isAuthenticated,
location,
logoutError,
@ -31,14 +36,40 @@ export const LandingPageComponent = props => {
sendVerificationEmailError,
onResendVerificationEmail,
} = 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 schemaDescription = intl.formatMessage({ id: 'LandingPage.schemaDescription' });
const schemaImage = `${config.canonicalRootURL}/${facebookImage}`;
return (
<PageLayout
className={css.root}
authInfoError={authInfoError}
logoutError={logoutError}
scrollingDisabled={scrollingDisabled}
title="Landing page"
contentType="website"
description={schemaDescription}
title={schemaTitle}
facebookImages={[{ url: facebookImage, width: 1200, height: 630 }]}
twitterImages={[{ url: twitterImage, width: 600, height: 314 }]}
schema={
`
{
"@context": "http://schema.org",
"@type": "WebPage",
"description": "${schemaDescription}",
"name": "${schemaTitle}",
"image": [
"${schemaImage}"
]
}
`
}
>
<Topbar
isAuthenticated={isAuthenticated}
authInProgress={authInProgress}
@ -99,6 +130,9 @@ LandingPageComponent.propTypes = {
// from withRouter
history: object.isRequired,
location: object.isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const mapStateToProps = state => {
@ -135,6 +169,7 @@ const mapDispatchToProps = dispatch => ({
const LandingPage = compose(
connect(mapStateToProps, mapDispatchToProps),
injectIntl,
withRouter,
withFlattenedRoutes
)(LandingPageComponent);

View file

@ -1,4 +1,5 @@
import React from 'react';
import { fakeIntl } from '../../util/test-data';
import { renderShallow } from '../../util/test-helpers';
import { LandingPageComponent } from './LandingPage';
import { RoutesProvider } from '../../components';
@ -16,6 +17,7 @@ describe('LandingPage', () => {
scrollingDisabled={false}
authInProgress={false}
currentUserHasListings={false}
intl={fakeIntl}
isAuthenticated={false}
onLogout={noop}
onManageDisableScrolling={noop}

View file

@ -1,9 +1,40 @@
exports[`LandingPage matches snapshot 1`] = `
<PageLayout
authInfoError={null}
contentType="website"
description="LandingPage.schemaDescription"
facebookImages={
Array [
Object {
"height": 630,
"url": "saunatimeFacebook-1200x630.jpg",
"width": 1200,
},
]
}
logoutError={null}
schema="
{
\"@context\": \"http://schema.org\",
\"@type\": \"WebPage\",
\"description\": \"LandingPage.schemaDescription\",
\"name\": \"LandingPage.schemaTitle\",
\"image\": [
\"https://localhost:3000/saunatimeFacebook-1200x630.jpg\"
]
}
"
scrollingDisabled={false}
title="Landing page">
title="LandingPage.schemaTitle"
twitterImages={
Array [
Object {
"height": 314,
"url": "saunatimeTwitter-600x314.jpg",
"width": 600,
},
]
}>
<Topbar
authInProgress={false}
currentUser={null}

View file

@ -330,12 +330,39 @@ export class ListingPageComponent extends Component {
</div>
: null;
const facebookImages = hasImages
? currentListing.images.map(image => {
return image.attributes.sizes.find(i => i.name === 'facebook');
})
: [];
const twitterImages = hasImages
? currentListing.images.map(image => {
return image.attributes.sizes.find(i => i.name === 'twitter');
})
: [];
const schemaImages = JSON.stringify(facebookImages.map(img => img.url));
return (
<PageLayout
authInfoError={authInfoError}
logoutError={logoutError}
title={`${title} ${formattedPrice}`}
scrollingDisabled={scrollingDisabled}
contentType="website"
description={description}
facebookImages={facebookImages}
twitterImages={twitterImages}
schema={
`
{
"@context": "http://schema.org",
"@type": "ItemPage",
"description": "${description}",
"name": "${title}",
"image": ${schemaImages}
}
`
}
>
{topbar}
<div className={listingClasses}>

View file

@ -1,9 +1,22 @@
exports[`ListingPage matches snapshot 1`] = `
<PageLayout
authInfoError={null}
contentType="website"
description="listing1 description"
facebookImages={Array []}
logoutError={null}
schema="
{
\"@context\": \"http://schema.org\",
\"@type\": \"ItemPage\",
\"description\": \"listing1 description\",
\"name\": \"listing1 title\",
\"image\": []
}
"
scrollingDisabled={false}
title="listing1 title 55">
title="listing1 title 55"
twitterImages={Array []}>
<Topbar
authInProgress={false}
currentUser={

View file

@ -1,14 +1,15 @@
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { debounce, isEqual, unionWith } from 'lodash';
import classNames from 'classnames';
import config from '../../config';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import { googleLatLngToSDKLatLng, googleBoundsToSDKBounds } from '../../util/googleMaps';
import { createResourceLocatorString } from '../../util/routes';
import { parse, stringify } from '../../util/urlHelpers';
import { createSlug, parse, stringify } from '../../util/urlHelpers';
import * as propTypes from '../../util/propTypes';
import { getListingsById } from '../../ducks/marketplaceData.duck';
import { sendVerificationEmail } from '../../ducks/user.duck';
@ -39,7 +40,7 @@ export class SearchPageComponent extends Component {
super(props);
this.state = {
isSearchMapOpenOnMobile: false,
isSearchMapOpenOnMobile: props.tab === 'map',
};
// Initiating map creates 'bounds_changes' event
@ -135,7 +136,9 @@ export class SearchPageComponent extends Component {
currentUser,
currentUserHasListings,
currentUserHasOrders,
flattenedRoutes,
history,
intl,
isAuthenticated,
listings,
location,
@ -149,7 +152,6 @@ export class SearchPageComponent extends Component {
searchInProgress,
searchListingsError,
searchParams,
tab,
sendVerificationEmailInProgress,
sendVerificationEmailError,
onResendVerificationEmail,
@ -231,6 +233,31 @@ export class SearchPageComponent extends Component {
const searchParamsForPagination = parse(location.search);
// 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 schemaDescription = intl.formatMessage({ id: 'SearchPage.schemaDescription' });
const schemaListings = listings.map((l, i) => {
const title = l.attributes.title;
const pathToItem = createResourceLocatorString('ListingPage', flattenedRoutes, {
id: l.id.uuid,
slug: createSlug(title),
});
return {
'@type': 'ListItem',
position: i,
url: `${config.canonicalRootURL}${pathToItem}`,
name: title,
};
});
const schemaMainEntity = JSON.stringify({
'@type': 'ItemList',
name: address,
itemListOrder: 'http://schema.org/ItemListOrderAscending',
itemListElement: schemaListings,
});
// N.B. openMobileMap button is sticky.
// For some reason, stickyness doesn't work on Safari, if the element is <button>
/* eslint-disable jsx-a11y/no-static-element-interactions */
@ -238,8 +265,20 @@ export class SearchPageComponent extends Component {
<PageLayout
authInfoError={authInfoError}
logoutError={logoutError}
title={`Search page: ${tab}`}
scrollingDisabled={scrollingDisabled}
description={schemaDescription}
title={schemaTitle}
schema={
`
{
"@context": "http://schema.org",
"@type": "SearchResultsPage",
"description": "${schemaDescription}",
"name": "${schemaTitle}",
"mainEntity": [${schemaMainEntity}]
}
`
}
>
<Topbar
className={css.topbar}
@ -360,6 +399,9 @@ SearchPageComponent.propTypes = {
location: shape({
search: string.isRequired,
}).isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const mapStateToProps = state => {
@ -417,6 +459,7 @@ const mapDispatchToProps = dispatch => ({
const SearchPage = compose(
connect(mapStateToProps, mapDispatchToProps),
injectIntl,
withFlattenedRoutes,
withRouter
)(SearchPageComponent);

View file

@ -34,6 +34,7 @@ describe('SearchPageComponent', () => {
searchInProgress: false,
authInProgress: false,
currentUserHasListings: false,
intl: fakeIntl,
isAuthenticated: false,
onLogout: noop,
onManageDisableScrolling: noop,

View file

@ -1,9 +1,19 @@
exports[`SearchPageComponent matches snapshot 1`] = `
<PageLayout
authInfoError={null}
description="SearchPage.schemaDescription"
logoutError={null}
schema="
{
\"@context\": \"http://schema.org\",
\"@type\": \"SearchResultsPage\",
\"description\": \"SearchPage.schemaDescription\",
\"name\": \"SearchPage.schemaTitle\",
\"mainEntity\": [{\"@type\":\"ItemList\",\"itemListOrder\":\"http://schema.org/ItemListOrderAscending\",\"itemListElement\":[]}]
}
"
scrollingDisabled={false}
title="Search page: listings">
title="SearchPage.schemaTitle">
<Topbar
authInProgress={false}
currentUser={null}

View file

@ -166,6 +166,8 @@
"InboxPage.statePending": "Pending",
"InboxPage.stateRequested": "Requested",
"InboxPage.title": "Inbox",
"LandingPage.schemaTitle": "Book saunas everywhere",
"LandingPage.schemaDescription": "You can book a sauna from Saunatime or get some income by sharing your own sauna",
"ListingCard.hostedBy": "Hosted by {authorName}.",
"ListingCard.perNight": "per night",
"ListingCard.unsupportedPrice": "({currency})",
@ -391,6 +393,8 @@
"SearchPage.loadingResults": "Loading search results…",
"SearchPage.noResults": "Could not find any listings.",
"SearchPage.openMapView": "Map view",
"SearchPage.schemaTitle": "Saunatime | search saunas everywhere",
"SearchPage.schemaDescription": "Showing search results",
"SearchPage.searchError": "Search failed. Please try again.",
"SearchResultsPanel.nextPage": "Next page",
"SearchResultsPanel.previousPage": "Previous page",