Skip to content

Instantly share code, notes, and snippets.

@jeromecovington
Last active January 27, 2025 17:07
Show Gist options
  • Save jeromecovington/42c7c2ebf52ddd6dae0d5097c89bb2d4 to your computer and use it in GitHub Desktop.
Save jeromecovington/42c7c2ebf52ddd6dae0d5097c89bb2d4 to your computer and use it in GitHub Desktop.
Sanity Schema Design and GROQ Query Examples

Sanity Schema Design and GROQ Query Examples

This document provides schema design examples and corresponding GROQ queries for various use cases. It’s a practical guide for developers to get familiar with GROQ querying and the basics of Sanity schema design.


Query to Get All Top-Level Types

Use the following query to retrieve a list of all unique top-level types in your dataset:

array::unique(*[_type != null]._type) | order(@ asc)

Explanation:

  • *[_type != null]: Selects all documents with a _type field.
  • ._type: Projects only the _type field for each document.
  • array::unique(): Filters the results to include only unique values.
  • | order(@ asc): Sorts the unique types alphabetically.

Example Output:

[
  "author",
  "blogPost",
  "category",
  "product",
  "tag"
]

Why Use This Query?

  1. Dynamic Schema Analysis:
    • Sanity datasets are schemaless at runtime, so the query analyzes the actual data to infer structure.
  2. Discover All Types:
    • The query identifies all unique document types in your dataset.

Schema Example (Nested Category, Tags, and Product)

Category

import { defineType, defineField } from 'sanity';

export default defineType({
  name: 'category',
  type: 'document',
  title: 'Category',
  fields: [
    defineField({
      name: 'name',
      type: 'string',
      title: 'Name',
    }),
  ],
});

Tag

import { defineType, defineField } from 'sanity';

export default defineType({
  name: 'tag',
  type: 'document',
  title: 'Tag',
  fields: [
    defineField({
      name: 'label',
      type: 'string',
      title: 'Label',
    }),
  ],
});

Product

import { defineType, defineField } from 'sanity';

export default defineType({
  name: 'product',
  type: 'document',
  title: 'Product',
  fields: [
    defineField({
      name: 'title',
      type: 'string',
      title: 'Title',
    }),
    defineField({
      name: 'price',
      type: 'number',
      title: 'Price',
    }),
    defineField({
      name: 'isAvailable',
      type: 'boolean',
      title: 'Available',
    }),
    defineField({
      name: 'features',
      type: 'array',
      title: 'Features',
      of: [{ type: 'string' }],
    }),
    defineField({
      name: 'category',
      type: 'reference',
      title: 'Category',
      to: [{ type: 'category' }],
    }),
    defineField({
      name: 'tags',
      type: 'array',
      title: 'Tags',
      of: [{ type: 'reference', to: [{ type: 'tag' }] }],
    }),
  ],
});

1. Filter, Function, Projection, Sorting

GROQ Query

*[_type == "product" && isAvailable == true] | order(price desc) {
  title,
  price
}

Explanation

  • Filter: [_type == "product" && isAvailable == true] retrieves available products.
  • Sorting: order(price desc) sorts results by price in descending order.
  • Projection: { title, price } specifies which fields to return.

2. References and Joins

GROQ Query

*[_type == "product"] {
  title,
  "categoryName": category->name
}

Explanation

  • Join: category->name retrieves the name field from the referenced category document.
  • Projection: Adds "categoryName" as a computed field in the output.

3. Expanding References

GROQ Query

*[_type == "product"] {
  title,
  category->
}

Explanation

  • Expands the entire category document, returning all its fields inline.

4. Expanding an Array of References

GROQ Query

*[_type == "product"] {
  title,
  "tags": tags[]->label
}

Explanation

  • tags[]->label expands the array of references, retrieving only the label field.

5. Filtering by References

GROQ Query

*[_type == "product" && "tag-123" in tags[]._ref] {
  title
}

Explanation

  • Filters products containing a reference to the tag with _id: "tag-123".

6. Naked Projections

GROQ Query

*[_type == "category"].name

Explanation

  • Returns only the name field for all categories, skipping the usual {} projection syntax.

7. Queries in Projections

GROQ Query

*[_type == "category"] {
  name,
  "products": *[_type == "product" && references(^._id)]{ title }
}

Explanation

  • Embedded query fetches all products associated with a category.

8. Examples of the Pipe Operator

GROQ Query

*[_type == "product"] | order(price desc)[0...5] {
  title,
  price
}

Explanation

  • Pipes data to order and [0...5] to retrieve the top 5 most expensive products.

9. Finer Points on Arrays and Projections

GROQ Query

*[_type == "product"] {
  title,
  features[0...2]
}

Explanation

  • Retrieves the first two elements of the features array.

10. The Ellipses Operator

GROQ Query

*[_type == "product"] {
  ...,
  "priceWithTax": price * 1.1
}

Explanation

  • ... includes all fields, while adding a computed field for price with tax.

11. Queries That Don’t Start with *

GROQ Query

count(*[_type == "product" && isAvailable == true])

Explanation

  • Returns the count of available products without returning their details.

Schema Example (Nested Author and Blog)

Author

import { defineType, defineField } from 'sanity';

export default defineType({
  name: 'author',
  type: 'document',
  title: 'Author',
  fields: [
    defineField({
      name: 'name',
      type: 'string',
      title: 'Name',
    }),
    defineField({
      name: 'bio',
      type: 'text',
      title: 'Bio',
    }),
  ],
});

Blog Post

import { defineType, defineField } from 'sanity';

export default defineType({
  name: 'blogPost',
  type: 'document',
  title: 'Blog Post',
  fields: [
    defineField({
      name: 'title',
      type: 'string',
      title: 'Title',
    }),
    defineField({
      name: 'author',
      type: 'reference',
      title: 'Author',
      to: [{ type: 'author' }],
    }),
  ],
});

12. Sub-Projections Inside the Outer Projection

GROQ Query

*[_type == "blogPost"] {
  title,
  author->{
    name,
    bio
  }
}

Explanation

  • Sub-projection { name, bio } within author fetches specific fields.

13. Joins

GROQ Query

*[_type == "blogPost"] {
  title,
  author->{
    name,
    "posts": *[_type == "blogPost" && author._ref == ^._id]{ title }
  }
}

Explanation

  • Fetches blog posts by the same author using a nested query.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment