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: opt-in alternate link consistency #3320

Merged
merged 6 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/content/docs/4.api/0.options.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,12 @@ This feature relies on [Nuxt's `experimental.typedRoutes`](https://nuxt.com/docs
Changing this will also change the paths in `locales` returned by `useI18n()`{lang="ts"}.
::

### `alternateLinkCanonicalQueries`

- type: `boolean`{lang="ts-type"}
- default: `false`{lang="ts"}
- Whether to remove non-canonical query parameters from alternate link meta tags

## customBlocks

Configure the `i18n` custom blocks of SFC.
Expand Down
35 changes: 31 additions & 4 deletions specs/basic_usage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ describe('basic usage', async () => {
i18n: {
baseUrl: 'http://localhost:3000',
skipSettingLocaleOnNavigate: undefined,
detectBrowserLanguage: undefined
detectBrowserLanguage: undefined,
experimental: {
alternateLinkCanonicalQueries: false
}
}
}
}
Expand Down Expand Up @@ -415,13 +418,35 @@ describe('basic usage', async () => {
}
})

const html = await $fetch('/?noncanonical')
const html = await $fetch('/?noncanonical&canonical')
const dom = getDom(html)
await assertLocaleHeadWithDom(dom, '#home-use-locale-head')

const links = getDataFromDom(dom, '#home-use-locale-head').link
const i18nCan = links.find(x => x.id === 'i18n-can')
expect(i18nCan.href).toContain(configDomain)
expect(dom.querySelector('#i18n-alt-fr').href).toEqual(
'https://runtime-config-domain.com/fr?noncanonical&canonical'
)

await restore()
})

test('render seo tags with `experimental.alternateLinkCanonicalQueries`', async () => {
const restore = await startServerWithRuntimeConfig({
public: {
i18n: {
experimental: {
alternateLinkCanonicalQueries: true
}
}
}
})

// head tags - alt links are updated server side
const html = await $fetch('/?noncanonical&canonical')
const dom = getDom(html)
expect(dom.querySelector('#i18n-alt-fr').href).toEqual('http://localhost:3000/fr?canonical=')

await restore()
})
Expand Down Expand Up @@ -468,8 +493,10 @@ describe('basic usage', async () => {

// Translated params are not lost on query changes
await page.locator('#params-add-query').click()
await waitForURL(page, '/nl/products/rode-mok?test=123')
expect(await page.locator('#nuxt-locale-link-en').getAttribute('href')).toEqual('/products/red-mug?test=123')
await waitForURL(page, '/nl/products/rode-mok?test=123&canonical=123')
expect(await page.locator('#nuxt-locale-link-en').getAttribute('href')).toEqual(
'/products/red-mug?test=123&canonical=123'
)

await page.locator('#params-remove-query').click()
await waitForURL(page, '/nl/products/rode-mok')
Expand Down
35 changes: 31 additions & 4 deletions specs/basic_usage_compat_4.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ describe('basic usage - compatibilityVersion: 4', async () => {
i18n: {
baseUrl: 'http://localhost:3000',
skipSettingLocaleOnNavigate: undefined,
detectBrowserLanguage: undefined
detectBrowserLanguage: undefined,
experimental: {
alternateLinkCanonicalQueries: false
}
}
}
}
Expand Down Expand Up @@ -415,13 +418,35 @@ describe('basic usage - compatibilityVersion: 4', async () => {
}
})

const html = await $fetch('/?noncanonical')
const html = await $fetch('/?noncanonical&canonical')
const dom = getDom(html)
await assertLocaleHeadWithDom(dom, '#home-use-locale-head')

const links = getDataFromDom(dom, '#home-use-locale-head').link
const i18nCan = links.find(x => x.id === 'i18n-can')
expect(i18nCan.href).toContain(configDomain)
expect(dom.querySelector('#i18n-alt-fr').href).toEqual(
'https://runtime-config-domain.com/fr?noncanonical&canonical'
)

await restore()
})

