[Series: Headless Site on Optimizely Graph] Exploring interesting ways we can query the content

blog header image

Look up in the sky! It's a bird! No, wait - it's Optimizely Graph! This is part 2 in the blog post series where we explore how a fully functioning headless site can be build in .NET core using Optimizely Graph, and in this post we'll see how Optimizely Graph is both a powerful search & query engine (on par with good old Episerver Find) - but also how it can fully replace the content delivery API.

Building a .NET Core headless site on Optimizely Graph and SaaS CMS

This blogpost is part of a series of blogposts and open-source code where I build a headless site using .NET Core and Optimizely Content Graph on Optimizely SaaS CMS.

These are the posts so far:

  1. Getting started - importing Alloy content to a SaaS CMS
  2. Querying the Content Graph

The first post started out a bit soft, by simple importing the content from good ol' Alloy into Optimizely SaaS CMS - and hence also to the built-in Graph index, and I ended it with a quick video showing how you can use the built-in tools to examine the graph.

But - if you've ever worked with GraphQL before, you'll know that there is a lot more to it than that. Here I'll explore some of those options - and try to approach how we can query it to get the content we need for our headless site. In this post I'll try to restrict myself to focussing on the stuff we'll need to accomplish the goal of this series - having some version of Alloy running on a headless .NET Core site using Graph. Optimizely Content Graph has a tons of features and I strongly recommend you to check out the official documentation here, as I'll only touch on a few of them: https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/docs/introduction-optimizely-graph

ContentGraph currently comes in 2 'flavors'

