Querying Umbraco Heartcore with GraphQL

    In this tutorial we will be looking at how we can fetch data from Umbraco Heartcore using GraphQL.

    We will be using https://demo.heartcore.dev/ as a reference.

    At the end of the tutorial we should be able to use the Umbraco Heartcore GraphQL API and be able to fetch all the content needed to render the page in with a single query.

    Creating the Document Types

    First, we will need to create some Document Types, and as a start, we will create some element types.

    Start by creating a folder named Elements under the Document Types folder in the Settings section of the Backoffice.

    In that folder create the following Document Types:

    Text and Image

    Alias: textAndImage

    Text and Image Document Type

    Add a new group called Content with the following properties:

    NameAliasProperty EditorProperty Editor Configuration
    TitletitleTextstringUse defaults
    TexttextRich Text EditorUse defaults
    ImageimageMedia PickerPick multiple items: not checked
    Pick only images: checked
    Disable folder select: checked
    Show large imageshowLargeImageCheckboxUse defaults

    Then under permissions check Element Type

    Unique Selling Point

    Alias: uniqueSellingPoint

    Unique Selling Point Document Type

    Add a new group called Content with the following properties:

    NameAliasProperty EditorProperty Editor Configuration
    ImageimageMedia PickerPick multiple items: not checked
    Pick only images: checked
    Disable folder select: checked
    TitletitleTextstringUse defaults
    TexttextTextareaUse defaults
    LinklinkMulti Url PickerMax number of items: 1

    Then under permissions check Element Type


    Then create another folder called Compositions and create the following Document Types in that folder:

    Elements Composition

    Alias: elementsComposition

    Elements Composition Document Type

    Add a new group called Elements with the following properties:

    NameAliasProperty EditorProperty Editor Configuration
    ElementselementsNested ContentDocument Types:
    Element Type: Text and image, Group: Content, Template: {{title}}
    Confirm Deletes: checked
    Show icons: checked
    Hide label: checked

    Nested Content Configured with Elements

    Then click the Reorder button and change the value for Elements from 0 to 15

    Elements Composition Reorder

    Hero Composition

    Alias: heroComposition

    Hero Composition Document Type

    Add a new group called Hero with the following properties:

    NameAliasProperty EditorProperty Editor Configuration
    ImageheroImageMedia PickerPick multiple items: not checked
    Pick only images: checked
    Disable folder select: checked
    TitleheroTitleTextstringUse defaults
    SubtitleheroSubtitleTextstringUse defaults

    Unique Selling Points Composition

    Alias: uniqueSellingPointsComposition

    Unique Selling Points Composition Document Type

    Add a new group called Unique Selling Points with the following properties:

    NameAliasProperty EditorProperty Editor Configuration
    TitleuniqueSellingPointsTitleTextstringDocument Types:
    Use defaulst
    Unique Selling PointsuniqueSellingPointsNested ContentDocument Types:
    Element Type: Unique Selling Point, Group: Content, Template: {{title}}
    Confirm Deletes: checked
    Show icons: not checked
    Hide label: checked

    Nested Content Configured with Unique Selling Points

    Then click the Reorder button and change the value for Elements from 0 to 20

    Unique Selling Points Composition Reorder


    At the root create the following Document Types:

    Textpage

    Alias: textpage

    Textpage Document Type

    Click on Compositions and select Elements Composition and Hero Composition

    Textpage Compositions

    Frontpage

    Alias: frontpage

    Frontpage Document Type

    Add a new group called Footer with the following properties:

    NameAliasProperty EditorProperty Editor Configuration
    TitlefooterTitleTextstringUse defaults
    LinksfooterLinksMulti Url PickerUse defaults

    Then click on Compositions and select Elements Composition, Hero Composition and Unique Selling Points Composition, click on Submit.

    Frontpage Compositions

    On the permissions tab check Allow at root and add Textpage to the Allowed child node types property.

    Frontpage Permissions

    Then go to the Content section and create a new Frontpage with the name Home and create some subpages.

    Querying the GraphQL API

    The GraphQL endpoint accepts POST requests with the content type application/json, the body should be an object containing as a minimum a query property, e.g.

    {
      "query": "..."
    }

    For the rest of this tutorial the GraphQL queries are written in plain text that can be executed with the GraphQL Playground.

    Lets start with a basic query that fetches the name, url, and heroTitle properties from the Frontpage.

    {
      frontpage(url: "/home/") {
        name
        url
        heroTitle
      }
    }

    Result:

    {
      "data": {
        "frontpage": {
          "name": "Home",
          "url": "/home/",
          "heroTitle": "Umbraco Heartcore"
        }
      }
    }

    Making the Query Generic

    Up until now, we have been working on a single document type which in most cases is fine, but since we want to dynamically fetch content based on the url we can use the content field.

    The content field returns the Content interface type that contains the default fields. To allow us to query the fields on the derived types we can use fragments.

    A fragment allows us to query data on the underlying concrete type.

    {
      content(url: "/home/") {
        name
        ... on Frontpage {
          heroTitle
          heroSubtitle
          heroImage {
            url(width: 1980, height: 430, cropMode: CROP)
          }
        }
      }
    }

    This returns the following JSON:

    {
      "data": {
        "content": {
          "name": "Home",
          "heroTitle": "Umbraco Heartcore",
          "heroSubtitle": "Umbraco Heartcore is a headless CMS by Umbraco. With Heartcore, you can use the Umbraco backoffice to manage content and media that's ready to be displayed on any device.",
          "heroImage": {
            "url": "https://media.umbraco.io/demo-headless/8d832c5cee78cf8/umbraco-heartcore-preview.png?mode=crop&width=1980&height=430&upscale=false"
          }
        }
      }
    }

    The query is fetching the name field which exists on the Content interface, it also fetches the heroTitle, heroSubtitle and heroImage on the Frontpage type.

    Since the heroImage is a Media picker we can pass arguments to the url field telling the server to generate an url with the Image Cropper query string parameters.

    Querying Composition Fields

    There is one problem with our query though. Try changing the url argument to a subpage and see whats happening.

    As you can see we don't get any data back for the hero fields, even though our Document Type has those fields. This is because we are telling GraphQL that we only want them on the Frontpage type. You might be tempted to fix this by adding another fragment on the Textpage type which would work, but remember we created a Document Type that only contains hero fields which we have added as a Composition to both the Frontpage and Textpage Document Types. We can change the ... on Frontpage fragment to ... on HeroComposition and we will now get the expected data back.

    Now our query looks like this:

    {
      content(url: "/home/") {
        name
        ... on HeroComposition {
          heroTitle
          heroSubtitle
          heroImage {
            url(width: 1980, height: 430, cropMode: CROP)
          }
        }
      }
    }

    Querying Nested Content

    Since Nested Content is an interface named Element we can query the fields by the specific type using fragments.

    {
      frontpage(url: "/home/") {
        elements {
          ... on TextAndImage {
            title
            text
            showLargeImage
            image {
              small: url(width: 320, height: 240, cropMode: CROP)
              medium: url(width: 480, height: 360, cropMode: CROP)
              large: url(width: 1024, height: 768, cropMode: CROP)
            }
          }
        }
      }
    }

    Now try to write a query that fetches the title, text, link and image fields for the Unique Selling Points property.

    The only thing we are missing now to be able to render the complete page is the main navigation and the footer.

    The main navigation is showing the children of the frontpage, we can fetch them with the following query:

    {
      frontpage(url: "/home/") {
        children {
          edges {
            node {
              name
              url
            }
          }
        }
      }
    }

    The footer can be fetched using the following query:

    {
      frontpage(url: "/home/") {
        footerTitle
        footerLinks {
          name
          target
          type
          url
        }
      }
    }

    We can even combine the two queries into a single query:

    {
      global: frontpage(url: "/home/") {
        mainNavigation: children {
          edges {
            node {
              name
              url
            }
          }
        }
        footerTitle
        footerLinks {
          name
          target
          type
          url
        }
      }
    }

    In the query above we are also using field aliases, this means that in the response data frontpage will be named global and children will be named mainNavigation.

    {
      "data": {
        "global": {
          "mainNavigation": {
            "edges": [
              {
                "node": {
                  "name": "What is a Headless CMS?",
                  "url": "/home/what-is-a-headless-cms/"
                }
              },
              {
                "node": {
                  "name": "What you get with Umbraco Heartcore",
                  "url": "/home/what-you-get-with-umbraco-heartcore/"
                }
              }
            ]
          },
          "footerTitle": "About Umbraco",
          "footerLinks": [
            {
              "name": "Umbraco Website",
              "target": "_blank",
              "type": "EXTERNAL",
              "url": "https://umbraco.com/products/umbraco-heartcore"
            },
            {
              "name": "Umbraco Heartcore Documentation",
              "target": "_blank",
              "type": "EXTERNAL",
              "url": "https://our.umbraco.com/documentation/Umbraco-Heartcore/"
            }
          ]
        }
      }
    }

    Using Variables

    You might have noticed that the url argument is hardcoded in the query. This means that we will always get back the Content for that url. What we want is to pass the argument to the query dynamically.

    We can do this by altering the query to include a variable instead of the hardcoded value, this can be done by replacing the argument with $url and wrapping the query with query($url: String).

    query ($url: String!){
      content(url: $url) {
        name
        ... on HeroComposition {
          heroTitle
          heroSubtitle
          heroImage {
            url(width: 1980, height: 430, cropMode: CROP)
          }
        }
      }
    }

    And then we pass the variable in a separate JSON property called variables

    {
      "query": "...",
      "variables": {
        "url": "/home/"
      }
    }

    Putting it all together

    Now that we have all the individual parts we can combine it all into a single query that fetches all the data needed to display the pages on our site:

    query($url: String!) {
      content(url: $url) {
        ...Hero
        ...UnigueSellingPoints
        ...Elements
      }
      global: frontpage(url: "/home/") {
        mainNavigation: children {
          edges {
            node {
              name
              url
            }
          }
        }
        footerTitle
        footerLinks {
          name
          target
          type
          url
        }
      }
    }
    
    fragment Hero on HeroComposition {
      heroTitle
      heroSubtitle
      heroImage {
        url(width: 1980, height: 430, cropMode: CROP)
      }
    }
    
    fragment Elements on ElementsComposition {
      elements {
        ... on TextAndImage {
          title
          text
          showLargeImage
          image {
            small: url(width: 320, height: 240, cropMode: CROP)
            medium: url(width: 480, height: 360, cropMode: CROP)
            large: url(width: 1024, height: 768, cropMode: CROP)
          }
        }
      }
    }
    
    fragment UnigueSellingPoints on UniqueSellingPointsComposition {
      uniqueSellingPointsTitle
      uniqueSellingPoints {
        ... on UniqueSellingPoint {
          title
          text
          link {
            name
            target
            type
            url
          }
          image {
            url
          }
        }
      }
    }

    Variables:

    {
      "url":"/home/"
    }