How to create nested posts with ordered sub-posts in Sanity schema?

10 replies
Last updated: Aug 8, 2022
Hello everyone 👋 I'm pretty new to Sanity and its schema architecture and I've been trying to find any solution regarding my query but couldn't.Basically, I'm trying to make a blog platform similar to
Buildspace where there are "posts" and within those posts, there are "sub posts". How would I go about creating a schema that makes this possible? My initial thought was to have a sub post array in the Post schema that references another schema of SubPost, but I also want to have control over the order of sub posts within a post, and would also like to structure sub posts nicely for scalability purposes.
What would be the most straightforward solution for this? I've attached a picture regarding the type of thing I'm trying to build. Any help would be immensely helpful!
AI Update

Looking at your question about creating a blog platform with posts and sub-posts (similar to Buildspace), you have a couple of solid approaches in Sanity. Let me walk you through the most straightforward solution that gives you ordering control and good scalability.

Based on your needs, I'd recommend using an array of references in your Post schema. This is the pattern that works best for your use case because:

  1. Built-in ordering: Arrays in Sanity are inherently orderable with drag-and-drop functionality, so you get ordering control out of the box
  2. Scalability: Sub-posts remain independent documents that can be reused or managed separately
  3. Clean queries: Easy to fetch and traverse with GROQ

Here's how to structure it:

// schemas/post.ts
export default defineType({
  name: 'post',
  type: 'document',
  title: 'Post',
  fields: [
    defineField({
      name: 'title',
      type: 'string',
      title: 'Title'
    }),
    defineField({
      name: 'subPosts',
      type: 'array',
      title: 'Sub Posts',
      of: [{
        type: 'reference',
        to: [{type: 'subPost'}]
      }]
    })
  ]
})

// schemas/subPost.ts
export default defineType({
  name: 'subPost',
  type: 'document',
  title: 'Sub Post',
  fields: [
    defineField({
      name: 'title',
      type: 'string',
      title: 'Title'
    }),
    defineField({
      name: 'content',
      type: 'array',
      of: [{type: 'block'}]
    })
  ]
})

Querying the Data

To fetch posts with their ordered sub-posts, use this GROQ query:

*[_type == "post"] {
  _id,
  title,
  subPosts[]-> {
    _id,
    title,
    content
  }
}

The []-> syntax dereferences the array of references, fetching the full sub-post documents while maintaining the order you set in the Studio.

Why This Works Well

  • Ordering: The array maintains order via drag-and-drop in the Studio. Each item has a _key property that Sanity uses internally to track position
  • Scalability: Sub-posts are separate documents, so you can:
    • Query them independently if needed
    • Potentially reuse them across multiple posts (if that makes sense for your use case)
    • Keep your data normalized
  • Studio UX: You get a clean interface where you can select existing sub-posts or create new ones inline

Alternative: Inline Objects

If sub-posts will never be reused and are truly just components of a parent post, you could use inline objects instead:

defineField({
  name: 'subPosts',
  type: 'array',
  of: [{
    type: 'object',
    fields: [
      {name: 'title', type: 'string'},
      {name: 'content', type: 'array', of: [{type: 'block'}]}
    ]
  }]
})

This is simpler but less flexible—sub-posts can't exist independently or be queried separately.

The Pattern in Action

The approach I'm recommending is actually a common Sanity pattern for creating ordered collections. As one Sanity team member explained: "The usual approach is to make an 'order' document that holds an array of references to the document type you want to order."

In your case, the Post document serves as that ordering container for SubPosts. This gives you the best of both worlds: the independence of separate documents with the ordering control of arrays.

Good luck with your blog platform! Feel free to ask if you need help with querying or displaying this structure.

Show original thread
10 replies
Hey there, I’m doing something very similar with my ‘project’ doc schema, which has an array of ‘sections’, one of which is a ‘projectStructure’ object schema.Each of which has a ‘title’ field and an ‘aspects’ array field of type ‘projectStructureAspect’.
Each of which (in your case) could have whatever fields are relevant to you for lessons, like a ‘name’ string field and ‘text’ array of type ‘block’ (for a general rich text editor).
You can browse
my repo here to get an idea. Just pay attention to the ‘project’ schema and directory, copy/edit as necessary, and play with how Studio compiles it, doing a hard browser refresh sometimes to test changes.
Hello, thank you for the reply! I was actually thinking of something similar but since I need to control the order of the sub posts, I was wondering if an array type will retain the order of the subposts. Will I also be able to reorder the sub posts from sanity studio?
Yes, you can natively reorder items in an array.So it looks like you’d have 3 files:
• ‘project’ (document), fields: title, array of type:
• ‘step’ (object), fields: title, array of type:
• ‘lesson’ (object), fields: heading, subheading, body
Yes, I'm going to keep it simple for now and just have a "project" schema that has an array of type "lesson". Thanks a lot for the help, appreciate it :)
Sure! If individual lessons are ONLY relevant to a particular ‘project’ instance, then ‘lesson’ should be of type ‘object’. If a ‘lesson’ is reusable across projects, then of type ‘document’.
Yes one lesson is only relevant to a project but when I change the type of lesson to 'object' I'm only able to search for lessons, not create new ones. Any idea why?
On the contrary, if I set the type to 'document' I can create the lessons outside and refer to them in this list. But since a lesson is unique to a project it's not good to have documents of random lessons together right?
exactly, it’s the referencing part that’s making a lookup. Try this:
export default {name: "projectTopic",
  type: "object",
  title: "Project Topic",
  fields: [
    {
      name: "heading",
      type: "string",
      title: "Heading",
      options: {
        list: [...topics],
      },
      readOnly: ({ parent, value }) => {
        return (
          topics.map((t) => t.value).includes(value) &&
          (parent?.text || parent?.subtopic)
        );
      },
    },
    {
      name: "text",
      type: "array",
      title: "Text Area",
      of: [
        {
          type: "block",
          title: "Text Area",
        },
      ],
      hidden: ({ parent }) => {
        return parent?.heading === "Process" || !parent?.heading;
      },
    },
exactly, it’s the reference part that’s creating the lookup. Try this:
export default {
  name: "lesson",
  type: "object",
  title: "Lesson",
  fields: [
    {
      name: "title",
      type: "string",
      title: "Title",
    },
    {
      name: "text",
      type: "array",
      title: "Text Area",
      of: [
        {
          type: "block",
          title: "Text Area",
        },
      ],
    },
Oh gotcha! Removed the reference and it works flawlessly. Thanks a bunch :)

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?