From 945f6d48626ac7f06900750b2a03f2d280e26a84 Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Wed, 25 Oct 2017 13:15:49 +0300 Subject: [PATCH 1/8] Inject Google Analytics when tracker ID is in the environment --- public/index.html | 2 ++ server/renderer.js | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/public/index.html b/public/index.html index a4810318..a3bcc5f5 100644 --- a/public/index.html +++ b/public/index.html @@ -6,6 +6,8 @@ + + diff --git a/server/renderer.js b/server/renderer.js index dc3e0349..50f68e21 100644 --- a/server/renderer.js +++ b/server/renderer.js @@ -108,6 +108,23 @@ exports.render = function(requestUrl, context, preloadedState) { `; + // We want to precicely control where the analytics script is + // injected in the HTML file so we can catch all events as early as + // possible. This is why we inject the GA script separately from + // react-helmet. This script also ensures that all the GA scripts + // are added only when the proper env var is present. + // + // See: https://developers.google.com/analytics/devguides/collection/analyticsjs/#alternative_async_tracking_snippet + const googleAnalyticsScript = process.env.REACT_APP_GOOGLE_ANALYTICS_ID + ? ` + + + ` + : ''; + return template({ htmlAttributes: head.htmlAttributes.toString(), title: head.title.toString(), @@ -115,6 +132,7 @@ exports.render = function(requestUrl, context, preloadedState) { meta: head.meta.toString(), script: head.script.toString(), preloadedStateScript, + googleAnalyticsScript, body, }); }; From e82a7750491e8315d7db9ad719c2c0954c1f4511 Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Wed, 25 Oct 2017 17:01:57 +0300 Subject: [PATCH 2/8] Add Routing duck to track location changes --- src/Routes.js | 9 +++++++++ src/ducks/Routing.duck.js | 32 ++++++++++++++++++++++++++++++++ src/ducks/index.js | 8 +++++--- 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 src/ducks/Routing.duck.js diff --git a/src/Routes.js b/src/Routes.js index 40b28786..83963a9a 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import { Switch, Route, withRouter } from 'react-router-dom'; import { NotFoundPage } from './containers'; import { NamedRedirect } from './components'; +import { locationChanged } from './ducks/Routing.duck'; import * as propTypes from './util/propTypes'; import * as log from './util/log'; @@ -34,10 +35,17 @@ const callLoadData = props => { } }; +const handleLocationChanged = (dispatch, location) => { + const { pathname, search, hash } = location; + const canonicalUrl = `${pathname}${search}${hash}`; + dispatch(locationChanged(location, canonicalUrl)); +}; + class RouteComponentRenderer extends Component { componentDidMount() { // Calling loadData on initial rendering (on client side). callLoadData(this.props); + handleLocationChanged(this.props.dispatch, this.props.location); } componentWillReceiveProps(nextProps) { @@ -45,6 +53,7 @@ class RouteComponentRenderer extends Component { // This makes it possible to use loadData as default client side data loading technique. // However it is better to fetch data before location change to avoid "Loading data" state. callLoadData(nextProps); + handleLocationChanged(nextProps.dispatch, nextProps.location); } render() { diff --git a/src/ducks/Routing.duck.js b/src/ducks/Routing.duck.js new file mode 100644 index 00000000..0172b05e --- /dev/null +++ b/src/ducks/Routing.duck.js @@ -0,0 +1,32 @@ +// ================ Action types ================ // + +export const LOCATION_CHANGED = 'app/Routing/LOCATION_CHANGED'; + +// ================ Reducer ================ // + +const initialState = { + currentLocation: null, + currentCanonicalUrl: null, +}; + +export default function routingReducer(state = initialState, action = {}) { + const { type, payload } = action; + switch (type) { + case LOCATION_CHANGED: + return { + ...state, + currentLocation: payload.location, + currentCanonicalUrl: payload.canonicalUrl, + }; + + default: + return state; + } +} + +// ================ Action creators ================ // + +export const locationChanged = (location, canonicalUrl) => ({ + type: LOCATION_CHANGED, + payload: { location, canonicalUrl }, +}); diff --git a/src/ducks/index.js b/src/ducks/index.js index 183c2afa..0f451fe7 100644 --- a/src/ducks/index.js +++ b/src/ducks/index.js @@ -6,20 +6,22 @@ import { reducer as form } from 'redux-form'; import Auth from './Auth.duck'; +import EmailVerification from './EmailVerification.duck'; import FlashNotification from './FlashNotification.duck'; import LocationFilter from './LocationFilter.duck'; +import Routing from './Routing.duck'; import UI from './UI.duck'; import marketplaceData from './marketplaceData.duck'; import user from './user.duck'; -import EmailVerification from './EmailVerification.duck'; export { - form, Auth, + EmailVerification, FlashNotification, LocationFilter, + Routing, UI, + form, marketplaceData, user, - EmailVerification, }; From ceb87c2bb097387d1bf475d0b8b41e8f41c37fd7 Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Wed, 25 Oct 2017 17:03:11 +0300 Subject: [PATCH 3/8] Add analytics middleware and pluggable handlers --- src/analytics/analytics.js | 18 ++++++++++++++++++ src/analytics/handlers.js | 19 +++++++++++++++++++ src/index.js | 20 +++++++++++++++++++- src/store.js | 5 +++-- 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/analytics/analytics.js create mode 100644 src/analytics/handlers.js diff --git a/src/analytics/analytics.js b/src/analytics/analytics.js new file mode 100644 index 00000000..abfc75da --- /dev/null +++ b/src/analytics/analytics.js @@ -0,0 +1,18 @@ +import { LOCATION_CHANGED } from '../ducks/Routing.duck'; + +// Create a Redux middleware from the given analytics handlers. Each +// handler should have the following methods: +// +// - trackPageView(url): called when the URL is changed +export const createMiddleware = handlers => () => next => action => { + const { type, payload } = action; + + if (type === LOCATION_CHANGED) { + const { canonicalUrl } = payload; + handlers.forEach(handler => { + handler.trackPageView(canonicalUrl); + }); + } + + next(action); +}; diff --git a/src/analytics/handlers.js b/src/analytics/handlers.js new file mode 100644 index 00000000..f96a92a5 --- /dev/null +++ b/src/analytics/handlers.js @@ -0,0 +1,19 @@ +export class LoggingAnalyticsHandler { + trackPageView(url) { + console.log('Analytics page view:', url); + } +} + +export class GoogleAnalyticsHandler { + constructor(ga) { + if (typeof ga !== 'function') { + throw new Error('Variable `ga` missing for Google Analytics'); + } + this.ga = ga; + } + trackPageView(url) { + // https://developers.google.com/analytics/devguides/collection/analyticsjs/single-page-applications#tracking_virtual_pageviews + this.ga('set', 'page', url); + this.ga('send', 'pageview'); + } +} diff --git a/src/index.js b/src/index.js index be5c452e..3ef4df4d 100644 --- a/src/index.js +++ b/src/index.js @@ -24,6 +24,7 @@ import { authInfo } from './ducks/Auth.duck'; import { fetchCurrentUser } from './ducks/user.duck'; import routeConfiguration from './routeConfiguration'; import * as log from './util/log'; +import { LoggingAnalyticsHandler, GoogleAnalyticsHandler } from './analytics/handlers'; import './marketplaceIndex.css'; @@ -53,6 +54,22 @@ const setupStripe = () => { window.Stripe.setPublishableKey(config.stripe.publishableKey); }; +const setupAnalyticsHandlers = () => { + let handlers = []; + + // Log analytics page views and events in dev mode + if (config.dev) { + handlers.push(new LoggingAnalyticsHandler()); + } + + // Add Google Analytics handler if tracker ID is found + if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { + handlers.push(new GoogleAnalyticsHandler(window.ga)); + } + + return handlers; +}; + // If we're in a browser already, render the client application. if (typeof window !== 'undefined') { // set up logger with Sentry DSN client key and environment @@ -74,7 +91,8 @@ if (typeof window !== 'undefined') { }, ], }); - const store = configureStore(sdk, initialState); + const analyticsHandlers = setupAnalyticsHandlers(); + const store = configureStore(initialState, sdk, analyticsHandlers); setupStripe(); render(store); diff --git a/src/store.js b/src/store.js index 3180affb..b667e8f5 100644 --- a/src/store.js +++ b/src/store.js @@ -2,13 +2,14 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; import createReducer from './reducers'; +import * as analytics from './analytics/analytics'; /** * Create a new store with the given initial state. Adds Redux * middleware and enhancers. */ -export default function configureStore(sdk, initialState = {}) { - const middlewares = [thunk.withExtraArgument(sdk)]; +export default function configureStore(initialState = {}, sdk = null, analyticsHandlers = []) { + const middlewares = [thunk.withExtraArgument(sdk), analytics.createMiddleware(analyticsHandlers)]; // Enable Redux Devtools in client side dev mode. const composeEnhancers = From 5942d11cb6295724fb85fa3e4804ec311ca00e6e Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Thu, 26 Oct 2017 11:41:48 +0300 Subject: [PATCH 4/8] Use the canonical url for analytics page views --- src/Routes.js | 7 +++-- src/util/routes.js | 29 ++++++++++++++++++ src/util/routes.test.js | 65 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/src/Routes.js b/src/Routes.js index 83963a9a..fb64788a 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -8,6 +8,8 @@ import { NamedRedirect } from './components'; import { locationChanged } from './ducks/Routing.duck'; import * as propTypes from './util/propTypes'; import * as log from './util/log'; +import { canonicalRouteUrl } from './util/routes'; +import routeConfiguration from './routeConfiguration'; const { arrayOf, bool, object, func, shape, string } = PropTypes; @@ -36,9 +38,8 @@ const callLoadData = props => { }; const handleLocationChanged = (dispatch, location) => { - const { pathname, search, hash } = location; - const canonicalUrl = `${pathname}${search}${hash}`; - dispatch(locationChanged(location, canonicalUrl)); + const url = canonicalRouteUrl(routeConfiguration(), location); + dispatch(locationChanged(location, url)); }; class RouteComponentRenderer extends Component { diff --git a/src/util/routes.js b/src/util/routes.js index 8149b887..a94bc4de 100644 --- a/src/util/routes.js +++ b/src/util/routes.js @@ -88,3 +88,32 @@ export const findRouteByRouteName = (nameToFind, routes) => { } return route; }; + +/** + * Get the canonical URL from the given location + * + * @param {Array<{ route }>} routes - Route configuration as flat array + * @param {Object} location - location object from React Router + * + * @return {String} Canonical URL of the given location + * + */ +export const canonicalRouteUrl = (routes, location) => { + const { pathname, search, hash } = location; + + const matches = matchPathname(pathname, routes); + const isListingRoute = matches.length === 1 && matches[0].route.name === 'ListingPage'; + + if (isListingRoute) { + // Remove the dynamic slug from the listing page canonical URL + + const parts = pathname.split('/'); + if (parts.length !== 4) { + throw new Error('Expecting ListingPage pathname to have 4 parts'); + } + const canonicalListingPathname = `/${parts[1]}/${parts[3]}`; + return `${canonicalListingPathname}${search}${hash}`; + } + + return `${pathname}${search}${hash}`; +}; diff --git a/src/util/routes.test.js b/src/util/routes.test.js index 58a46d95..037ed2d2 100644 --- a/src/util/routes.test.js +++ b/src/util/routes.test.js @@ -4,7 +4,7 @@ import { RoutesProvider } from '../components'; import routeConfiguration from '../routeConfiguration'; import { renderDeep, renderShallow } from './test-helpers'; import * as propTypes from './propTypes'; -import { createResourceLocatorString, findRouteByRouteName } from './routes'; +import { createResourceLocatorString, findRouteByRouteName, canonicalRouteUrl } from './routes'; const { arrayOf } = PropTypes; @@ -68,4 +68,67 @@ describe('util/routes.js', () => { ); }); }); + + describe('canonicalRouteUrl', () => { + it('handles non-listing route', () => { + const routes = routeConfiguration(); + const location = { + pathname: '/', + search: '?some=value', + hash: '#and-some-hash', + }; + expect(canonicalRouteUrl(routes, location)).toEqual('/?some=value#and-some-hash'); + }); + it('handles ListingPage', () => { + const routes = routeConfiguration(); + const location = { + pathname: '/l/some-slug-here/00000000-0000-0000-0000-000000000000', + search: '', + hash: '', + }; + expect(canonicalRouteUrl(routes, location)).toEqual( + '/l/00000000-0000-0000-0000-000000000000' + ); + }); + it('handles ListingBasePage', () => { + const routes = routeConfiguration(); + const location = { + pathname: '/l', + search: '', + hash: '', + }; + expect(canonicalRouteUrl(routes, location)).toEqual('/l'); + }); + it('handles CheckoutPage', () => { + const routes = routeConfiguration(); + const location = { + pathname: '/l/some-slug-here/00000000-0000-0000-0000-000000000000/checkout', + search: '', + hash: '', + }; + expect(canonicalRouteUrl(routes, location)).toEqual( + '/l/some-slug-here/00000000-0000-0000-0000-000000000000/checkout' + ); + }); + it('handles NewListingPage', () => { + const routes = routeConfiguration(); + const location = { + pathname: '/l/new', + search: '', + hash: '', + }; + expect(canonicalRouteUrl(routes, location)).toEqual('/l/new'); + }); + it('handles ListingPageCanonical', () => { + const routes = routeConfiguration(); + const location = { + pathname: '/l/00000000-0000-0000-0000-000000000000', + search: '', + hash: '', + }; + expect(canonicalRouteUrl(routes, location)).toEqual( + '/l/00000000-0000-0000-0000-000000000000' + ); + }); + }); }); From 985be6ef7b90cbc30cd9bac7bd22466456362661 Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Thu, 26 Oct 2017 11:42:18 +0300 Subject: [PATCH 5/8] Fix missing tx prop --- src/containers/BookingDatesForm/BookingDatesForm.js | 1 + src/containers/BookingDatesForm/BookingDatesForm.test.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/containers/BookingDatesForm/BookingDatesForm.js b/src/containers/BookingDatesForm/BookingDatesForm.js index b6786fab..25b9709b 100644 --- a/src/containers/BookingDatesForm/BookingDatesForm.js +++ b/src/containers/BookingDatesForm/BookingDatesForm.js @@ -49,6 +49,7 @@ const estimatedNightlyTransaction = (bookingStart, bookingEnd, unitPrice) => { unitPrice: unitPrice, quantity: new Decimal(nightCount), lineTotal: totalPrice, + reversal: false, }, ], }, diff --git a/src/containers/BookingDatesForm/BookingDatesForm.test.js b/src/containers/BookingDatesForm/BookingDatesForm.test.js index 2095f898..8d7f1179 100644 --- a/src/containers/BookingDatesForm/BookingDatesForm.test.js +++ b/src/containers/BookingDatesForm/BookingDatesForm.test.js @@ -28,7 +28,7 @@ describe('BookingDatesForm', () => { ); expect(tree).toMatchSnapshot(); }); - it('matches snapshot with selected dates', () => { + it('matches selected dates', () => { const price = new Money(1099, 'USD'); const startDate = new Date(Date.UTC(2017, 3, 14)); const endDate = new Date(Date.UTC(2017, 3, 16)); @@ -58,6 +58,7 @@ describe('BookingDatesForm', () => { unitPrice: price, quantity: new Decimal(2), lineTotal: new Money(2198, 'USD'), + reversal: false, }, ]); }); From 482db41992a1fdecf140dc39273e322799e00406 Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Thu, 26 Oct 2017 12:46:15 +0300 Subject: [PATCH 6/8] Add docs for analytics --- docs/README.md | 1 + docs/analytics.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 docs/analytics.md diff --git a/docs/README.md b/docs/README.md index 0476533f..bcb8f37c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,3 +14,4 @@ Documentation for specific topics can be found in the following files: - [Error logging with Sentry](sentry.md) - [CI](ci.md) - [Static pages](static-pages.md) + - [Analytics](analytics.md) diff --git a/docs/analytics.md b/docs/analytics.md new file mode 100644 index 00000000..636a140b --- /dev/null +++ b/docs/analytics.md @@ -0,0 +1,43 @@ +# Analytics + +The application supports tracking page views with pluggable analytics +handlers. + +## Google Analytics + +Google Analytics (GA) is supported by default. The enable GA, set the +GA tracker ID to the environment variable +`REACT_APP_GOOGLE_ANALYTICS_ID`; + +## Custom analytics handler + +If you want to add a new analytics library, you can do as follows: + +1. Add the analytics library script + + If the analytics library has an external script, add the library + script tag to the [src/public/index.html](../public/index.html) + file. If you need more control, see how the GA script is added in + [server/renderer.js](../server/renderer.js). + +1. Create a handler + + To track page views, create a custom handler e.g. in + [src/analytics/handlers.js](../src/analytics/handlers.js). The + handler should be a class that implements a `trackPageView(url)` + method. + + Note that the `url` parameter might not be the same as in the URL + bar of the browser. It is the canonical form of the URL. For + example, in the listing page it doesn't have the dynamic title slug + in the middle. This allows unified analytics and correct tracking + of pages that can be accessed from multiple different URLs. + + If you analytics library takes the page URL from the browser, you + might need to override that behavior to use the canonical URL that + is given to the method. + +1. Initialise the handler + + Initialise the handler in the `setupAnalyticsHandlers()` function + in [src/index.js](../src/index.js). From a798d9a741dc62c4a658558ebb630d0b5050db00 Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Thu, 26 Oct 2017 14:12:45 +0300 Subject: [PATCH 7/8] Rename canonicalRouteUrl to canonicalRoutePath --- src/Routes.js | 4 ++-- src/util/routes.js | 2 +- src/util/routes.test.js | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Routes.js b/src/Routes.js index fb64788a..8452babb 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -8,7 +8,7 @@ import { NamedRedirect } from './components'; import { locationChanged } from './ducks/Routing.duck'; import * as propTypes from './util/propTypes'; import * as log from './util/log'; -import { canonicalRouteUrl } from './util/routes'; +import { canonicalRoutePath } from './util/routes'; import routeConfiguration from './routeConfiguration'; const { arrayOf, bool, object, func, shape, string } = PropTypes; @@ -38,7 +38,7 @@ const callLoadData = props => { }; const handleLocationChanged = (dispatch, location) => { - const url = canonicalRouteUrl(routeConfiguration(), location); + const url = canonicalRoutePath(routeConfiguration(), location); dispatch(locationChanged(location, url)); }; diff --git a/src/util/routes.js b/src/util/routes.js index a94bc4de..599a4c96 100644 --- a/src/util/routes.js +++ b/src/util/routes.js @@ -98,7 +98,7 @@ export const findRouteByRouteName = (nameToFind, routes) => { * @return {String} Canonical URL of the given location * */ -export const canonicalRouteUrl = (routes, location) => { +export const canonicalRoutePath = (routes, location) => { const { pathname, search, hash } = location; const matches = matchPathname(pathname, routes); diff --git a/src/util/routes.test.js b/src/util/routes.test.js index 037ed2d2..9c5aae90 100644 --- a/src/util/routes.test.js +++ b/src/util/routes.test.js @@ -4,7 +4,7 @@ import { RoutesProvider } from '../components'; import routeConfiguration from '../routeConfiguration'; import { renderDeep, renderShallow } from './test-helpers'; import * as propTypes from './propTypes'; -import { createResourceLocatorString, findRouteByRouteName, canonicalRouteUrl } from './routes'; +import { createResourceLocatorString, findRouteByRouteName, canonicalRoutePath } from './routes'; const { arrayOf } = PropTypes; @@ -69,7 +69,7 @@ describe('util/routes.js', () => { }); }); - describe('canonicalRouteUrl', () => { + describe('canonicalRoutePath', () => { it('handles non-listing route', () => { const routes = routeConfiguration(); const location = { @@ -77,7 +77,7 @@ describe('util/routes.js', () => { search: '?some=value', hash: '#and-some-hash', }; - expect(canonicalRouteUrl(routes, location)).toEqual('/?some=value#and-some-hash'); + expect(canonicalRoutePath(routes, location)).toEqual('/?some=value#and-some-hash'); }); it('handles ListingPage', () => { const routes = routeConfiguration(); @@ -86,7 +86,7 @@ describe('util/routes.js', () => { search: '', hash: '', }; - expect(canonicalRouteUrl(routes, location)).toEqual( + expect(canonicalRoutePath(routes, location)).toEqual( '/l/00000000-0000-0000-0000-000000000000' ); }); @@ -97,7 +97,7 @@ describe('util/routes.js', () => { search: '', hash: '', }; - expect(canonicalRouteUrl(routes, location)).toEqual('/l'); + expect(canonicalRoutePath(routes, location)).toEqual('/l'); }); it('handles CheckoutPage', () => { const routes = routeConfiguration(); @@ -106,7 +106,7 @@ describe('util/routes.js', () => { search: '', hash: '', }; - expect(canonicalRouteUrl(routes, location)).toEqual( + expect(canonicalRoutePath(routes, location)).toEqual( '/l/some-slug-here/00000000-0000-0000-0000-000000000000/checkout' ); }); @@ -117,7 +117,7 @@ describe('util/routes.js', () => { search: '', hash: '', }; - expect(canonicalRouteUrl(routes, location)).toEqual('/l/new'); + expect(canonicalRoutePath(routes, location)).toEqual('/l/new'); }); it('handles ListingPageCanonical', () => { const routes = routeConfiguration(); @@ -126,7 +126,7 @@ describe('util/routes.js', () => { search: '', hash: '', }; - expect(canonicalRouteUrl(routes, location)).toEqual( + expect(canonicalRoutePath(routes, location)).toEqual( '/l/00000000-0000-0000-0000-000000000000' ); }); From d7a554ff70f73bc11970a00e5d7beb67627c855e Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Thu, 26 Oct 2017 14:19:04 +0300 Subject: [PATCH 8/8] Handle canonical URLs within Page --- src/components/Page/Page.js | 20 +++++++++---------- src/containers/ListingPage/ListingPage.js | 9 --------- .../ListingPage/ListingPage.test.js | 12 ++++++++--- .../__snapshots__/ListingPage.test.js.snap | 1 - src/util/seo.js | 2 -- 5 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/components/Page/Page.js b/src/components/Page/Page.js index e9367626..fda88e49 100644 --- a/src/components/Page/Page.js +++ b/src/components/Page/Page.js @@ -4,8 +4,10 @@ import Helmet from 'react-helmet'; import { withRouter } from 'react-router-dom'; import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; import classNames from 'classnames'; +import routeConfiguration from '../../routeConfiguration'; import config from '../../config'; -import { canonicalURL, metaTagProps } from '../../util/seo'; +import { metaTagProps } from '../../util/seo'; +import { canonicalRoutePath } from '../../util/routes'; import * as propTypes from '../../util/propTypes'; import facebookImage from '../../assets/saunatimeFacebook-1200x630.jpg'; @@ -55,7 +57,7 @@ class PageComponent extends Component { className, rootClassName, children, - history, + location, intl, logoutError, scrollingDisabled, @@ -69,7 +71,6 @@ class PageComponent extends Component { title, twitterHandle, twitterImages, - canonicalPath, updated, } = this.props; @@ -85,10 +86,10 @@ class PageComponent extends Component { [css.scrollingDisabled]: scrollingDisabled, }); - const { pathname, search = '' } = history.location; - const pathWithSearch = `${pathname}${search}`; - const canonicalRootURL = config.canonicalRootURL; + const canonicalPath = canonicalRoutePath(routeConfiguration(), location); + const canonicalUrl = `${canonicalRootURL}${canonicalPath}`; + const siteTitle = config.siteTitle; const schemaTitle = intl.formatMessage({ id: 'Page.schemaTitle' }, { siteTitle }); const schemaDescription = intl.formatMessage({ id: 'Page.schemaDescription' }); @@ -122,7 +123,7 @@ class PageComponent extends Component { title: metaTitle, twitterHandle, updated, - url: canonicalURL(pathWithSearch), + url: canonicalUrl, locale: intl.locale, }); @@ -174,7 +175,7 @@ class PageComponent extends Component { }} > {title} - + {metaTags} @@ -204,7 +205,6 @@ PageComponent.defaultProps = { description: null, facebookImages: null, twitterImages: null, - canonicalPath: null, published: null, schema: null, tags: null, @@ -243,12 +243,12 @@ PageComponent.propTypes = { title: string.isRequired, // page title twitterHandle: string, // twitter handle updated: string, // article:modified_time - canonicalPath: string, // link rel=canonical // from withRouter history: shape({ listen: func.isRequired, }).isRequired, + location: object.isRequired, // from injectIntl intl: intlShape.isRequired, diff --git a/src/containers/ListingPage/ListingPage.js b/src/containers/ListingPage/ListingPage.js index b9e9ff90..cb9dd6e5 100644 --- a/src/containers/ListingPage/ListingPage.js +++ b/src/containers/ListingPage/ListingPage.js @@ -330,14 +330,6 @@ export class ListingPageComponent extends Component { { title, price: formattedPrice, siteTitle } ); - const canonicalPath = createResourceLocatorString( - 'ListingPageCanonical', - routeConfiguration(), - { - id: listingId.uuid, - } - ); - return ( null; describe('ListingPage', () => { it('matches snapshot', () => { const currentUser = createCurrentUser('user-2'); - const listing1 = createListing('listing1', {}, { author: createUser('user-1') }); + const id = 'listing1'; + const slug = 'listing1-title'; + const listing1 = createListing(id, {}, { author: createUser('user-1') }); const getListing = () => listing1; const props = { - location: { search: '' }, + location: { + pathname: `/l/${slug}/${id}`, + search: '', + hash: '', + }, history: { push: () => console.log('HistoryPush called'), }, - params: { slug: 'listing1-title', id: 'listing1' }, + params: { id, slug }, currentUser, getListing: getListing, intl: fakeIntl, diff --git a/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap b/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap index 9d0a7e74..792fb615 100644 --- a/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap +++ b/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap @@ -3,7 +3,6 @@ exports[`ListingPage matches snapshot 1`] = ` `${config.canonicalRootURL}${path}`; - const ensureOpenGraphLocale = locale => { switch (locale) { case 'en':