Before we dive in too deeply in the details of the ContentGraph and how you can query it, I find it worth noting that the schema of the Optimizely CMS ContentGraph currently exists in 2 different versions. 

  1. If you were to request a Graph index as a free trial (or get it through DXP), and install the integration in your CMS 12, you'd notice that when you explore the index that the content looks a lot like the models you are currently used to. You have IContent types, and all the fields are more or less where you expect them to be - and similarly named.
  2. But, if you instead get to spin up the new SaaS CMS (that I'm using for this demo) the content models are a bit more removed from old-style CMS, as it's prepared for a more generic understanding of content management - which shines through both in field naming and structure of the models. Don't worry - you essentially get the same, but you might have to look twice to find it.
    For example, you'll find a lot of the default fields (like name, url, key, status) on a content item under the "_metadata" node and not directly in the root.

 

Experimenting with queries

Now we are close to ready to start playing around with ContentGraph. If you are using the SaaS CMS, or have installed the integration to your regular CMS 12, you can find access to the GraphiQL playground in your UI. 
Otherwise, you can also just browse to "https://cg.optimizely.com/app/graphiql?auth=[your single key]". 
Essentially, there are 2 ways of authentication to your index. When you get the index you get an AppKey, a Secret and a Single Key. 
The Single Key let's you do read-only queries against all content in the Graph that is publicly available (everyone can read and it is published). 
If you want to write to the index, or access restricted content you need to use the Key and Secret in an authorization header (base64 encoded, Basic authentication style). Typically this would be for cases where you want to index custom content from outside optimizely (really fun), subscribe to webhooks or perhaps setup link-mappings and similar in the index. 
Previewing unpublished content can also be done with a special preview token - but we can perhaps dive into that a bit later. But I digress - key thing here is that to get started, get your single key and go to the GraphiQL interface - it's a great place to experiment and learn.

Minimized results

A major difference to how we are used to working with both Find (Search & Navigation) but also the normal ContentDelivery API, is that in both of those approaches you by default get 'everything' for each document you ask for. However - with Graph, you only get returned what you ask for. And - there is no 'wildcard'. If you don't request a field specifically in the query you don't get it. At first I found this a bit of an annoyance - but I've rather started to like it - as it keeps traffic to a minimum and you really have to consider which queries you send.

query MyQuery {
  _Content {
    items {
      _metadata {
        displayName
        url {
          hierarchical
        }
        key
        created
        lastModified
        locale
        status
        types
      }
    }
  }
}

Take a look at this query, for example. In here I'm asking that for every single item of the type "_Content" it should return the metadata properties DisplayName, hierarchical url, key, created, lastmodified and so on. I have to specify every single field needed.
The downside is that this would mean that you could end up having to write a lot of very detailed custom queries for every single scenario you'd need content and for every single content type. Something that's screams maintenance nightmare. However, there are some tricks to help you with this...

Fragments are one of the most important concepts to understand here - because these are essentially where you can define a reusable definition of what fields to get for a given property/type. Think of these as the building blocks you use to construct your query - and you can have as many of them as you want.

For example - on my Alloy site, I know that every page that is a StandardPage (or inherits from it) will have a MainBody field and a TeaserText field. Now, I want to make sure that whenever I query the _Content type and it is of a type that is based on the StandardPage I want those properties included. To do that, I simply create a general building block - in this example I call it "standardPageFragment" and include it outside my query block. And then I can simply reference it by calling "...standardPageFragment" inside my query. Like this:

query MyQuery {
  _Content {
    items {
      _metadata {
        displayName
        url {
          hierarchical
        }
        key
        created
        lastModified
        locale
        status
        types
      }
      ...standardPageFragment
    }
  }
}
  fragment standardPageFragment on StandardPage{
  	MainBody{
      html
    }
    TeaserText
  }

Something worth noting is that every type can be defined into fragments. Notice for example how the XhtmlString in the new SaaS-schema is an object where I have to fetch the "html" field of? The other field is "json" because you can also get it in structure json if you want to have your own parsing. Now, I could also just define a "richTextFragment" to make sure I always get exactly the property I want from it - and yes, then I could use a fragment inside a fragment - and so on.
This gets extremely useful when dealing with ContentAreas - as you can essentially define fragments that has other fragments inside them - and even themselfes, recursively.  And - with a ContentArea it's of course possible to have Blocks that also has content areas inside them - with blocks with content areas and so on. You can for example take a look here:

query MyQuery {
  _Content{
    items{
      ...standardPageFragment
    }
  }
}

fragment standardPageFragment on StandardPage{
  MainContentArea{
    ...iContentFragment
  }
}
fragment iContentFragment on _IContent{
  _metadata{
    displayName
  }
	... on StandardPage{
    MainContentArea{
      ...iContentFragment
    }
  }
}

Note, that recursiveness tends to get a bit tricky, but it can certainly be made to work - however there are some caveats and it's a good idea to consult the documentation (although in my experience it might be a bit behind).

Parametized queries

When you are building your queries you of course want to pass in parameters. Let's say, for example that I want to do a query where I list the displayName of all the product pages.

Now, I could of course just query the ProductPage type (and not _Content) - but the ProductPage type does not have the _metadata with the displayName. 
So - for this I am still going to query all _Content - but add a filter parameter to limit it to a specific type, like this:

query MyQuery {
  _Content(where: { _metadata: { types: { eq: "ProductPage" } } }) {
    items {
      _metadata {
        displayName
      }
    }
  }
}

This is where we really start to meet the functionality of Episerver Find - because you can add all kinds of filtering, sorting and functionality as parameters, as well as doing full-text and semantic search in many different languages. I won't dive too deep into those parts yet, but maybe in a future post. Same goes for facets which is extremely powerful - but out of scope for this quick introduction.
But - if we stick with the basic filtering, something we will need is to be able to filter for a specific page given a url path. And not only do we need to filter for it - we also need to be able to pass in different paths to our GraphQL query - which means we have to add it as a parameter to our query itself. Like this:

Content Hierarchy 

One more thing we need to touch on, is how to get the content tree out from the ContentGraph? That's pretty essential if we want to do stuff like list links to the child pages in navigation.

Well, luckily it's fairly easy using Linking. 
There is a default parent/child link setup for _Content the ContentGraph so if we just query the "_link" field on a page, we will get the children. And - of course we can also filter on the children if we want to sort them a specific way - or only return some of the children - or perhaps use pagination.

query MyQuery($path: String) {
  _Page(where: { _metadata: { url: { hierarchical: { eq: $path } } } }) {
    items {
      _metadata {
        displayName
        url {
          hierarchical
        }
      }
      _link {
        _Page {
          items {
            _metadata {
              displayName
            }
          }
        }
      }
    }
  }
}

Linking is a really powerful feature though, and you can also setup custom relationship between objects in your graph and use Linking to query them together. Once again - I encourage you to check out the documentation on this.

Introspection

Now, before this posts gets too long - let me just 'warm you up' for the next post in the series. Because by now you might be a bit overwhelmed and thinking "this looks like a lot of code". At least that's how I felt when I was introduced to it. Imagining that adding a new property to a page in the CMS would also require me to add it to a similar model object on the client, perhaps to some mapping logic in the integration and to numerous queries sent to the graph. Sounds like a nightmare to maintain, right? 
Well, the obvious thing to do here is to automate it of course. And of course the clever folks at Optimizely thought about that as well - so they made this: https://github.com/episerver/graph-net-sdk which has a CLI that'll help you do just that - or at least partially. This tool will generate client c# model classes for you - but not the queries you need to use them.

And - while I appreciate the effort, the model classes it generated are pretty static and didn't fit all my needs to how I wanted them - so I ended up putting together my own tool for generating models and queries - and I will share that in the next post in this series. 
Until then, I'll leave you with one last query - one very similar to what the tool uses. It's an introspection query that'll ask the ContentGraph to return all the types it has in it's schema - and the fields they contain.

query Introspection{
  __schema {
  	queryType {
      name
      fields {
        name
      }
    }
    types {
      name
      fields {
        name
        type{
          name
          kind
          ofType{
            name
            kind
          }
        }
      }
    }
  }
}	
Recent posts