Avoid Broken Links Caused by URL Encoding When Using Sitecore JSS

Step by step troubleshooting and creative solutioning for the misuse of Sitecore external links

November 29, 2024

By Jeff L'Heureux

During a Sitecore XM Cloud migration using Next.js, we found that some links were returning HTTP 404, not found. While there can be many reasons for this symptom, it was the first time I encountered this specific one. The query strings and hash values were not rendering as expected in the DOM. They were URL encoded. Their question mark (?) were rendered as %3F, & as %26, and hash sign (#) as %23.

Example: <a href="/search%3Futm_source=website%23filter=product">Browse all products</a>

This caused the page path to be /search%3Futm_source=website%23filter=product instead of just /search. This also caused both the query string and the hash to be empty.

The first step was to check the field in Sitecore. I found out that it was an internal link with a relative URL set as an external link. The URL was not URL encoded at this level.

Screenshot of a dialog box in Sitecore to 'Insert External Link'

The next step was getting the field JSON value. Nothing seemed wrong with it and it was not URL encoded before being passed to the JSS Link component.

{
    "value": {
        "href": "/search?utm_source=website#filter=product",
        "text": "Browse all products",
        "linktype": "external",
        "url": "/search?utm_source=website#filter=product",
        "anchor": "",
        "target": ""
    }
}

Then, I looked at the Sitecore JSS Link component code where I found an interesting fact. It executes this logic (simplified for the example):

import NextLink from 'next/link';

// Regex to match strings that start with a forward slash / (relative URLs)
const internalLinkMatcher = /^\//g,

// Destructure 3 properties of the field value
const { href, querystring, anchor } = value;

// If the href is an internal route, return a next/link component.
// Set its href using the 3 destructured properties.
if (internalLinkMatcher.test(href)) {
  return (
    <NextLink
      href={{ pathname: href, query: querystring, hash: anchor }}
      // ...
    >
      // ...
    </NextLink>
  );
}

This code works well for internal links which have these “Query string” and “Anchor” parameters:

Screenshot of a dialog box in Sitecore to input 'Link Details'

However, external links do not have those properties in Sitecore as we could see in the first screenshot and the field JSON value. This means that the next/link href prop for my example link was href={{ pathname: "/search?utm_source=website#filter=product", query: "", hash: "" }}. I guess that somewhere, Next.js must URL encode the pathname property value.

What Are the Possible Solutions?

If you have only a few affected links, it would be easier to edit those in Sitecore. You should train the content authors to understand the differences between an internal and an external link, when to use which, and how to fix incorrect ones by themselves.

If you are dealing with a lot of existing links, the best approach would be a link wrapper component.

Equipped with the knowledge of how the JSS SDK handles the field in its Link component, I implemented a wrapper that fixes the issue by moving the hash and the query string from the external link href property value to the proper anchor and querystring properties.

The wrapper modifies the field JSON value to get this output:

{
    "value": {
        "href": "/search",
        "text": "Browse all products",
        "linktype": "external",
        "url": "/search?utm_source=website#filter=product",
        "anchor": "filter=product",
        "querystring": "utm_source=website"
        "target": ""
    }
}

Then, the next/link href prop becomes href={{ pathname: "/search", query: "utm_source=website", hash: "filter=product" }} and the link in the DOM becomes <a href="/search?utm_source=website#filter=product">Browse all products</a> as expected.

Here is the code of the component:

import {
  LayoutServicePageState,
  Link,
  LinkField,
  LinkFieldValue,
  LinkProps,
  useSitecoreContext,
} from '@sitecore-jss/sitecore-jss-nextjs';

type CustomLinkField = LinkField | LinkFieldValue;

type CustomLinkProps = {
  field: CustomLinkField;
} & LinkProps;

const getLinkValue = (linkField: CustomLinkField): LinkFieldValue => {
  return 'href' in linkField ? linkField : (linkField.value as LinkFieldValue);
};

/**
 * Fixes an edge case where an internal URL with a querystring
 * and/or hash is incorrectly set as an external link.
 * This can happen when content editors paste internal URLs into
 * external link fields in Sitecore.
 *
 * The function:
 * 1. Checks if the link is marked as external but starts with
 *    '/' (indicating internal URL).
 * 2. If a hash exists, separates it from the base URL.
 * 3. If a querystring exists, separates it from the base URL.
 * 4. Returns a fixed link object with the separated hash and
 *    querystring.
 *
 * @param field - The link field to check and potentially fix
 * @returns The fixed link field value with separated hash and querystring, or original field if no fix needed
 */
const splitInternalUrlHashAndQuerystringInExternalLink = (
  field: CustomLinkField
): LinkFieldValue => {
  if (!field) return field;

  const value = getLinkValue(field);
  if (!value.href) return value;

  const isInternalUrlInExternalLink = value.linktype === 'external' && value.href.startsWith('/');
  if (!isInternalUrlInExternalLink) return value;

  const indexOfHash = value.href.indexOf('#');
  const hasHash = indexOfHash !== -1;
  const indexOfQueryString = value.href.indexOf('?');
  const hasQueryString = indexOfQueryString !== -1;

  if (!hasHash && !hasQueryString) return value;

  let href = value.href;
  let anchor = '';
  let querystring = '';

  if (hasHash) {
    anchor = href.substring(indexOfHash + 1);
    href = href.substring(0, indexOfHash);
  }

  if (hasQueryString) {
    querystring = href.substring(indexOfQueryString + 1);
    href = href.substring(0, indexOfQueryString);
  }

  const fixedLink = {
    ...value,
    href,
    anchor,
    querystring,
  };

  return fixedLink;
};

export const CustomLink = ({ field, ...props }: CustomLinkProps) => {
  const { sitecoreContext } = useSitecoreContext();
  const isEditMode = sitecoreContext?.pageState === LayoutServicePageState.Edit;

  // Do not alter the field value in edit mode
  if (isEditMode) {
    return <Link field={field} {...props} />;
  }

  const updatedField = {
    ...field,
    value: splitInternalUrlHashAndQuerystringInExternalLink(field),
  };

  return <Link field={updatedField} {...props} />;
};

This wrapper component can be used like this in your own components:

import { LinkField } from '@sitecore-jss/sitecore-jss-nextjs';
import { CustomLink } from './CustomLink';

type MyComponentProps = {
  fields: {
    link: LinkField;
  };
};

export const MyComponent = (props: MyComponentProps) => (
  <CustomLink field={props.fields.link} />
);

The wrapper component provides a robust solution that can handle existing content without requiring manual updates, while maintaining full compatibility with Sitecore's editing experience.

Conclusion

Every day we find creative ways content authors are using Sitecore. As developers, we must often adapt our code to support them. Being able to investigate issues step by step is a crucial skill for success. Having the Sitecore JSS SDK open source is a blessing that allows us to dig even deeper and develop creative solutions.

I hope this incursion into my thought process and this link wrapper component will be helpful for you.

Happy Sitecoring!

Jeff L'Heureux

Jeff L'Heureux

Director of Technology

Jean-François (Jeff) L'Heureux is an experienced leader in Sitecore and Coveo technologies, having worked in both organizations. He is a three-times Sitecore Technology MVP and three-time Coveo MVP. He has 16 years of software development experience, including ten years of Sitecore experience. He specializes in front-end, and he has experience in technologies like Next.js, React, Vercel, Netlify, Docker, Coveo Cloud, Coveo for Sitecore, Sitecore XP/XM, and the latest Sitecore technologies, including XM Cloud, JSS, CDP, Personalize, OrderCloud, Discover, Send, Search, and Content Hub ONE. Outside work, he can be found outside rock climbing, mountain biking, hiking, snowshoeing, or cross-country skiing.