March 06, 2021

Hierarchies, Graphs, and Navigation

By Ronald Aveling & Knut Melvær

Relating content in a structured way creates meaning. But what about the meaning we make when we shape content into hierarchies?

This guide explores the value of hierarchies, how they differ from other content structures, and how to best use them for navigation and beyond.

In the last chapter of our content modeling series, we learned a lot about building fields and relationships in an open, flexible way.

But sometimes you need to order things into hierarchical parent-child structures. Let‘s take a closer look at the difference between the two approaches, when order is a good idea, and what influence navigation concerns should have on the way we build hierarchies.

Graphs and Trees explained

Graphs

When different types of content are linked together in a network of relationships they form a graph.

A graph structure. Each thing can relate to any number of other things.

Graphs are everywhere in the world of data modeling, but they’re also super common in nature. A food web is a form of a graph defined by who eats who within an ecosystem.

Aquatic food web by Kestin Schulz, Mariya W. Smit, Lydie Herfort and Holly M. Simon (source: https://bit.ly/3efvzUq)

We recently created a graph of relationships in our content model, where the structure of our graph is a function of the value we seek to deliver through it.

CandiCorp’s content graph.

Graphs are really useful, but sometimes you need an organizing principle defined by hierarchical rules. That’s where trees come in.

Trees

Trees are just a hierarchical form of a graph. They’re usually bound by the rule that any node in a tree structure can have only one “parent”, but multiple “children”.

A hierarchical “Tree”. Each item has only one parent, up to the “root’ item.

Protip

Sanity is naturally graph-like in the way it organizes content. You’re never forced into formal structures, but you can make them when you need to.

Why hierarchies are useful

If you’ve ever found what you’re after within an organizational chart or site map, you’ve successfully navigated a tree. You’re also tree-making when you create nested folder structures on your computer. They’re a really handy tool for finding your way through a bunch of stuff, and their usefulness is comprised of three things:

  • What it contains (is it a useful collection?).
  • The organizing principles behind it (are things grouped in a way that makes sense to the user).
  • How well its “root”, “leaves” and “branches” are labeled.

The limits of hierarchy

Sometimes an item in a collection isn’t always suited to the organizing principles by which it is bound. As an example, pianos have strings but are often classified as percussion instruments due to the way they are played. In truth, pianos aren’t a great fit for either group, and their existence places pressure on the rules of the system.

Sometimes the best thing to do is make a new branch to accommodate your edge case, but at a certain point more branches = more confusion. Boundary conditions like these are hard to avoid, so when you encounter them try to balance the needs of the thing you’re categorizing against the needs of the collection. Most of the time you have to shoehorn a few things into funny slots in order to preserve the organizing principle.

Ways to create hierarchies with Sanity.io

Declare a parent

To set up a child-to-parent relationship, we just make a reference to the same content type.

// schemas/documents/department.js

export default {
  title: 'Department',
  name: 'department',
  type: 'document',
  fields: [
    { title: 'Title', name: 'title', type: 'string', },
    {
      title: 'Parent Department',
      name: 'parentDepartment',
      type: 'reference',
      to: [
        {type: 'department'},
      ]
    }
  ]
}

This approach is worthwhile if the overall structure of your tree is known in advance.

Protip

If you need to contain every published document within a tree, make the parent selector field mandatory with validation.

Declare many children

We can flip the previous example on its head with a parent-to-child relationship. Instead of referencing a single item, just change it to an array:

// schemas/documents/department.js

export default {
  title: 'Department',
  name: 'department',
  type: 'document',
  fields: [
    { title: 'Title', name: 'title', type: 'string', },
    {
      title: 'Child departments',
      name: 'childDepartments',
      type: 'array',
      of: [
        {
          type: 'reference',
          to: [
            {type: 'department'},
          ]
        }
      ]
    }
  ]
}

It’s faster to connect things this way, but if you’re not careful you end up with documents containing multiple parents. Also, every time you want to move a “leaf” to a different “branch” you need to make the change in two places.

Declare many parents

Sometimes referencing more than one parent is helpful. These relationships are called polyhierarchies. They’re common in e-commerce menus, where a user might expect to find a product in more than one place.

A child item with two (red and green) parents.

Bear in mind, you’re kinda “breaking the rules” of an organized system when you do this, and at a certain point, your tree becomes an informal graph again. When taking this approach it‘s often a good idea to list a "primary parent" to retain the formal qualities of your tree. This approach is handy when you need to provide breadcrumb navigation from a polyhierarchy.

Make a Table of Contents

You can also create a separate content type to handle hierarchies. From here it’s possible to reference any document from any number of types. This approach is handy if you need to apply a different layer of meaning to your content’s fundamental structure. You can think of this content type as an “organizing lens” that shows you a structure when you look through it. The advantage is that it frees you to make many “lenses” and reuse your content in many different hierarchies.

Website menus are a great use case. This method will let you quickly make a tree of links spanning different types without having to alter fields in the documents themselves. If you want to try out an alternate menu structure just make a new Table of Contents document and point your front end to the new version.

Protip

To limit this document type to a single instance, make it a singleton with Structure Builder.

Gotcha

Because this approach is decoupled from your main content structure, it can be easy to lose sync with new or changed documents.

// schemas/documents/toc.js

export default {
  name: 'toc',
  type: 'document',
  title: 'Table of Contents',
  fields: [
    {
      type: 'string',
      name: 'title',
      title: 'Title',
    },
    {
      type: 'array',
      name: 'sections',
      title: 'Sections',
      of: [{ type: 'tocSection' }]
    }
  ]
}
// schemas/objects/tocSection.js

export default {
  name: 'tocSection',
  type: 'object',
  title: 'Section',
  fields: [
    {
      type: 'reference',
      name: 'target',
      title: 'Target',
      to: [
        { type: 'department'},
        // etc
      ]
    },
    {
      type: 'string',
      name: 'title',
      title: 'Title'
      // to override title from referenced items
    },
    {
      type: 'array',
      name: 'links',
      title: 'Links',
      of: [{ type: 'tocLink' }]
    }
  ]
}
// schemas/objects/tocLink.js

export default {
  title: 'TOC link',
  name: 'tocLink',
  type: 'object',
  fields: [
    {
      type: 'reference',
      name: 'target',
      title: 'Target',
      to: [
        { type: 'department'},
        // etc
      ],
    },
    {
      type: 'string',
      name: 'title',
      title: 'Title',
      // to override title from referenced items
    },
    {
      type: 'array',
      name: 'children',
      title: 'Children',
      of: [{ type: 'tocLink' }]
    }
  ],
}

Navigation and content structure doesn’t have to be the same thing

The Table of Contents above lets you provide a hierarchical model for a certain audience without changing the fundamentals of your content graph. This headless way of thinking about navigation can let you:

  • Test and iterate on new ideas without having to rebuild your core structure.
  • Provide different hierarchies for different channels (voice, search, and print to name a few).

What we learned

We looked at the difference between graphs and trees, explored different ways to build trees with Sanity, and why separating navigation concerns from structure is usually a good idea.

Next up, we’ll explore different ways to support personalized content with Sanity.

Other guides by authors