Combining Content Models

In this guide, we'll work through an example of how to tie together two data models. This guide builds from the data model you created in the guide.

We'll work through these steps:

  1. Update the Post model to add a new author field that is a reference to a Person object
  2. Update our Post query to include the new author data
  3. Merge our Post and Person Zod objects to achieve type safety
  4. Render our new author data on a post page

Let's get started.

Open the Post model and add a new author field.

content-models/src/post.ts
import { defineField, defineType } from "sanity";
import { z } from "zod";
import * as S from "sanity-zod-types";
import { Tag } from "./tag";
import { Person } from "./person";

export const postSanityDefinition = defineType({
  name: "post",
  title: "Post",
  type: "document",
  fields: [
    defineField({
      name: "title",
      title: "Title",
      type: "string",
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: "slug",
      title: "Slug",
      type: "slug",
      options: {
        source: "title",
        maxLength: 96,
      },
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: "description",
      title: "Description",
      type: "text",
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: "mainImage",
      title: "Main image",
      type: "image",
      options: {
        hotspot: true,
      },
    }),
    defineField({
      name: "body",
      title: "Body",
      type: "blockContent",
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: "importance",
      title: "Importance Score",
      type: "number",
      description: "Used to order search results. Higher is more important",
      initialValue: 0,
      validation: (Rule) => Rule.required().min(0).max(100).precision(1),
    }),
    defineField({
      name: "isVisible",
      title: "Is Visible",
      description:
        "Hidden posts will not show on the site unless explicitly queried",
      type: "boolean",
      initialValue: true,
    }),
    defineField({
      name: "tags",
      title: "Tags",
      description: "Tags help make resources easier to find by search",
      type: "array",
      of: [{ type: "reference", to: { type: "tag" } }],
    }),
    defineField({
      name: "language",
      type: "string",
      readOnly: true,
      hidden: true,
    }),
    defineField({
      name: "author",
      title: "Author",
      type: "reference",
      to: { type: "person" },
    }),
  ],
});

export const Post = S.Document.extend({
  title: S.String,
  slug: S.Slug,
  description: S.String,
  mainImage: S.Image.nullable(),
  body: z.any().nullable(), // Zod will not validate Portable Text
  importance: S.Number.min(0).max(100),
  isVisible: S.Boolean,
  tags: z.array(Tag).nullable(),
  language: S.String,
  author: Person.nullable(),
});

export type Post = z.infer<typeof Post>;

We define a new author attribute as a reference to a "person", then we update the Zod object to include our new author field. Zod objects can be composed of other Zod objects, so this part is easy peasy.

Over in our post query, let's grab our new author data. will not give us the person data by default. Our data connection is by "reference", but we can "dereference" in GROQ directly by using a -> operator.

Check it out here:

site-astro/src/queries/post.ts
import { groq, useSanityClient } from "astro-sanity";
import { Post } from "content-models";
import { z } from "zod";
import { blockContentQuery } from "./partials/blockContent";
import { tagsQuery, TagsResult } from "./partials/tag";
import { backlinksQuery, BacklinkResult } from "./partials/backlink";
import { Person } from "content-models";

// Posts are sorted by creation date by default
// To use importance as the sorter:
// swap out `order(_createdAt desc)` with `order(importance desc)`
export async function getAllPostsList() {
  const query = groq`*[_type == "post" && isVisible == true && language == $lang] | order(_createdAt desc) {
    title,
    slug,
    description,
    ${tagsQuery},
    "author": author -> {
      name
    } 
  }`;

  const MergedPost = Post.extend({
    tags: TagsResult,
    author: Person.pick({
      name: true,
    }).nullable(),
  });

  const PostsResult = z.array(
    MergedPost.pick({
      title: true,
      slug: true,
      description: true,
      tags: true,
      author: true,
    })
  );

  const data = await useSanityClient().fetch(query, {
    lang: "en",
  });

  try {
    return PostsResult.parse(data);
  } catch (error: any) {
    throw new Error(`Error parsing getAllPostsList, \n${error.message}`);
  }
}

What we changed:

  1. Import the Person Zod object
  2. Get the person data as an author attribute from the Post
  3. Update our "merged" Zod object to include a subset of Person attributes
  4. Update our "result" Zod object to include author

Our data is merged and we're ready to show it on our blog page.

Type hinting should work for our author as a nested, optional attribute of a post.

site-astro/src/pages/blog/index.astro
{
	data &&
		data.map((post) => {
			const lines = splitIntoLines(post.description);
			return (
				<a
					href={getUrlForSanityType("post", post.slug.current)}
					class="group"
				>
					<h2 class="h3 group-hover:text-sky-500">{post.title}</h2>
					{lines.map((line, i) => (
						<p class="p text-md" set:html={line} />
					))}
					{post.author && <p>{post.author.name}</p>}
				</a>
			);
		})
}

Wrapping Up

Data model merging is a complex, but important piece to the Space Madness stack. Now that you can create, query, and combine data models you can build almost anything.

It might help to read through some of the code examples that come with the project. Most use cases are covered. Feel free to open an issue if you need a bit of guidance.

Stay sane out there ✌️🧑‍🚀

Backlinks