The Sitecore Experience Edge schema has been built to handle pagination on the results. It has already set a cursor-based system
Let’s say we want to query the list of all the Blog Pages for your website. The criteria for your BlogPage template are the following.
- Inherits the default Page template
- A heading that is set to Single-line text
- An image that is an ImageField
- An author as well which for the sake of a simple example is another Single-line text
How Experience Edge’s Pagination works
The Experience Edge schema has integrated a cursor-based system when requesting from a list of results. On the results list object, they usually add in a pageInfo object which holds how you’ll be able to manage pagination. This object returns two pieces of data, which are the endCursor and hasNext.
pageInfo {
endCursor
hasNext
}
The endCursor is a string that holds the text needed to know where the first item should start. There is another field named after which you use the endCursor value on. The hasNext value is a boolean value which indicates whether you have reached the end of the list. The query below has the following characteristics that it tries to do.
- Returns 10 ArticlePages from a specific source, the source.
- ({XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}) represents the source which is the main listing page where all the sub pages are found.
- The results are filtered by only selecting the datasources with a specific template ID ({YYYYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}). This represents the template ID of the ArticlePage template.
query {
item(path: "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}", language: "en") {
children(
first: 10
includeTemplateIDs: ["{YYYYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}"]
) {
pageInfo {
endCursor
hasNext
}
results {
... on ArticlePage {
name
}
}
}
}
}
Based on the code above, we are fetching 10 items from the whole list, so if we were supposed to need like 50 items we just set the first field value to 50 instead of 10. Then if we were to get even more like 100 or 200 items it would be the same solution. Theoretically it would work but Sitecore’s queries has a limit to how complex your query can be. Quantitatively, the complexity score needed is 250 which doesn’t really mean anything since we cannot really put a number on how complex our query can be. You will have to do some experimenting and testing to see the limit of your query. The complexity can come from different factors. It can come from the amount of fields you are trying to fetch, the total number of items you are fetching as results and maybe how deep your queries go. To prevent this we can leverage the use of pagination in order to grab more items and prevent the chances of your queries from failing due to complexity score.
How to Populate the List With Cursor-Based Queries?
The solution to prevent having high complexity score is by breaking down your single query into multiple smaller queries. One way of doing this is by lowering the amount of results we are getting and instead concatenating it to your final list by fetching data repeatedly until you get your desired total instead. We already have our sample query above, let’s continue with setting up the needed information for GraphQL to work. Based on a previous blog we did, we learned how to use the query to grab data using the GraphQL query. Let’s use some parts of the code there which would be useful in our example now.
import { gql, GraphQLClient } from 'graphql-request';
import config from 'temp/config';
export const fetchArticlePages = async (datasource: string, language: string) => {
const graphQLClient = new GraphQLClient(config.graphQLEndpoint);
graphQLClient.setHeader('sc_apikey', config.sitecoreApiKey);
};
We have a function we named fetchArticlePages since this function is responsible for fetching the pages. Then we have the initial setup of our graphQLClient which will require your Sitecore API Key.
import { gql, GraphQLClient } from "graphql-request";
import config from "temp/config";
export const fetchArticlePages = async (language: string) => {
const graphQLClient = new GraphQLClient(config.graphQLEndpoint);
graphQLClient.setHeader("sc_apikey", config.sitecoreApiKey);
const articleSourcePath = "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}";
const articlePageTemplateId = "{YYYYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}";
const query = `
query {
item(path: "${articleSourcePath}", language: "${language}") {
children(
first: 10
includeTemplateIDs: ["${articlePageTemplateId}"]
) {
pageInfo {
endCursor
hasNext
}
results {
... on ArticlePage {
name
}
}
}
}
}
`;
const data = await graphQLClient.request(query);
return data;
};
The code above fetches the first 10 pages from the whole list of pages, We’re not there yet, since we haven’t implementing how it grabs the rest of the pages and we’ll need to continue calling this until we’ve completed getting the required number of pages we need. Navigating between the different pages is by entering a value in the after field. When we perform a fetch on the the first page by calling the function above which has the after field empty and including the pageInfo object as part of the requested data, we can use the endCursor as the value on our after field. It may be hard to visualize without code so here is the code with everything needed. We can go through the new parts one by one to get a better understanding of what’s going on.
import { gql, GraphQLClient } from "graphql-request";
import config from "temp/config";
export const fetchArticlePages = async (language: string) => {
const graphQLClient = new GraphQLClient(config.graphQLEndpoint);
graphQLClient.setHeader("sc_apikey", config.sitecoreApiKey);
const articleSourcePath = "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}";
const articlePageTemplateId = "{YYYYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}";
// variable to handle the cursor-based logic
let populateData: ArticlePageListType = {
search: {
results: [],
pageInfo: {
endCursor: "",
hasNext: true,
},
},
};
while (populateData.search.pageInfo.hasNext || populateData.search.results.length >= 50) {
const query = gql`
query($path: String!, $language: String!, $first: Int!, $cursor: String) {
item(path: $path, language: $language) {
children(
first: $first
includeTemplateIDs: ["${articlePageTemplateId}"]
after: $cursor
) {
pageInfo {
endCursor
hasNext
}
results {
... on ArticlePage {
name
}
}
}
}
}
`;
const variables = {
path: articleSourcePath,
language,
first: 10,
cursor: populateData.search.pageInfo.endCursor,
};
const data = await graphQLClient.request(query, variables);
populateData.search.results.push(...data.item.children.results);
populateData.search.pageInfo = data.item.children.pageInfo;
}
return { populateData };
};
A lot has been updated on the initial function we created. Let’s start by looking at the updates of our GraphQL query. Now the we’ve applied the use of variables on it and as you can see we’ve added the after field as well.
query($path: String!, $language: String!, $first: Int!, $cursor: String) {
item(path: $path, language: $language) {
children(
first: $first
includeTemplateIDs: ["${articlePageTemplateId}"]
after: $cursor
) {
pageInfo {
endCursor
hasNext
}
results {
... on ArticlePage {
name
}
}
}
}
}
`;
The rest of the additional code just basically tells us that the loop continues until either no new data can be found or it has reached the limit we want which is 50 items. We grab the endCursor and use it to define the value of after.
Exploring Advanced Pagination and Query Optimization with Sitecore Experience Edge
There are more useful uses of the Sitecore Experience Edge schema which we’ll be introducing next time. If you’re interested in learning more about the search feature of the Experience Edge schema check out Sitecore’s official documentation on it. You’ll be able to code more complex solutions which can help push further your skills and the strategies you are able to make when tackling different problems in your development journey.