diff --git a/docs/3.api/1.components/4.nuxt-link.md b/docs/3.api/1.components/4.nuxt-link.md index 7c2c0f6e29..eda9da9757 100644 --- a/docs/3.api/1.components/4.nuxt-link.md +++ b/docs/3.api/1.components/4.nuxt-link.md @@ -25,6 +25,23 @@ In this example, we use `` component to link to another page of the ap ``` +### Handling 404s + +When using `` for `/public` directory files or when pointing to a different app on the same domain, you should use the `external` prop. + +Using `external` forces the link to be rendered as an `a` tag instead of a Vue Router `RouterLink`. + +```vue [pages/index.vue] + +``` + +The external logic is applied by default when using absolute URLs and when providing a `target` prop. + ## External Routing In this example, we use `` component to link to a website. @@ -68,19 +85,30 @@ In this example, we use `` with `target`, `rel`, and `noRel` props. ## Props +### RouterLink + +When not using `external`, `` supports all Vue Router's [`RouterLink` props](https://router.vuejs.org/api/interfaces/RouterLinkProps.html) + - `to`: Any URL or a [route location object](https://router.vuejs.org/api/interfaces/RouteLocation.html) from Vue Router -- `href`: An alias for `to`. If used with `to`, `href` will be ignored -- `target`: A `target` attribute value to apply on the link -- `rel`: A `rel` attribute value to apply on the link. Defaults to `"noopener noreferrer"` for external links. -- `noRel`: If set to `true`, no `rel` attribute will be added to the link -- `activeClass`: A class to apply on active links. Works the same as [Vue Router's `active-class` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-activeClass) on internal links. Defaults to Vue Router's default (`"router-link-active"`) +- `custom`: Whether `` should wrap its content in an `` element. It allows taking full control of how a link is rendered and how navigation works when it is clicked. Works the same as [Vue Router's `custom` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-custom) - `exactActiveClass`: A class to apply on exact active links. Works the same as [Vue Router's `exact-active-class` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-exactActiveClass) on internal links. Defaults to Vue Router's default `"router-link-exact-active"`) - `replace`: Works the same as [Vue Router's `replace` prop](https://router.vuejs.org/api/interfaces/RouteLocationOptions.html#Properties-replace) on internal links - `ariaCurrentValue`: An `aria-current` attribute value to apply on exact active links. Works the same as [Vue Router's `aria-current-value` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-ariaCurrentValue) on internal links -- `external`: Forces the link to be considered as external (`true`) or internal (`false`). This is helpful to handle edge-cases -- `prefetch` and **noPrefetch**: Whether to enable prefetching assets for links that enter the view port. +- `activeClass`: A class to apply on active links. Works the same as [Vue Router's `active-class` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-activeClass) on internal links. Defaults to Vue Router's default (`"router-link-active"`) + +### NuxtLink + +- `href`: An alias for `to`. If used with `to`, `href` will be ignored +- `noRel`: If set to `true`, no `rel` attribute will be added to the link +- `external`: Forces the link to be rendered as an `a` tag instead of a Vue Router `RouterLink`. +- `prefetch`: When enabled will prefetch middleware, layouts and payloads (when using [payloadExtraction](/docs/api/nuxt-config#crossoriginprefetch)) of links in the viewport. Used by the experimental [crossOriginPrefetch](/docs/api/nuxt-config#crossoriginprefetch) config. +- `noPrefetch`: Disables prefetching. - `prefetchedClass`: A class to apply to links that have been prefetched. -- `custom`: Whether `` should wrap its content in an `` element. It allows taking full control of how a link is rendered and how navigation works when it is clicked. Works the same as [Vue Router's `custom` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-custom) + +### Anchor + +- `target`: A `target` attribute value to apply on the link +- `rel`: A `rel` attribute value to apply on the link. Defaults to `"noopener noreferrer"` for external links. ::callout Defaults can be overwritten, see [overwriting defaults](#overwriting-defaults) if you want to change them. @@ -102,17 +130,18 @@ You can then use `` component as usual with your new defaults. ### `defineNuxtLink` Signature ```ts -defineNuxtLink({ +interface NuxtLinkOptions { componentName?: string; externalRelAttribute?: string; activeClass?: string; exactActiveClass?: string; prefetchedClass?: string; trailingSlash?: 'append' | 'remove' -}) => Component +} +function defineNuxtLink(options: NuxtLinkOptions): Component {} ``` -- `componentName`: A name for the defined `` component. +- `componentName`: A name for the component. Default is `NuxtLink`. - `externalRelAttribute`: A default `rel` attribute value applied on external links. Defaults to `"noopener noreferrer"`. Set it to `""` to disable - `activeClass`: A default class to apply on active links. Works the same as [Vue Router's `linkActiveClass` option](https://router.vuejs.org/api/interfaces/RouterOptions.html#Properties-linkActiveClass). Defaults to Vue Router's default (`"router-link-active"`) - `exactActiveClass`: A default class to apply on exact active links. Works the same as [Vue Router's `linkExactActiveClass` option](https://router.vuejs.org/api/interfaces/RouterOptions.html#Properties-linkExactActiveClass). Defaults to Vue Router's default (`"router-link-exact-active"`) diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index 88a0081bc1..63f06dc2c4 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -1,8 +1,14 @@ -import type { ComputedRef, DefineComponent, InjectionKey, PropType } from 'vue' +import type { + AllowedComponentProps, + AnchorHTMLAttributes, + ComputedRef, + DefineComponent, + InjectionKey, PropType, + VNodeProps +} from 'vue' import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, resolveComponent } from 'vue' -import type { RouteLocation, RouteLocationRaw } from '#vue-router' +import type { RouteLocation, RouteLocationRaw, Router, RouterLinkProps } from '#vue-router' import { hasProtocol, joinURL, parseQuery, parseURL, withTrailingSlash, withoutTrailingSlash } from 'ufo' - import { preloadRouteComponents } from '../composables/preload' import { onNuxtReady } from '../composables/ready' import { navigateTo, useRouter } from '../composables/router' @@ -17,52 +23,94 @@ const firstNonUndefined = (...args: (T | undefined)[]) => args.find(arg => a const DEFAULT_EXTERNAL_REL_ATTRIBUTE = 'noopener noreferrer' const NuxtLinkDevKeySymbol: InjectionKey = Symbol('nuxt-link-dev-key') -export type NuxtLinkOptions = { +/** + * Create a NuxtLink component with given options as defaults. + * @see https://nuxt.com/docs/api/components/nuxt-link + */ +export interface NuxtLinkOptions extends + Pick, + Pick { + /** + * The name of the component. + * @default "NuxtLink" + */ componentName?: string + /** + * A default `rel` attribute value applied on external links. Defaults to `"noopener noreferrer"`. Set it to `""` to disable. + */ externalRelAttribute?: string | null - activeClass?: string - exactActiveClass?: string - prefetchedClass?: string + /** + * An option to either add or remove trailing slashes in the `href`. + * If unset or not matching the valid values `append` or `remove`, it will be ignored. + */ trailingSlash?: 'append' | 'remove' } -export type NuxtLinkProps = { - // Routing - to?: RouteLocationRaw - href?: RouteLocationRaw +/** + * is a drop-in replacement for both Vue Router's component and HTML's tag. + * @see https://nuxt.com/docs/api/components/nuxt-link + */ +export interface NuxtLinkProps extends Omit { + /** + * Route Location the link should navigate to when clicked on. + */ + to?: RouteLocationRaw // need to manually type to avoid breaking typedPages + /** + * An alias for `to`. If used with `to`, `href` will be ignored + */ + href?: NuxtLinkProps['to'] + /** + * Forces the link to be considered as external (true) or internal (false). This is helpful to handle edge-cases + */ external?: boolean - replace?: boolean - custom?: boolean - - // Attributes + /** + * Where to display the linked URL, as the name for a browsing context. + */ target?: '_blank' | '_parent' | '_self' | '_top' | (string & {}) | null - rel?: string | null + /** + * A rel attribute value to apply on the link. Defaults to "noopener noreferrer" for external links. + */ + rel?: 'noopener' | 'noreferrer' | 'nofollow' | 'sponsored' | 'ugc' | (string & {}) | null + /** + * If set to true, no rel attribute will be added to the link + */ noRel?: boolean - + /** + * A class to apply to links that have been prefetched. + */ + prefetchedClass?: string + /** + * When enabled will prefetch middleware, layouts and payloads of links in the viewport. + */ prefetch?: boolean + /** + * Escape hatch to disable `prefetch` attribute. + */ noPrefetch?: boolean - - // Styling - activeClass?: string - exactActiveClass?: string - - // Vue Router's `` additional props - ariaCurrentValue?: string } -/*@__NO_SIDE_EFFECTS__*/ + /*@__NO_SIDE_EFFECTS__*/ export function defineNuxtLink (options: NuxtLinkOptions) { const componentName = options.componentName || 'NuxtLink' - const checkPropConflicts = (props: NuxtLinkProps, main: keyof NuxtLinkProps, sub: keyof NuxtLinkProps): void => { + function checkPropConflicts (props: NuxtLinkProps, main: keyof NuxtLinkProps, sub: keyof NuxtLinkProps): void { if (import.meta.dev && props[main] !== undefined && props[sub] !== undefined) { console.warn(`[${componentName}] \`${main}\` and \`${sub}\` cannot be used together. \`${sub}\` will be ignored.`) } } - const resolveTrailingSlashBehavior = ( + + function resolveTrailingSlashBehavior ( + to: string, + resolve: Router['resolve'] + ): string + function resolveTrailingSlashBehavior ( to: RouteLocationRaw, - resolve: (to: RouteLocationRaw) => RouteLocation & { href?: string } - ): RouteLocationRaw | RouteLocation => { + resolve: Router['resolve'] + ): Omit + function resolveTrailingSlashBehavior ( + to: RouteLocationRaw, + resolve: Router['resolve'] + ): RouteLocationRaw | RouteLocation { if (!to || (options.trailingSlash !== 'append' && options.trailingSlash !== 'remove')) { return to } @@ -97,72 +145,72 @@ export function defineNuxtLink (options: NuxtLinkOptions) { // Attributes target: { - type: String as PropType, + type: String as PropType, default: undefined, required: false }, rel: { - type: String as PropType, + type: String as PropType, default: undefined, required: false }, noRel: { - type: Boolean as PropType, + type: Boolean as PropType, default: undefined, required: false }, // Prefetching prefetch: { - type: Boolean as PropType, + type: Boolean as PropType, default: undefined, required: false }, noPrefetch: { - type: Boolean as PropType, + type: Boolean as PropType, default: undefined, required: false }, // Styling activeClass: { - type: String as PropType, + type: String as PropType, default: undefined, required: false }, exactActiveClass: { - type: String as PropType, + type: String as PropType, default: undefined, required: false }, prefetchedClass: { - type: String as PropType, + type: String as PropType, default: undefined, required: false }, // Vue Router's `` additional props replace: { - type: Boolean as PropType, + type: Boolean as PropType, default: undefined, required: false }, ariaCurrentValue: { - type: String as PropType, + type: String as PropType, default: undefined, required: false }, // Edge cases handling external: { - type: Boolean as PropType, + type: Boolean as PropType, default: undefined, required: false }, // Slot API custom: { - type: Boolean as PropType, + type: Boolean as PropType, default: undefined, required: false } @@ -254,7 +302,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) { return () => { if (!isExternal.value) { - const routerLinkProps: Record = { + const routerLinkProps: RouterLinkProps & VNodeProps & AllowedComponentProps & AnchorHTMLAttributes = { ref: elRef, to: to.value, activeClass: props.activeClass || options.activeClass, @@ -270,7 +318,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) { if (prefetched.value) { routerLinkProps.class = props.prefetchedClass || options.prefetchedClass } - routerLinkProps.rel = props.rel + routerLinkProps.rel = props.rel || undefined } // Internal link @@ -319,14 +367,13 @@ export function defineNuxtLink (options: NuxtLinkOptions) { fullPath: url.pathname, get query () { return parseQuery(url.search) }, hash: url.hash, - // stub properties for compat with vue-router params: {}, name: undefined, matched: [], redirectedFrom: undefined, meta: {}, href - } + } satisfies RouteLocation & { href: string } }, rel, target,