Sitecore JSS Link and Prefetch

Sitecore JSS Link and Prefetch
Photo by Rob Fuller / Unsplash

Next's Link component from Next.js can prefetch pages when the component enters the user's viewport or is hovered. This allows for faster client-side transitions.

However, this prefetch functionality breaks down when the Headless site is loaded through Experience Editor, Sitecore Pages, or a non-editor experience such as a preview. This leads to excessive calls to the backend that report as 404s.

Hovering over a link in Sitecore's Skate Park demo site in XM Cloud, the network tab in developer tools shows the request triggered from the prefetch fails.

This is due mainly to prefetch requests operating relative to the site domain. If the site were loaded on Vercel, this would work as expected.

Next.js provides a way to disable prefetch on the Link component with:

import Link from 'next/link'
 
export default function Page() {
  return (
    <Link href="/dashboard" prefetch={false}>
      Dashboard
    </Link>
  )
}

https://nextjs.org/docs/pages/api-reference/components/link#prefetch

Depending on where Next.js is deployed, this prefetch could be set by environment variable:

import Link from 'next/link'

export default function Page() {
  const disablePrefetch = process.env.process.env.NEXT_PUBLIC_DISABLE_PREFETCH && process.env.process.env.NEXT_PUBLIC_DISABLE_PREFETCH === 'true'

  return (
    <Link href="/dashboard" prefetch={disablePrefetch}>
      Dashboard
    </Link>
  )
}

However, the Link component in Sitecore JSS is limited in the props it feeds back to Next Link. Prefetch is not an option, as shown below.

import NextLink from 'next/link';

export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
  (props: LinkProps, ref): JSX.Element | null => {
    const {
      field,
      editable,
      children,
      internalLinkMatcher = /^\//g,
      showLinkTextWithChildrenPresent,
      ...htmlLinkProps
    } = props;

    ...

    if (href && !isEditing) {
      const text = showLinkTextWithChildrenPresent || !children ? value.text || value.href : null;

      // determine if a link is a route or not.
      if (internalLinkMatcher.test(href)) {
        return (
          <NextLink
            href={{ pathname: href, query: querystring, hash: anchor }}
            key="link"
            locale={false}
            title={value.title}
            target={value.target}
            className={value.class}
            {...htmlLinkProps}
            ref={ref}
          >
            {text}
            {children}
          </NextLink>
        );
      }
    }

    ...
    }
);

Only workarounds can address the issue until Sitecore provides an elegant way to fix these requests in this context.

Another workaround is to use Next.js experimental configuration optimisticClientCache

This can be set by updating the next.config.js as follows:

/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {

  experimental: { optimisticClientCache: (process.env.DISABLE_PREFETCH && process.env.DISABLE_PREFETCH === 'true')},

};

module.exports = () => {
  // Run the base config through any configured plugins
  return Object.values(plugins).reduce((acc, plugin) => plugin(acc), nextConfig);
}

This allows not having to remember to set this on every Link component. However, as indicated, the setting is experimental, and there is little documentation on exactly what this setting does if it only impacts Link components.

If experimental settings do not sit well with you, another option exists.

The Next router for prefetch can be updated to unset the async function responsible for triggering the request.

import { useRouter } from 'next/router';
import { useEffect } from 'react';
import type { AppProps } from 'next/app';
import { I18nProvider } from 'next-localization';
import { SitecorePageProps } from 'lib/page-props';

const App = ({ Component, pageProps }: AppProps<SitecorePageProps>): JSX.Element => {
  const { dictionary, ...rest } = pageProps;

  const router = useRouter();
  // Prefetching is disabled in preview mode to prevent unnecessary requests to Sitecore.
  useEffect(() => {
    const disablePretech = process.env.NEXT_PUBLIC_DISABLE_PREFETCH;
    if (disablePrefetch === 'true') {
      // eslint-disable-next-line @typescript-eslint/no-empty-function
      router.prefetch = async () => {};
    }
  }, [router]);

  return(
    <I18nProvider lngDict={dictionary} locale={pageProps.locale}>
      <Component {...rest} />
    </I18nProvider>
  );
}

export default App;
  

This approach was suggested in the Next.js community as described in this issue https://github.com/vercel/next.js/discussions/24437

All these options can work depending on how they best resolve the issue for your Sitecore environments. Disabling prefetch in either lower environments or preview environments does not degrade the experience or is not a critical feature for editors. Hopefully, Sitecore can determine the best fix for this issue and make it into a future release.

If you need to follow up with Sitecore Support on this issue, this is a logged issue JSS-1847

Resources:

Components: <Link> | Next.js
API reference for the <Link> component.
jss/packages/sitecore-jss-nextjs/src/components/Link.tsx at main · Sitecore/jss
Software development kit for JavaScript developers building web applications with Sitecore Experience Platform - Sitecore/jss
Functions: useRouter | Next.js
Learn more about the API of the Next.js Router, and access the router instance in your page with the useRouter hook.