DEV Community

Cover image for Sitecore XM Cloud: Getting Around the Navigation Content Resolver
Kenneth McAndrew
Kenneth McAndrew

Posted on

Sitecore XM Cloud: Getting Around the Navigation Content Resolver

So, in my last blog, I implored you all to avoid rendering contents resolvers. What scares me is I feel like I said it too much, like I called for Beetlejuice, because lately I've seen a couple of blogs discussing their use. I'm very much an advocate against them, but as I noted, the navigation content resolver is a sticky wicket.

Beetlejuice

Why a Sticky Wicket?

Well, to be honest, the rendering parameters are the problem. In theory, you could write GraphQL to traverse your site tree, give it a bunch of "children" calls, and call it a day. You might need to up your complexity a lot on the CM side, but the Edge side is pretty hardy. But, those rendering parameters are tricky for a couple of reasons:

  1. The level from/to and navigation filter dropdowns are droplinks, which means you'd need to resolve those IDs first, an extra step. There's a config way to do this, but it's not perfect yet, and I put in a feature request as a result.

  2. Trickier part...you can't pass rendering parameters to GraphQL on the rendering itself. So you're stuck with GraphQL in your component file, and you need to figure out how to work the levels. Luckily, that's what this blog is to help with!

Setup: Rendering Parameters

First things first, make your own rendering parameter template for this effort. I used droplists for the level from, level to, and navigation filter options. I also put in a field for how many children to retrieve, since you have to specify beyond the default of 10. Note that, for now at least, I dropped the start page parameter (we'll assume we're starting at the homepage) and the flattened and add root parameters (we'll assume you'll configure the output as needed). Hook this parameter up to your rendering definition.

Setup: The fetchNavData function

From here, in the TSX file (we're rolling with the OG XM Cloud foundation header), we're wrapping the call for fetchNavData in a useEffect call. We're passing in the context ID for the page we're on, the level from, the level to, and the children count.

First thing we're doing is getting the full Sitecore path of the context ID we're on. Once we've got that, we use the level from value to calculate what the starting path should be, by parsing the Sitecore path down to where it needs to be. Then we build a GraphQL query where we dynamically slide in the number of child levels we need (level to - level from), and finally execute it.

Now that's a lot to work through, but as I'm a big believer in this, I'm going to provide the code you need. Feel free to tweak it as needed, of course. I'm using this shared code file for both a contextual navigation component and a sitemap component. Note the lib/graphql-client-factory was in the foundation head I believe, so that code should be findable online.

import graphqlClientFactory from 'lib/graphql-client-factory';

export type NavFilter = {
  fields: {
    Key: {
      value: string;
    };
  };
};

export type NavItem = {
  id: string;
  url: { path: string };
  title: { value: string };
  navTitle?: { value: string };
  navFilter?: { filters: NavFilter[] };
  children?: { results: NavItem[] };
};

export type NavResult = {
  QueryResults: NavItem;
};

export type NavPathResult = {
  QueryResults: { path: string };
};

export type NavSectionProps = {
  key: string;
  item: NavItem;
  navFilter?: string;
};

export const fetchNavData = async (
  contextId: string,
  startLevel: number,
  endLevel: number,
  childrenToShow: number
) => {
  let navData = null;

  try {
    const graphQLClient = graphqlClientFactory();

    // 1. Get the full path of the context item
    const navPath = await graphQLClient.request<NavPathResult>(NavPathQuery, {
      datasource: contextId,
      language: 'en',
    });

    if (!navPath?.QueryResults?.path) {
      return { navData };
    }

    // 2. Compute the "start path" at the requested level
    const startPath = getPathUpToLevel(navPath.QueryResults.path, startLevel);

    if (!startPath) {
      return { navData };
    }

    // 3. Build the query (just structure, no baked-in path)
    const navQuery = getNavQuery(endLevel - startLevel, childrenToShow);

    // 4. Run query with $datasource = startPath
    navData = await graphQLClient.request<NavResult>(navQuery, {
      datasource: startPath,
      language: 'en',
    });
  } catch (ex) {
    console.error('Failed to load data for nav component:', ex);
  }

  return { navData };
};

function getNavQuery(levels: number, childrenToShow: number): string {
  const fields = `
    id
    url { path }
    title: field(name:"Title") { value }
    navTitle: field(name:"NavigationTitle") { value }
    navFilter: field(name:"NavigationFilter") { filters: jsonValue }
  `;

  const childrenBlock = buildChildren(fields, levels, childrenToShow);

  // Use $datasource instead of hardcoding path
  return `
    query NavDataQuery($datasource: String!, $language: String!) {
      QueryResults: item(path: $datasource, language: $language) {
        ${fields}
        ${childrenBlock}
      }
    }
  `;
}

// Build children nesting (recursive helper)
function buildChildren(fields: string, level: number, first: number): string {
  if (level === 0) {
    return '';
  }

  return `
      children(hasLayout: true, first: ${first}) {
        results {
          ${fields}
          ${buildChildren(fields, level - 1, first)}
        }
      }
    `;
}

function getPathUpToLevel(fullPath: string, level: number): string | null {
  const parts = fullPath.split('/').filter(Boolean);

  // Find "Home" in the Sitecore path
  const homeIndex = parts.indexOf('Home');

  if (homeIndex === -1) {
    return null;
  }

  // Slice from start of path → requested level past Home
  // Example: level=2 => Home + 1 child
  const endIndex = homeIndex + level;

  if (endIndex > parts.length) {
    return null;
  }

  const wanted = parts.slice(0, endIndex);

  return '/' + wanted.join('/');
}

const NavPathQuery = `
query NavPathQuery($datasource: String!, $language: String!) {
  QueryResults: item(path: $datasource, language: $language) {
    path
  }
}
`;
Enter fullscreen mode Exit fullscreen mode

Usage in the Rendering

From there, add the useEffect call to your rendering TSX file to get the data (included are the relevant references):

  const { sitecoreContext } = useSitecoreContext();
  const contextId = sitecoreContext?.route?.itemId ?? '';
  const levelFrom = parseInt(props.params.NavLevelFrom ?? '2');
  const levelTo = parseInt(props.params.NavLevelTo ?? '5');
  const childrenToShow = parseInt(props.params.Children ?? '20');
  const navFilter = props.params.NavFilter ?? '';

  const [navData, setNavData] = useState<NavItem | null>(null);
  const [loading, setLoading] = useState(false);

  // Fetch nav data
  useEffect(() => {
    if (!contextId) return;

    fetchNavData(contextId, levelFrom, levelTo, childrenToShow)
      .then((data) => {
        setNavData(data.navData?.QueryResults ?? null);
        setLoading(true);
      })
      .catch((err) => console.error(err));
  }, [contextId, levelFrom, levelTo, childrenToShow]);
Enter fullscreen mode Exit fullscreen mode

The data model is in the other code above, so it should be all nice and neat. From there, you can work up the appropriate front-end code to loop through the data model, put in a loading image, or whatever else you'd like to do for the presentation.

Happy coding!

Top comments (0)