Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for query params in canonical url #1274

Merged
merged 15 commits into from Oct 18, 2021
1 change: 1 addition & 0 deletions docs/content/en/api.md
Expand Up @@ -75,6 +75,7 @@ All [Vue I18n properties and methods](http://kazupon.github.io/vue-i18n/api/#vue
The `options` object accepts these optional properties:
- `addDirAttribute` - Adds a `dir` attribute to the HTML element. Default: `false`
- `addSeoAttributes` - Adds various SEO attributes. Default: `false`
- `canonicalQueries` - An array of strings corresponding to query params you would like to include in your canonical URL. Default: `[]`

## Extension of VueI18n

Expand Down
31 changes: 27 additions & 4 deletions src/templates/head-meta.js
Expand Up @@ -4,9 +4,10 @@ import { formatMessage } from './utils-common'

/**
* @this {import('vue/types/vue').Vue}
* @param {import('../../types/vue').NuxtI18nHeadOptions} options
* @return {import('vue-meta').MetaInfo}
*/
export function nuxtI18nHead ({ addDirAttribute = false, addSeoAttributes = false } = {}) {
export function nuxtI18nHead ({ addDirAttribute = false, addSeoAttributes = false, canonicalQueries = [] } = {}) {
// Can happen when using from a global mixin.
if (!this.$i18n) {
return {}
Expand Down Expand Up @@ -124,13 +125,35 @@ export function nuxtI18nHead ({ addDirAttribute = false, addSeoAttributes = fals
name: this.getRouteBaseName()
})

const canonicalPath = currentRoute ? currentRoute.path : null
if (currentRoute) {
let href = toAbsoluteUrl(currentRoute.path, baseUrl)

if (canonicalQueries.length) {
const currentRouteQueryParams = currentRoute.query
const params = new URLSearchParams()
for (const queryParamName of canonicalQueries) {
if (queryParamName in currentRouteQueryParams) {
rchl marked this conversation as resolved.
Show resolved Hide resolved
const queryParamValue = currentRouteQueryParams[queryParamName]

if (Array.isArray(queryParamValue)) {
queryParamValue.forEach(v => params.append(queryParamName, v || ''))
} else {
params.append(queryParamName, queryParamValue || '')
rchl marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

const queryString = params.toString()

if (queryString) {
href = `${href}?${queryString}`
}
}

if (canonicalPath) {
link.push({
hid: 'i18n-can',
rel: 'canonical',
href: toAbsoluteUrl(canonicalPath, baseUrl)
href
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion test/fixture/basic/nuxt.config.js
Expand Up @@ -11,7 +11,7 @@ const config = {
// SPARenderer calls this function without having `this` as the root Vue Component
// so null-check before calling.
if (this.$nuxtI18nHead) {
return this.$nuxtI18nHead({ addSeoAttributes: true })
return this.$nuxtI18nHead({ addSeoAttributes: true, canonicalQueries: ['foo'] })
}
return {}
}
Expand Down
2 changes: 1 addition & 1 deletion test/fixture/basic/pages/about.vue
Expand Up @@ -17,7 +17,7 @@ export default {
},
/** @return {import('../../../../types/vue').NuxtI18nMeta} */
head () {
return this.$nuxtI18nHead()
return this.$nuxtI18nHead({ addSeoAttributes: true, canonicalQueries: ['page'] })
},
computed: {
/** @return {string} */
Expand Down
2 changes: 1 addition & 1 deletion test/fixture/basic/pages/locale.vue
Expand Up @@ -5,7 +5,7 @@
<script>
export default {
head () {
return this.$nuxtI18nHead({ addDirAttribute: true, addSeoAttributes: true })
return this.$nuxtI18nHead({ addDirAttribute: true, addSeoAttributes: true, canonicalQueries: ['foo'] })
}
}
</script>
2 changes: 1 addition & 1 deletion test/fixture/no-lang-switcher/pages/seo.vue
Expand Up @@ -9,7 +9,7 @@
<script>
export default {
head () {
return this.$nuxtI18nHead({ addSeoAttributes: true })
return this.$nuxtI18nHead({ addSeoAttributes: true, canonicalQueries: ['foo'] })
}
}
</script>
32 changes: 32 additions & 0 deletions test/module.test.js
Expand Up @@ -1173,6 +1173,38 @@ describe('prefix_and_default strategy', () => {
expect(links.length).toBe(1)
expect(links[0].getAttribute('href')).toBe('nuxt-app.localhost/')
})

test('canonical SEO link includes query params in canonicalQueries', async () => {
const html = await get('/?foo="bar"')
const dom = getDom(html)
const links = dom.querySelectorAll('head link[rel="canonical"]')
expect(links.length).toBe(1)
expect(links[0].getAttribute('href')).toBe('nuxt-app.localhost/?foo=%22bar%22')
})

test('canonical SEO link includes query params without values in canonicalQueries', async () => {
const html = await get('/?foo')
const dom = getDom(html)
const links = dom.querySelectorAll('head link[rel="canonical"]')
expect(links.length).toBe(1)
expect(links[0].getAttribute('href')).toBe('nuxt-app.localhost/?foo=')
})

test('canonical SEO link does not include query params not in canonicalQueries', async () => {
const html = await get('/?bar="baz"')
const dom = getDom(html)
const links = dom.querySelectorAll('head link[rel="canonical"]')
expect(links.length).toBe(1)
expect(links[0].getAttribute('href')).toBe('nuxt-app.localhost/')
})

test('canonical SEO link includes query params in canonicalQueries on page level', async () => {
const html = await get('/about-us?foo=baz&page=1')
const dom = getDom(html)
const links = dom.querySelectorAll('head link[rel="canonical"]')
expect(links.length).toBe(1)
expect(links[0].getAttribute('href')).toBe('nuxt-app.localhost/about-us?page=1')
})
})

describe('no_prefix strategy', () => {
Expand Down
5 changes: 5 additions & 0 deletions types/vue.d.ts
Expand Up @@ -24,6 +24,11 @@ interface NuxtI18nHeadOptions {
* @default false
*/
addSeoAttributes?: boolean
/**
* An array of strings corresponding to query params you would like to include in your canonical URL.
* @default []
*/
canonicalQueries?: string[]
}

type NuxtI18nMeta = Required<Pick<MetaInfo, 'htmlAttrs' | 'link' | 'meta'>>
Expand Down