Querying with GraphQL
Learn how to query 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.
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:
Alias: textAndImage

Text and Image Document Type
Add a new group called Content with the following properties:
Name | Alias | Property Editor | Property Editor Configuration |
---|---|---|---|
Title | title | Textstring | Use defaults |
Text | text | Rich Text Editor | Use defaults |
Image | image | Media Picker | Pick multiple items: not checked
Pick only images: checked
Disable folder select: checked |
Show large image | showLargeImage | Checkbox | Use defaults |
Then under permissions check Element Type
Alias: uniqueSellingPoint

Unique Selling Point Document Type
Add a new group called Content with the following properties:
Name | Alias | Property Editor | Property Editor Configuration |
---|---|---|---|
Image | image | Media Picker | Pick multiple items: not checked
Pick only images: checked
Disable folder select: checked |
Title | title | Textstring | Use defaults |
Text | text | Textarea | Use defaults |
Link | link | Multi Url Picker | Max 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:
Alias: elementsComposition

Elements Composition Document Type
Add a new group called Elements with the following properties:
Name | Alias | Property Editor | Property Editor Configuration |
---|---|---|---|
Elements | elements | Nested Content | Document 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
Alias: heroComposition

Hero Composition Document Type
Add a new group called Hero with the following properties:
Name | Alias | Property Editor | Property Editor Configuration |
---|---|---|---|
Image | heroImage | Media Picker | Pick multiple items: not checked
Pick only images: checked
Disable folder select: checked |
Title | heroTitle | Textstring | Use defaults |
Subtitle | heroSubtitle | Textstring | Use defaults |
Alias: uniqueSellingPointsComposition

Unique Selling Points Composition Document Type
Add a new group called Unique Selling Points with the following properties:
Name | Alias | Property Editor | Property Editor Configuration |
---|---|---|---|
Title | uniqueSellingPointsTitle | Textstring | Document Types:
Use defaulst |
Unique Selling Points | uniqueSellingPoints | Nested Content | Document 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:
Alias: textpage

Textpage Document Type
Click on Compositions and select Elements Composition and Hero Composition

Textpage Compositions
Alias: frontpage

Frontpage Document Type
Add a new group called Footer with the following properties:
Name | Alias | Property Editor | Property Editor Configuration |
---|---|---|---|
Title | footerTitle | Textstring | Use defaults |
Links | footerLinks | Multi Url Picker | Use 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.
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"
}
}
}
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.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)
}
}
}
}
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://docs.umbraco.com/umbraco-heartcore/"
}
]
}
}
}
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/"
}
}
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/"
}
Last modified 1mo ago