diff --git a/packages/vinext/src/shims/link.tsx b/packages/vinext/src/shims/link.tsx index 4ee80087..da5821c3 100644 --- a/packages/vinext/src/shims/link.tsx +++ b/packages/vinext/src/shims/link.tsx @@ -280,19 +280,15 @@ const Link = forwardRef(function Link( // where href is a route pattern like "/user/[id]" and as is "/user/1") const resolvedHref = as ?? resolveHref(href); - // Block dangerous URI schemes (javascript:, data:, vbscript:). - // Render an inert without href to prevent XSS while preserving - // styling and attributes like className, id, aria-*. - if (typeof resolvedHref === "string" && isDangerousScheme(resolvedHref)) { - if (process.env.NODE_ENV !== "production") { - console.warn(` blocked dangerous href: ${resolvedHref}`); - } - const { passHref: _p, ...safeProps } = restWithoutLocale; - return {children}; - } - - // Apply locale prefix if specified - const localizedHref = applyLocaleToHref(resolvedHref, locale); + const isDangerous = + typeof resolvedHref === "string" && isDangerousScheme(resolvedHref); + + // Apply locale prefix if specified (safe even for dangerous hrefs since we + // won't use the result when isDangerous is true) + const localizedHref = applyLocaleToHref( + isDangerous ? "/" : resolvedHref, + locale, + ); // Full href with basePath for browser URLs and fetches const fullHref = withBasePath(localizedHref); @@ -307,7 +303,7 @@ const Link = forwardRef(function Link( // Prefetching: observe the element when it enters the viewport. // prefetch={false} disables, prefetch={true} or undefined/null (default) enables. const internalRef = useRef(null); - const shouldPrefetch = prefetchProp !== false; + const shouldPrefetch = prefetchProp !== false && !isDangerous; const setRefs = useCallback( (node: HTMLAnchorElement | null) => { @@ -474,6 +470,17 @@ const Link = forwardRef(function Link( const linkStatusValue = React.useMemo(() => ({ pending }), [pending]); + // Block dangerous URI schemes (javascript:, data:, vbscript:). + // Render an inert without href to prevent XSS while preserving + // styling and attributes like className, id, aria-*. + // This check is placed after all hooks to satisfy the Rules of Hooks. + if (isDangerous) { + if (process.env.NODE_ENV !== "production") { + console.warn(` blocked dangerous href: ${resolvedHref}`); + } + return {children}; + } + return ( diff --git a/packages/vinext/src/shims/script.tsx b/packages/vinext/src/shims/script.tsx index 3745191e..a50b0b1d 100644 --- a/packages/vinext/src/shims/script.tsx +++ b/packages/vinext/src/shims/script.tsx @@ -107,26 +107,10 @@ function Script(props: ScriptProps): React.ReactElement | null { } = props; const hasMounted = useRef(false); - - // SSR path: only "beforeInteractive" renders a