test('render seo tags with `experimental.alternateLinkCanonicalQueries`', async () => {
const restore = await startServerWithRuntimeConfig({
public: {
i18n: {
experimental: {
alternateLinkCanonicalQueries: true
}
}
}
})

// head tags - alt links are updated server side
const html = await $fetch('/?noncanonical&canonical')
const dom = getDom(html)
expect(dom.querySelector('#i18n-alt-fr').href).toEqual('http://localhost:3000/fr?canonical=')

await restore()
})
Expand Down Expand Up @@ -468,8 +493,10 @@ describe('basic usage - compatibilityVersion: 4', async () => {

// Translated params are not lost on query changes
await page.locator('#params-add-query').click()
await waitForURL(page, '/nl/products/rode-mok?test=123')
expect(await page.locator('#nuxt-locale-link-en').getAttribute('href')).toEqual('/products/red-mug?test=123')
await waitForURL(page, '/nl/products/rode-mok?test=123&canonical=123')
expect(await page.locator('#nuxt-locale-link-en').getAttribute('href')).toEqual(
'/products/red-mug?test=123&canonical=123'
)

await page.locator('#params-remove-query').click()
await waitForURL(page, '/nl/products/rode-mok')
Expand Down
43 changes: 38 additions & 5 deletions specs/experimental/switch_locale_path_link_ssr.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test, expect, describe } from 'vitest'
import { fileURLToPath } from 'node:url'
import { $fetch, setup } from '../utils'
import { getDom, gotoPath, renderPage, waitForURL } from '../helper'
import { getDom, gotoPath, renderPage, startServerWithRuntimeConfig, waitForURL } from '../helper'

await setup({
rootDir: fileURLToPath(new URL(`../fixtures/basic_usage`, import.meta.url)),
Expand All @@ -12,13 +12,15 @@ await setup({
runtimeConfig: {
public: {
i18n: {
baseUrl: ''
baseUrl: '',
alternateLinkCanonicalQueries: false
}
}
},
i18n: {
experimental: {
switchLocalePathLinkSSR: true
switchLocalePathLinkSSR: true,
alternateLinkCanonicalQueries: false
}
}
}
Expand All @@ -35,14 +37,45 @@ describe('experimental.switchLocalePathLinkSSR', async () => {

// Translated params are not lost on query changes
await page.locator('#params-add-query').click()
await waitForURL(page, '/nl/products/rode-mok?test=123')
expect(await page.locator('#switch-locale-path-link-en').getAttribute('href')).toEqual('/products/red-mug?test=123')
await waitForURL(page, '/nl/products/rode-mok?test=123&canonical=123')
expect(await page.locator('#switch-locale-path-link-en').getAttribute('href')).toEqual(
'/products/red-mug?test=123&canonical=123'
)

await page.locator('#params-remove-query').click()
await waitForURL(page, '/nl/products/rode-mok')
expect(await page.locator('#switch-locale-path-link-en').getAttribute('href')).toEqual('/products/red-mug')
})

test('respects `experimental.alternateLinkCanonicalQueries`', async () => {
const restore = await startServerWithRuntimeConfig({
public: {
i18n: {
experimental: {
switchLocalePathLinkSSR: true,
alternateLinkCanonicalQueries: true
}
}
}
})

// head tags - alt links are updated server side
const product1Html = await $fetch('/products/big-chair?test=123&canonical=123')
const product1Dom = getDom(product1Html)
expect(product1Dom.querySelector('#i18n-alt-nl').href).toEqual('/nl/products/grote-stoel?canonical=123')
expect(product1Dom.querySelector('#switch-locale-path-link-nl').href).toEqual(
'/nl/products/grote-stoel?test=123&canonical=123'
)

const product2Html = await $fetch('/nl/products/rode-mok?test=123&canonical=123')
const product2dom = getDom(product2Html)
expect(product2dom.querySelector('#i18n-alt-en').href).toEqual('/products/red-mug?canonical=123')
expect(product2dom.querySelector('#switch-locale-path-link-en').href).toEqual(
'/products/red-mug?test=123&canonical=123'
)
await restore()
})

test('dynamic parameters rendered correctly during SSR', async () => {
// head tags - alt links are updated server side
const product1Html = await $fetch('/products/big-chair')
Expand Down
2 changes: 1 addition & 1 deletion specs/fixtures/basic_usage/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useI18n, useLocaleHead } from '#i18n'

const route = useRoute()
const { t } = useI18n()
const head = useLocaleHead({ key: 'id', seo: { canonicalQueries: ['page'] } })
const head = useLocaleHead({ key: 'id', seo: { canonicalQueries: ['page', 'canonical'] } })
const title = computed(() => `Page - ${t(route.meta?.title ?? '')}`)
</script>

Expand Down
1 change: 1 addition & 0 deletions specs/fixtures/basic_usage/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default defineNuxtConfig({
locales: ['en', 'fr'],
defaultLocale: 'en',
experimental: {
alternateLinkCanonicalQueries: false,
autoImportTranslationFunctions: true
}
// debug: true,
Expand Down
2 changes: 1 addition & 1 deletion specs/fixtures/basic_usage/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ definePageMeta({
alias: ['/aliased-home-path']
})

const i18nHead = useLocaleHead({ key: 'id', seo: { canonicalQueries: ['page'] } })
const i18nHead = useLocaleHead({ key: 'id', seo: { canonicalQueries: ['page', 'canonical'] } })
useHead(() => ({
htmlAttrs: {
lang: i18nHead.value.htmlAttrs!.lang
Expand Down
4 changes: 3 additions & 1 deletion specs/fixtures/basic_usage/pages/products.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ onMounted(async () => {
<LangSwitcher />
<ul>
<li>
<NuxtLink id="params-add-query" :to="localePath({ query: { test: '123' } })">Add query</NuxtLink>
<NuxtLink id="params-add-query" :to="localePath({ query: { test: '123', canonical: '123' } })"
>Add query</NuxtLink
>
</li>
<li>
<NuxtLink id="params-remove-query" :to="localePath({ query: undefined })">Remove query</NuxtLink>
Expand Down
2 changes: 1 addition & 1 deletion specs/fixtures/basic_usage/pages/products/[slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const product = ref()
const { locale } = useI18n()
const route = useRoute()

const setI18nParams = useSetI18nParams()
const setI18nParams = useSetI18nParams({ canonicalQueries: ['canonical'] })
product.value = await $fetch(`/api/products/${route.params.slug}`)
if (product.value != null) {
const availableLocales = Object.keys(product.value.slugs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useI18n, useLocaleHead } from '#i18n'

const route = useRoute()
const { t } = useI18n()
const head = useLocaleHead({ key: 'id', seo: { canonicalQueries: ['page'] } })
const head = useLocaleHead({ key: 'id', seo: { canonicalQueries: ['page', 'canonical'] } })
const title = computed(() => `Page - ${t(route.meta?.title ?? '')}`)
</script>

Expand Down
2 changes: 1 addition & 1 deletion specs/fixtures/basic_usage_compat_4/app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ definePageMeta({
alias: ['/aliased-home-path']
})

const i18nHead = useLocaleHead({ key: 'id', seo: { canonicalQueries: ['page'] } })
const i18nHead = useLocaleHead({ key: 'id', seo: { canonicalQueries: ['page', 'canonical'] } })
useHead(() => ({
htmlAttrs: {
lang: i18nHead.value.htmlAttrs!.lang
Expand Down
4 changes: 3 additions & 1 deletion specs/fixtures/basic_usage_compat_4/app/pages/products.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ onMounted(async () => {
<LangSwitcher />
<ul>
<li>
<NuxtLink id="params-add-query" :to="localePath({ query: { test: '123' } })">Add query</NuxtLink>
<NuxtLink id="params-add-query" :to="localePath({ query: { test: '123', canonical: '123' } })"
>Add query</NuxtLink
>
</li>
<li>
<NuxtLink id="params-remove-query" :to="localePath({ query: undefined })">Remove query</NuxtLink>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const product = ref()
const { locale } = useI18n()
const route = useRoute()

const setI18nParams = useSetI18nParams()
const setI18nParams = useSetI18nParams({ canonicalQueries: ['canonical'] })
product.value = await $fetch(`/api/products/${route.params.slug}`)
if (product.value != null) {
const availableLocales = Object.keys(product.value.slugs)
Expand Down
1 change: 1 addition & 0 deletions specs/fixtures/basic_usage_compat_4/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default defineNuxtConfig({
vueI18n: './config/i18n.config.ts',
defaultLocale: 'en',
experimental: {
alternateLinkCanonicalQueries: false,
autoImportTranslationFunctions: true,
localeDetector: './localeDetector.ts'
},
Expand Down
3 changes: 2 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export const DEFAULT_OPTIONS = {
autoImportTranslationFunctions: false,
typedPages: true,
typedOptionsAndMessages: false,
generatedLocaleFilePathFormat: 'absolute'
generatedLocaleFilePathFormat: 'absolute',
alternateLinkCanonicalQueries: false
},
bundle: {
compositionOnly: true,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function useSetI18nParams(seo?: SeoAttributesOptions): SetI18nParamsFunct

// prettier-ignore
metaObject.link.push(
...getHreflangLinks(common, locales, key),
...getHreflangLinks(common, locales, key, seo),
...getCanonicalLink(common, key, seo)
)

Expand Down
Loading