codeflood logo

Experience Edge Schema for XM Tenants

Sitecore Experience Edge is a multi-tenant system hosting tenants for a number of different Sitecore source systems. The data models of each source system are quite different which requires a different schema in the GraphQL API for each kind of source system. In this post I'll be exploring the GraphQL schema used for XM tenants on Experience Edge. This schema is used by XM, XM cloud and managed cloud XM instances.

The Sitecore XM data model is a hierarchical tree of items. The fields of an item are defined in the data template used by the item. Although most (if not all) templates inherit from the standard template (to provide all the default functionality), developers define all the fields used by business users in their own data templates, which are then used in their implementations.

The GraphQL schema used for XM tenants on Experience Edge continues to evolve in a non-breaking manner, to add more functionality and expose addition data as required by features like headless SXA. Many of these new features require updating the XM Edge connector to the latest version, which is released as part of Sitecore Headless Rendering. For the samples in this post I'm using the Experience Edge tenant used by the MVP site, the SUGCON Europe and SUGCON ANZ sites. Big thank you to the Sitecore technical marketing team (especially Rob Earlam) for letting me use it for this post. These sites are hosted on XM cloud which is currently using the latest version of the XM Edge connector version 21.0. If you're using an older version, you might not see some of the fields mentioned below.

To execute any of the queries in this post, you can use the IDE which is accessible at https://edge.sitecorecloud.io/api/graphql/ide or you can use the tips I provided in my last post about Using the Experience Edge GraphQL API. In either case, you'll need an XM tenant on Experience Edge and an API key for that tenant.

Top-level Query Fields

There are 4 fields on the top-level Query type in the XM Edge schema:

  • item which allows accessing items by path or ID.
  • layout which allows resolving routes to items.
  • search which allows searching for items matching certain criteria.
  • site which allows accessing the sites that have been defined. Only available if using XM Edge connector v21 or later.

The Item Query

The item query allows you to access a single item by either path or ID. You can query any of the fields or properties (such as name, display name, etc) on the item and also navigate the content tree through the children and parent fields.

Let's start with the "Hello World" of Sitecore queries and get the OOTB Home item.

query HomeItem {
  item(language: "en", path: "/sitecore/content/home") {
    id
    name
  }
}

This query will grab the home item and return it's ID and name.

{
  "data": {
    "item": {
      "id": "110D559FDEA542EA9C1C8A5DF7E70EF9",
      "name": "Home"
    }
  }
}

The item query returns an Item type in the GraphQL response. You can use the template field of the Item type to link to the template definition used by the item, and query it as well.

query HomeItem {
  item(language: "en", path: "/sitecore/content/home") {
    id
    name
    template {
      id
      name
    }
  }
}

Now we can see the home item is based on the Sample Item template.

{
  "data": {
    "item": {
      "id": "110D559FDEA542EA9C1C8A5DF7E70EF9",
      "name": "Home",
      "template": {
        "id": "76036F5ECBCE46D1AF0A4143F9B557AA",
        "name": "Sample Item"
      }
    }
  }
}

That right there is the power of GraphQL. To query the template used by the item you don't have to send another request for a different resource. You simply update the query to link to the linked resource and extract the properties of it that you want.

To access a single field, you can use the field field of the Item type. This will return an ItemField type:

query HomeItem {
  item(language: "en", path: "/sitecore/content/home") {
    id
    name
    field(name:"title") {
      name
      value
    }
  }
}

And here's the response which includes the field requested.

{
  "data": {
    "item": {
      "id": "110D559FDEA542EA9C1C8A5DF7E70EF9",
      "name": "Home",
      "field": {
        "name": "Title",
        "value": "Sitecore Experience Platform"
      }
    }
  }
}

To access multiple fields, you can include the field field multiple times. However, if the field is repeated you'll get an error when the query is executed because there are multiple output properties with the same name at the same level. So when accessing multiple fields you need to use aliases to give the output a different name.

query HomeItem {
  item(language: "en", path: "/sitecore/content/home") {
    id
    name
    title: field(name:"title") {
      value
    }
    text: field(name:"text") {
      value
    }
  }
}

Now the fields in the output use the alias we provided, which looks much cleaner.

