diff --git a/packages/gatsby-plugin-canonical-urls/README.md b/packages/gatsby-plugin-canonical-urls/README.md
index 1e899ae54d069..d7faf05820297 100644
--- a/packages/gatsby-plugin-canonical-urls/README.md
+++ b/packages/gatsby-plugin-canonical-urls/README.md
@@ -30,3 +30,21 @@ a `rel=canonical` e.g.
```html
```
+
+### Excluding search parameters
+
+URL search parameters are included in the canonical URL by default. If you worry about duplicate content because for example `/blog` and `/blog?tag=foobar` will be indexed separately, you should set the option `stripQueryString` to `true`. The latter will then be changed to `/blog`.
+
+```title=gatsby-config.js
+module.exports = {
+ plugins: [
+ {
+ resolve: `gatsby-plugin-canonical-urls`,
+ options: {
+ siteUrl: `https://www.example.com`,
+ stripQueryString: true,
+ },
+ },
+ ]
+}
+```
diff --git a/packages/gatsby-plugin-canonical-urls/src/__tests__/__snapshots__/gatsby-ssr.js.snap b/packages/gatsby-plugin-canonical-urls/src/__tests__/__snapshots__/gatsby-ssr.js.snap
index 9970d188a637f..d996b70a7fb84 100644
--- a/packages/gatsby-plugin-canonical-urls/src/__tests__/__snapshots__/gatsby-ssr.js.snap
+++ b/packages/gatsby-plugin-canonical-urls/src/__tests__/__snapshots__/gatsby-ssr.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Adds canonical link to head correctly creates a canonical link if siteUrl is set 1`] = `
+exports[`gatsby-plugin-canonical-urls creates a canonical link if siteUrl is set 1`] = `
[MockFunction] {
"calls": Array [
Array [
@@ -23,4 +23,50 @@ exports[`Adds canonical link to head correctly creates a canonical link if siteU
}
`;
-exports[`Adds canonical link to head correctly does not create a canonical link if siteUrl is not set 1`] = `[MockFunction]`;
+exports[`gatsby-plugin-canonical-urls does not create a canonical link if siteUrl is not set 1`] = `[MockFunction]`;
+
+exports[`gatsby-plugin-canonical-urls strips a trailing slash on the siteUrl and leaves search parameters 1`] = `
+[MockFunction] {
+ "calls": Array [
+ Array [
+ Array [
+ ,
+ ],
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+}
+`;
+
+exports[`gatsby-plugin-canonical-urls strips search paramaters if option stripQueryString is true 1`] = `
+[MockFunction] {
+ "calls": Array [
+ Array [
+ Array [
+ ,
+ ],
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+}
+`;
diff --git a/packages/gatsby-plugin-canonical-urls/src/__tests__/gatsby-browser.js b/packages/gatsby-plugin-canonical-urls/src/__tests__/gatsby-browser.js
new file mode 100644
index 0000000000000..e74fad5cfdb60
--- /dev/null
+++ b/packages/gatsby-plugin-canonical-urls/src/__tests__/gatsby-browser.js
@@ -0,0 +1,58 @@
+const { onRouteUpdate } = require(`../gatsby-browser`)
+
+describe(`gatsby-plugin-canonical-urls`, () => {
+ beforeEach(() => {
+ global.document.head.innerHTML = ``
+ })
+
+ it(`should update the href`, () => {
+ onRouteUpdate({ location: { pathname: `/hogwarts`, hash: ``, search: `` } })
+
+ expect(global.document.head.innerHTML).toMatchInlineSnapshot(
+ `""`
+ )
+ })
+ it(`should keep the hash`, () => {
+ onRouteUpdate({
+ location: { pathname: `/hogwarts`, hash: `#harry-potter`, search: `` },
+ })
+
+ expect(global.document.head.innerHTML).toMatchInlineSnapshot(
+ `""`
+ )
+ })
+ it(`shouldn't strip search parameter by default`, () => {
+ onRouteUpdate({
+ location: {
+ pathname: `/hogwarts`,
+ hash: ``,
+ search: `?house=gryffindor`,
+ },
+ })
+
+ expect(global.document.head.innerHTML).toMatchInlineSnapshot(
+ `""`
+ )
+ })
+ it(`should strip search paramaters if option stripQueryString is true`, () => {
+ const pluginOptions = {
+ stripQueryString: true,
+ }
+
+ onRouteUpdate(
+ {
+ location: {
+ pathname: `/hogwarts`,
+ hash: ``,
+ search: `?house=gryffindor`,
+ },
+ },
+ pluginOptions
+ )
+
+ expect(global.document.head.innerHTML).toMatchInlineSnapshot(
+ `""`
+ )
+ })
+})
diff --git a/packages/gatsby-plugin-canonical-urls/src/__tests__/gatsby-ssr.js b/packages/gatsby-plugin-canonical-urls/src/__tests__/gatsby-ssr.js
index fe92f2828111a..afeb7f99e2b07 100644
--- a/packages/gatsby-plugin-canonical-urls/src/__tests__/gatsby-ssr.js
+++ b/packages/gatsby-plugin-canonical-urls/src/__tests__/gatsby-ssr.js
@@ -1,6 +1,6 @@
const { onRenderBody } = require(`../gatsby-ssr`)
-describe(`Adds canonical link to head correctly`, () => {
+describe(`gatsby-plugin-canonical-urls`, () => {
it(`creates a canonical link if siteUrl is set`, async () => {
const pluginOptions = {
siteUrl: `http://someurl.com`,
@@ -20,6 +20,45 @@ describe(`Adds canonical link to head correctly`, () => {
expect(setHeadComponents).toHaveBeenCalledTimes(1)
})
+ it(`strips a trailing slash on the siteUrl and leaves search parameters`, async () => {
+ const pluginOptions = {
+ siteUrl: `http://someurl.com/`,
+ }
+ const setHeadComponents = jest.fn()
+ const pathname = `/somepost?tag=foobar`
+
+ await onRenderBody(
+ {
+ setHeadComponents,
+ pathname,
+ },
+ pluginOptions
+ )
+
+ expect(setHeadComponents).toMatchSnapshot()
+ expect(setHeadComponents).toHaveBeenCalledTimes(1)
+ })
+
+ it(`strips search paramaters if option stripQueryString is true`, async () => {
+ const pluginOptions = {
+ siteUrl: `http://someurl.com`,
+ stripQueryString: true,
+ }
+ const setHeadComponents = jest.fn()
+ const pathname = `/somepost?tag=foobar`
+
+ await onRenderBody(
+ {
+ setHeadComponents,
+ pathname,
+ },
+ pluginOptions
+ )
+
+ expect(setHeadComponents).toMatchSnapshot()
+ expect(setHeadComponents).toHaveBeenCalledTimes(1)
+ })
+
it(`does not create a canonical link if siteUrl is not set`, async () => {
const pluginOptions = {}
const setHeadComponents = jest.fn()
diff --git a/packages/gatsby-plugin-canonical-urls/src/gatsby-browser.js b/packages/gatsby-plugin-canonical-urls/src/gatsby-browser.js
index 687b4fcf54629..4fd35ae5aac0e 100644
--- a/packages/gatsby-plugin-canonical-urls/src/gatsby-browser.js
+++ b/packages/gatsby-plugin-canonical-urls/src/gatsby-browser.js
@@ -1,14 +1,22 @@
-exports.onRouteUpdate = ({ location }) => {
+exports.onRouteUpdate = (
+ { location },
+ pluginOptions = { stripQueryString: false }
+) => {
const domElem = document.querySelector(`link[rel='canonical']`)
- var existingValue = domElem.getAttribute(`href`)
- var baseProtocol = domElem.getAttribute(`data-baseProtocol`)
- var baseHost = domElem.getAttribute(`data-baseHost`)
+ const existingValue = domElem.getAttribute(`href`)
+ const baseProtocol = domElem.getAttribute(`data-baseProtocol`)
+ const baseHost = domElem.getAttribute(`data-baseHost`)
if (existingValue && baseProtocol && baseHost) {
- domElem.setAttribute(
- `href`,
- `${baseProtocol}//${baseHost}${location.pathname}${location.search}${
- location.hash
- }`
- )
+ let value = `${baseProtocol}//${baseHost}${location.pathname}`
+
+ const { stripQueryString } = pluginOptions
+
+ if (!stripQueryString) {
+ value += location.search
+ }
+
+ value += location.hash
+
+ domElem.setAttribute(`href`, `${value}`)
}
}
diff --git a/packages/gatsby-plugin-canonical-urls/src/gatsby-ssr.js b/packages/gatsby-plugin-canonical-urls/src/gatsby-ssr.js
index 87e7dd9cf75eb..74c623f055b40 100644
--- a/packages/gatsby-plugin-canonical-urls/src/gatsby-ssr.js
+++ b/packages/gatsby-plugin-canonical-urls/src/gatsby-ssr.js
@@ -6,15 +6,28 @@ exports.onRenderBody = (
pluginOptions
) => {
if (pluginOptions && pluginOptions.siteUrl) {
- const parsedUrl = url.parse(pluginOptions.siteUrl)
- const myUrl = `${pluginOptions.siteUrl}${pathname}`
+ const siteUrl = pluginOptions.siteUrl.replace(/\/$/, ``)
+ const parsed = url.parse(`${siteUrl}${pathname}`)
+ const stripQueryString =
+ typeof pluginOptions.stripQueryString !== `undefined`
+ ? pluginOptions.stripQueryString
+ : false
+
+ let pageUrl = ``
+
+ if (stripQueryString) {
+ pageUrl = `${parsed.protocol}//${parsed.host}${parsed.pathname}`
+ } else {
+ pageUrl = parsed.href
+ }
+
setHeadComponents([
,
])
}