Skip to content

Commit

Permalink
feat: opt-in alternate link consistency (#3320)
Browse files Browse the repository at this point in the history
Co-authored-by: Bobbie Goede <[email protected]>
  • Loading branch information
divine and BobbieGoede authored Feb 6, 2025
1 parent 9f47117 commit 16d8e18
Show file tree
Hide file tree
Showing 18 changed files with 186 additions and 47 deletions.
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

0 comments on commit 16d8e18

Please sign in to comment.