{
  "data": {
    "item": {
      "id": "110D559FDEA542EA9C1C8A5DF7E70EF9",
      "name": "Home",
      "title": {
        "value": "Sitecore Experience Platform"
      },
      "text": {
        "value": "<p style=\"line-height: 22px;\">From a single connected platform that also integrates 
            with other customer-facing platforms, to a single view of the customer in a big data marketing 
            repository, to completely eliminating much of the complexity that has previously held marketers 
            back, the latest version of Sitecore makes customer experience highly achievable. Learn how the 
            latest version of Sitecore gives marketers the complete data, integrated tools, and automation 
            capabilities to engage customers throughout an iterative lifecycle &ndash; the technology 
            foundation absolutely necessary to win customers for life.</p>\n<p>For further information, 
            please go to the <a href=\"https://doc.sitecore.net/\" target=\"_blank\" title=\"Sitecore 
            Documentation site\">Sitecore Documentation site</a></p>\r"
      }
    }
  }
}

You could also use an alias in the first field example above, to make the output cleaner.

When writing queries for your application, you'll know which fields you want to access. But when you're initially creating the queries you might need to explore a little. This is where the fields field (note it's multiple fields) can help. It enumerates all fields of the item allowing you to discover what fields the item has.

query HomeItem {
  item(language: "en", path: "/sitecore/content/home") {
    id
    name
    fields {
      name
      value
    }
  }
}

The response includes all the fields now.

{
  "data": {
    "item": {
      "id": "110D559FDEA542EA9C1C8A5DF7E70EF9",
      "name": "Home",
      "fields": [
        {
          "name": "Text",
          "value": "<p style=\"line-height: 22px;\">From a single connected platform that also integrates 
                with other customer-facing platforms, to a single view of the customer in a big data marketing 
                repository, to completely eliminating much of the complexity that has previously held marketers 
                back, the latest version of Sitecore makes customer experience highly achievable. Learn how the 
                latest version of Sitecore gives marketers the complete data, integrated tools, and automation 
                capabilities to engage customers throughout an iterative lifecycle &ndash; the technology 
                foundation absolutely necessary to win customers for life.</p>\n<p>For further information, 
                please go to the <a href=\"https://doc.sitecore.net/\" target=\"_blank\" title=\"Sitecore 
                Documentation site\">Sitecore Documentation site</a></p>\r"
        },
        {
          "name": "Title",
          "value": "Sitecore Experience Platform"
        }
      ]
    }
  }
}

Yeah, it's the same fields we've already been looking at above, but I didn't need to specify each field individually. Let's try doing that on something that uses a template other than the Sample Item template.

query MvpHomeItem {
  item(language: "en", path: "/sitecore/content/MvpSite/Home") {
    id
    name
    template {
      id
      name
    }
    fields {
      name
      value
    }
  }
}

This temlpate includes many more fields than the Sample Item template.

{
  "data": {
    "item": {
      "id": "94DE9AC3A9F740ABAE90ACDA364B9C40",
      "name": "Home",
      "template": {
        "id": "9C1E30A086914AD4BEF0E1F446EEF8F3",
        "name": "Homepage"
      },
      "fields": [
        {
          "name": "Logo SVG path",
          "value": "/images/sitecore.svg"
        },
        {
          "name": "IncludeInMenu",
          "value": ""
        },
        {
          "name": "MenuTitle",
          "value": "Home"
        },
        {
          "name": "RequiresAuthentication",
          "value": ""
        },
        {
          "name": "MetaDescription",
          "value": "MVP Home Description"
        },
        {
          "name": "MetaKeywords",
          "value": "MVP, Most Valuable Professional"
        },
        {
          "name": "OgTitle",
          "value": "Home"
        },
        {
          "name": "OgImage",
          "value": "<image mediaid=\"{1023987B-1EAD-49A0-BF57-9A1C90A0F88A}\" />"
        },
        {
          "name": "OgType",
          "value": "article"
        },
        {
          "name": "OgDescription",
          "value": "MVP Home Description"
        }
      ]
    }
  }
}

Concrete Types

So far we've been using the Item type in the query, which is actually an interface. Being it covers all kinds of items (all templates) the field names are quite generic. The Experience Edge schema for an XM tenant also includes concrete types for each template, all implementing the Item interface. Concrete types make the query even cleaner by exposing fields with names which correspond to the fields of the template.

Additionally, the type used for fields in the previous queries is ItemField which is also an interface. Just like with items and templates, there are concrete implementations of ItemField for each kind of field. Concrete field types expose the data of the field in more useable ways than just the raw string values we've seen so far. For example, a LookupField allows querying through to the item selected.

To use the concrete types, we must use an inline fragment so we can specify the fields when the concrete type of the object is the type of the fragment. Let's start with using concrete field types.

query MvpHomeItem {
  item(language: "en", path: "/sitecore/content/MvpSite/Home") {
    id
    name
    title: field(name:"ogtitle") {
      ... on TextField {
        value
      }
    }
    image: field(name:"ogimage") {
      ... on ImageField {
        name
        mimeType
        size
        alt
        src(maxHeight:100 maxWidth:200)
      }
    }
  }
}
{
  "data": {
    "item": {
      "id": "94DE9AC3A9F740ABAE90ACDA364B9C40",
      "name": "Home",
      "title": {
        "value": "Home"
      },
      "image": {
        "name": "OgImage",
        "mimeType": "image/jpeg",
        "size": 59918,
        "alt": "Sitecore Most Valuable Professional",
        "src": "https://edge.sitecorecloud.io/sitecoresaa94c3-xmcloudintr2ef7-production-9f57/media/Project/
          MvpSite/Sitecore-MVP-logo.jpg?mw=200&mh=100"
      }
    }
  }
}

Using the ItemField type we're now linking through to the media item which the image has selected, and extracting values from that media like the alt text.

Now let's try using a concrete Item type.

query MvpHomeItem {
  item(language: "en", path: "/sitecore/content/MvpSite/Home") {
    ... on Homepage {
      id
      name
      ogTitle {
        value
      }
      ogImage {
        name
        mimeType
        size
        alt
        src(maxHeight: 100, maxWidth: 200)
      }
    }
  }
}

See how the fields are now named after the template field? The types of those fields are also the concrete type and not the ItemField template, so we don't need to use inline fragments when using them.

{
  "data": {
    "item": {
      "id": "94DE9AC3A9F740ABAE90ACDA364B9C40",
      "name": "Home",
      "ogTitle": {
        "value": "Home"
      },
      "ogImage": {
        "name": "OgImage",
        "mimeType": "image/jpeg",
        "size": 59918,
        "alt": "Sitecore Most Valuable Professional",
        "src": "https://edge.sitecorecloud.io/sitecoresaa94c3-xmcloudintr2ef7-production-9f57/media/Project/
          MvpSite/Sitecore-MVP-logo.jpg?mw=200&mh=100"
      }
    }
  }
}

And the response is exactly like the previous example. We're still accessing the exact same data, just in a nicer "strongly-typed" fashion.

Sometimes it can be a bit difficult to work out what type something is. To help with discovery we can use some introspection to find what type an object is.

query MvpHomeItem {
  item(language: "en", path: "/sitecore/content/MvpSite/Home") {
    __typename
    title: field(name:"ogtitle") {
      __typename
    }
    image: field(name:"ogimage") {
      __typename
    }
  }
}

__typename is part of the GraphQL introspection features and exposes the name of the type of the object.

{
  "data": {
    "item": {
      "__typename": "Homepage",
      "title": {
        "__typename": "TextField"
      },
      "image": {
        "__typename": "ImageField"
      }
    }
  }
}

Navigating the content tree

The Item interface contains a couple of fields which can be used to navigate around the content tree, from a given item.

  • ancestors provides access to all the ancestor items of the current item.
  • children provides access to all the children of the current item.
  • hasChildren indicates whether the current item has any children.
  • parent provides access to the parent of the current item.

All the above fields expose different types which are more appropriate to the field being accessed. For example parent exposes an Item type whereas ancestors exposes an array of Item.

query MvpHomeItem {
  item(language: "en", path: "/sitecore/content/MvpSite/Home") {
    ...itemInfo
    ancestors {
      ...itemInfo
    }
    parent {
      ...itemInfo
    }
    hasChildren
    children {
      results {
        ...itemInfo
      }
    }
  }
}

fragment itemInfo on Item {
  id
  name
}
{
  "data": {
    "item": {
      "id": "94DE9AC3A9F740ABAE90ACDA364B9C40",
      "name": "Home",
      "ancestors": [
        {
          "id": "A43C669260F44743A1BE6424DE611DFA",
          "name": "MvpSite"
        },
        {
          "id": "0DE95AE441AB4D019EB067441B7C2450",
          "name": "content"
        },
        {
          "id": "11111111111111111111111111111111",
          "name": "sitecore"
        }
      ],
      "parent": {
        "id": "A43C669260F44743A1BE6424DE611DFA",
        "name": "MvpSite"
      },
      "hasChildren": true,
      "children": {
        "results": [
          {
            "id": "FE6EC725417344C5A2BC8C92CF7E74B5",
            "name": "404"
          },
          {
            "id": "0D97B45DC5894495A4959AAFF4FBD2C3",
            "name": "About"
          },
          {
            "id": "0068B8802204475995190ED217BBC156",
            "name": "Application"
          },
          {
            "id": "FA04808614BC4E7D998759F8B4ADC286",
            "name": "Become-an-mvp"
          },
          {
            "id": "F96C13C3E71B4DA2881F17F4DCAFE56F",
            "name": "Benefits"
          },
          {
            "id": "C59D92D6739B4424BF07D3AD7C2B9FC1",
            "name": "Contact"
          },
          {
            "id": "67BA86B64ADF422BBD311C17336C5FBF",
            "name": "Directory"
          },
          {
            "id": "CAFD6A4EF1C748BF96FA8689193C8B55",
            "name": "Mentor-Program"
          },
          {
            "id": "CBF43178A0934BAE8CE072D3DF81D77B",
            "name": "Podcast"
          },
          {
            "id": "728D5B0E573F4EB682339A5DC958507C",
            "name": "Thank-you"
          }
        ]
      }
    }
  }
}

You can also navigate the content tree just by manipulating the path argument when using the item query, a long as you know the paths you want to access.

The Layout Query

If you're using XM, then you're likely building websites and have a URL structure which resolves to items. The layout query allows resolving items from routes rather than fully qualified Sitecore paths. Let's find the item which is used for the /About route on the MVP site.

query AboutPage {
  layout(language:"en" site:"mvp-site" routePath:"/about") {
    item {
      id
      name
      path
    }
  }
}
{
  "data": {
    "layout": {
      "item": {
        "id": "0D97B45DC5894495A4959AAFF4FBD2C3",
        "name": "About",
        "path": "/sitecore/content/MvpSite/Home/About"
      }
    }
  }
}

The layout query returns a LayoutData type which only has a single field named item, which links to the item for the route provided.

The Search Query

The search query allows searching across all items instead of having to resolve them from paths or routes. For example, let's search for all items based on the Page template.

query Pages {
  search(
    first: 5
    where: { name: "_templates", value: "A054184C3B204C2BAB04C8A03ACD5522" }
    orderBy: { name: "_name", direction: ASC }
  ) {
    pageInfo {
      hasNext
      endCursor
    }
    results {
      id
      name
      path
    }
  }
}

The response includes the first page of results. I was expecting quite a lot of results, so I've used the first argument on the search query to limit the number of results to 5, and also used the orderBy argument so the results are ordered by name.

{
  "data": {
    "search": {
      "pageInfo": {
        "hasNext": true,
        "endCursor": "eyJzZWFyY2hBZnRlciI6WyJhZG1pbiIsIkI3Qjk0M0M0NTU1RDQwRTE5Njc3QzY4NzI2Njg2M
          TE5IiwiQjdCOTQzQzQ1NTVENDBFMTk2NzdDNjg3MjY2ODYxMTkiXSwiY291bnQiOjV9"
      },
      "results": [
        {
          "id": "9AC6DDA387854C63A2AB457D8CC56D19",
          "name": "$name",
          "path": "/sitecore/templates/Branches/Project/MvpSite/Page/$name"
        },
        {
          "id": "FE6EC725417344C5A2BC8C92CF7E74B5",
          "name": "404",
          "path": "/sitecore/content/MvpSite/Home/404"
        },
        {
          "id": "9C05E62330314A61B75F34E5EDB92DA0",
          "name": "__Standard Values",
          "path": "/sitecore/templates/Project/MvpSite/Page/__Standard Values"
        },
        {
          "id": "0D97B45DC5894495A4959AAFF4FBD2C3",
          "name": "About",
          "path": "/sitecore/content/MvpSite/Home/About"
        },
        {
          "id": "B7B943C4555D40E19677C68726686119",
          "name": "Admin",
          "path": "/sitecore/content/MvpSite/Home/Application/Admin"
        }
      ]
    }
  }
}

The where argument can also be made more complex by combining multiple ItemSearchPredicateInput types using AND and OR. For example, I could search for all items based on the Page template that have the field IncludeInMenu set to 1:

query Pages {
  search(
    first: 5
    where: {
      AND: [
        { name: "_templates", value: "A054184C3B204C2BAB04C8A03ACD5522" }
        { name: "IncludeInMenu", value: "1" }
      ]
    }
    orderBy: { name: "_name", direction: ASC }
  ) {
    pageInfo {
      hasNext
      endCursor
    }
    results {
      id
      name
      path
    }
  }
}

Now only items that match both predicates are returned:

{
  "data": {
    "search": {
      "pageInfo": {
        "hasNext": true,
        "endCursor": "eyJzZWFyY2hBZnRlciI6WyJhcHBseSIsIkIzRDM2OTg4QjRBRTQ5MTVBNkNGRjUxMzNCRTRFM
          TAwIiwiQjNEMzY5ODhCNEFFNDkxNUE2Q0ZGNTEzM0JFNEUxMDAiXSwiY291bnQiOjV9"
      },
      "results": [
        {
          "id": "9AC6DDA387854C63A2AB457D8CC56D19",
          "name": "$name",
          "path": "/sitecore/templates/Branches/Project/MvpSite/Page/$name"
        },
        {
          "id": "9C05E62330314A61B75F34E5EDB92DA0",
          "name": "__Standard Values",
          "path": "/sitecore/templates/Project/MvpSite/Page/__Standard Values"
        },
        {
          "id": "0D97B45DC5894495A4959AAFF4FBD2C3",
          "name": "About",
          "path": "/sitecore/content/MvpSite/Home/About"
        },
        {
          "id": "0068B8802204475995190ED217BBC156",
          "name": "Application",
          "path": "/sitecore/content/MvpSite/Home/Application"
        },
        {
          "id": "B3D36988B4AE4915A6CFF5133BE4E100",
          "name": "Apply",
          "path": "/sitecore/content/MvpSite/Home/Application/Apply"
        }
      ]
    }
  }
}

The name field of a ItemSearchPredicateInput type can include any field of the item, as well as these predefined fields:

  • _name for the item name.
  • _path for the item path.
  • _parent for the item parent IDs.
  • _templates for the item template ID or any base template ID in the template hierarchy.
  • _hasLayout to check if the item has layout defined.
  • _language for the language of the item.

These predefined fields are documented at https://doc.sitecore.com/xp/en/developers/hd/210/sitecore-headless-development/the-experience-edge-schema.html#available-fields-for-content-search.

The Sites Query

This is a new query type made available with the XM Edge connector version 21 and later. It allows querying all defined sites for the tenant and accessing detailed information for the site, such as error handling information, the root path of the site in the content tree or several other pieces.

Firstly, let's discover all the sites in this Edge tenant:

query Sites {
  site {
    siteInfoCollection {
      name
    }
  }
}

This will list all sites.

{
  "data": {
    "site": {
      "siteInfoCollection": [
        {
          "name": "sugcon-anz"
        },
        {
          "name": "sugconeu"
        },
        {
          "name": "sugcon-eu"
        },
        {
          "name": "website"
        },
        {
          "name": "mvp-site"
        },
        {
          "name": "sugconanz"
        }
      ]
    }
  }
}

I can also query directly for a single site, if I already have it's name:

query Sites {
  site {
    siteInfo(site: "mvp-site") {
      rootPath
      routes(language: "en", first: 5) {
        pageInfo {
          hasNext
          endCursor
        }
        results {
          routePath
        }
      }
    }
  }
}

This query will return a paged list of the routes for the mvp-site site.

{
  "data": {
    "site": {
      "siteInfo": {
        "rootPath": "/sitecore/content/MvpSite",
        "routes": {
          "pageInfo": {
            "hasNext": true,
            "endCursor": "eyJzZWFyY2hBZnRlciI6WzE2NjI1NjMzMDAwMDAsIkJBOTVCOERGRTQyMzRDMUVCQTNEN
               DA2NDg5RjIzQUY2IiwiQkE5NUI4REZFNDIzNEMxRUJBM0Q0MDY0ODlGMjNBRjYiXSwiY291bnQiOjV9"
          },
          "results": [
            {
              "routePath": "/Application/Admin/Roles/New"
            },
            {
              "routePath": "/Application/My-Data"
            },
            {
              "routePath": "/Application/Apply"
            },
            {
              "routePath": "/Application/Admin/Mvp-Types"
            },
            {
              "routePath": "/Application/Admin/Users/Edit"
            }
          ]
        }
      }
    }
  }
}

Reference

If you'd like to see some queries from a real XM headless implementation, the source code for the sites mentioned at the top of this post is available on github at https://github.com/Sitecore/XM-Cloud-Introduction.

There are also more example queries for XM tenants in the documentation at https://doc.sitecore.com/xp/en/developers/hd/210/sitecore-headless-development/query-examples.html.

Comments

Leave a comment

All fields are required.