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

fix(nuxt): NuxtLink no default rel for relative external links #25600

Merged
merged 7 commits into from Feb 4, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/3.api/1.components/4.nuxt-link.md
Expand Up @@ -40,7 +40,13 @@ In this example, we use `<NuxtLink>` component to link to a website.

## `target` and `rel` Attributes

In this example, we use `<NuxtLink>` with `target`, `rel`, and `noRel` props.
A `rel` attribute of `noopener noreferrer` is applied by default to absolute links and links that open in new tabs.
- `noopener` solves a [security bug](https://mathiasbynens.github.io/rel-noopener/) in older browsers.
- `noreferrer` improves privacy for your users by not sending the `Referer` header to the linked site.

These defaults have no negative impact on SEO and are considered [best practice](https://developer.chrome.com/docs/lighthouse/best-practices/external-anchors-use-rel-noopener).

When you need to overwrite this behavior you can use the `rel` and `noRel` props.

```vue [app.vue]
<template>
Expand Down
23 changes: 15 additions & 8 deletions packages/nuxt/src/app/components/nuxt-link.ts
Expand Up @@ -14,7 +14,6 @@ import { nuxtLinkDefaults } from '#build/nuxt.config.mjs'

const firstNonUndefined = <T> (...args: (T | undefined)[]) => args.find(arg => arg !== undefined)

const DEFAULT_EXTERNAL_REL_ATTRIBUTE = 'noopener noreferrer'
const NuxtLinkDevKeySymbol: InjectionKey<boolean> = Symbol('nuxt-link-dev-key')

export type NuxtLinkOptions = {
Expand Down Expand Up @@ -181,7 +180,9 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
})

// Lazily check whether to.value has a protocol
const isProtocolURL = computed(() => typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true }))
const isAbsoluteUrl = computed(() => typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true }))

const hasTarget = computed(() => props.target && props.target !== '_self')

// Resolving link type
const isExternal = computed<boolean>(() => {
Expand All @@ -191,7 +192,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
}

// When `target` prop is set, link is external
if (props.target && props.target !== '_self') {
if (hasTarget.value) {
return true
}

Expand All @@ -200,7 +201,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
return false
}

return to.value === '' || isProtocolURL.value
return to.value === '' || isAbsoluteUrl.value
})

// Prefetching
Expand Down Expand Up @@ -285,7 +286,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
// converts `""` to `null` to prevent the attribute from being added as empty (`href=""`)
const href = typeof to.value === 'object'
? router.resolve(to.value)?.href ?? null
: (to.value && !props.external && !isProtocolURL.value)
: (to.value && !props.external && !isAbsoluteUrl.value)
? resolveTrailingSlashBehavior(joinURL(config.app.baseURL, to.value), router.resolve) as string
: to.value || null

Expand All @@ -294,10 +295,16 @@ export function defineNuxtLink (options: NuxtLinkOptions) {

// Resolves `rel`
checkPropConflicts(props, 'noRel', 'rel')
const rel = (props.noRel)
? null
const rel = firstNonUndefined<string | null>(
// converts `""` to `null` to prevent the attribute from being added as empty (`rel=""`)
: firstNonUndefined<string | null>(props.rel, options.externalRelAttribute, href ? DEFAULT_EXTERNAL_REL_ATTRIBUTE : '') || null
props.noRel ? '' : props.rel,
options.externalRelAttribute,
/*
* A fallback rel of `noopener noreferrer` is applied for external links or links that open in a new tab.
* This solves a reverse tabnapping security flaw in browsers pre-2021 as well as improving privacy.
*/
(isAbsoluteUrl.value || hasTarget.value) ? 'noopener noreferrer' : ''
) || null

const navigate = () => navigateTo(href, { replace: props.replace })

Expand Down