mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-30 18:16:48 +10:00
LandingPage, SearchPage and ListingPage specify schema
This commit is contained in:
parent
94f0bc126c
commit
5a32ac350d
10 changed files with 177 additions and 14 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ describe('SearchPageComponent', () => {
|
|||
searchInProgress: false,
|
||||
authInProgress: false,
|
||||
currentUserHasListings: false,
|
||||
intl: fakeIntl,
|
||||
isAuthenticated: false,
|
||||
onLogout: noop,
|
||||
onManageDisableScrolling: noop,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue