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:
- Update the
Post
model to add a new author field that is a reference to aPerson
object - Update our
Post
query to include the new author data - Merge our
Post
andPerson
Zod objects to achieve type safety - Render our new author data on a post page
Let's get started.
Open the Post model and add a new author field.
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:
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:
- Import the
Person
Zod object - Get the person data as an author attribute from the
Post
- Update our "merged" Zod object to include a subset of Person attributes
- 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.
{
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 ✌️🧑🚀