diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index 814df1658e..353f8ccda6 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -18,6 +18,8 @@ import { cancelIdleCallback, requestIdleCallback } from '../compat/idle-callback // @ts-expect-error virtual file import { nuxtLinkDefaults } from '#build/nuxt.config.mjs' +import { hashMode } from '#build/router.options' + const firstNonUndefined = (...args: (T | undefined)[]) => args.find(arg => arg !== undefined) const NuxtLinkDevKeySymbol: InjectionKey = Symbol('nuxt-link-dev-key') @@ -110,6 +112,10 @@ export function defineNuxtLink (options: NuxtLinkOptions) { } } + function isHashLinkWithoutHashMode (link: unknown): boolean { + return !hashMode && typeof link === 'string' && link.startsWith('#') + } + function resolveTrailingSlashBehavior (to: string, resolve: Router['resolve']): string function resolveTrailingSlashBehavior (to: RouteLocationRaw, resolve: Router['resolve']): Exclude function resolveTrailingSlashBehavior (to: RouteLocationRaw | undefined, resolve: Router['resolve']): RouteLocationRaw | RouteLocation | undefined { @@ -176,7 +182,9 @@ export function defineNuxtLink (options: NuxtLinkOptions) { // Resolves `to` value if it's a route location object const href = computed(() => { - if (!to.value || isAbsoluteUrl.value) { return to.value as string } + if (!to.value || isAbsoluteUrl.value || isHashLinkWithoutHashMode(to.value)) { + return to.value as string + } if (isExternal.value) { const path = typeof to.value === 'object' && 'path' in to.value ? resolveRouteObject(to.value) : to.value @@ -373,7 +381,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) { } return () => { - if (!isExternal.value && !hasTarget.value) { + if (!isExternal.value && !hasTarget.value && !isHashLinkWithoutHashMode(to.value)) { const routerLinkProps: RouterLinkProps & VNodeProps & AllowedComponentProps & AnchorHTMLAttributes = { ref: elRef, to: to.value, diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts index c2215173bd..ba8c0a807d 100644 --- a/packages/nuxt/src/pages/module.ts +++ b/packages/nuxt/src/pages/module.ts @@ -129,6 +129,16 @@ export default defineNuxtModule({ 'export const START_LOCATION = Symbol(\'router:start-location\')', ].join('\n'), }) + // used by `` + addTemplate({ + filename: 'router.options.mjs', + getContents: () => { + return [ + 'export const hashMode = false', + 'export default {}', + ].join('\n') + }, + }) addTypeTemplate({ filename: 'types/middleware.d.ts', getContents: () => [ diff --git a/packages/nuxt/test/nuxt-link.test.ts b/packages/nuxt/test/nuxt-link.test.ts index f57c5d43b0..057214f3a1 100644 --- a/packages/nuxt/test/nuxt-link.test.ts +++ b/packages/nuxt/test/nuxt-link.test.ts @@ -119,6 +119,11 @@ describe('nuxt-link:isExternal', () => { expect(nuxtLink({ to: '/foo/bar', target: '_blank' }).type).toBe(EXTERNAL) expect(nuxtLink({ to: '/foo/bar?baz=qux', target: '_blank' }).type).toBe(EXTERNAL) }) + + it('returns `true` if link starts with hash', () => { + expect(nuxtLink({ href: '#hash' }).type).toBe(EXTERNAL) + expect(nuxtLink({ to: '#hash' }).type).toBe(EXTERNAL) + }) }) describe('nuxt-link:propsOrAttributes', () => { diff --git a/test/mocks/router-options.ts b/test/mocks/router-options.ts new file mode 100644 index 0000000000..7a16f6ffec --- /dev/null +++ b/test/mocks/router-options.ts @@ -0,0 +1,2 @@ +export default {} +export const hashMode = false diff --git a/vitest.config.ts b/vitest.config.ts index ec08cc258d..e9734754a4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ resolve: { alias: { '#build/nuxt.config.mjs': resolve('./test/mocks/nuxt-config'), + '#build/router.options': resolve('./test/mocks/router-options'), '#internal/nuxt/paths': resolve('./test/mocks/paths'), '#build/app.config.mjs': resolve('./test/mocks/app-config'), '#app': resolve('./packages/nuxt/dist/app'),