@arcteryx/sanity-content
This package exports methods to fetch content from our Sanity CMS. It is heavily influenced by this article recommended to us from Sanity tech contact. Type-safe GROQ Queries for Sanity Data with Zod.
Usage
import { fetchPage } from @arcteryx/sanity-content
import { createClient } from "@sanity/client";
const sanityClient = createClient({
projectId: "<project id>",
dataset: "development",
useCdn: false, // set to `false` to bypass the edge cache
apiVersion: '2023-05-03',
})
// The `page` object is type-safe. You can expect page to have the typed attributes defined in `pageSchema` of `fragments/page.ts`.
// For example: `page.region`, `page.breadcrumbs.items`, etc.
const page = await fetchPage(sanityClient, {
market: "outdoor", // "sale" for outlet
country: "ca",
language: "en",
slug: "help/womens/sizing",
});
`
// If you need to define an `interface` for the type of the `page` object, you can do something like this:
interface Props {
page: Awaited<ReturnType<typeof fetchPage>>,
}
const MyApp = ({ page }: Props) => {
const breadcrumbs = page.breadcrumbs?.items; // type-safe
}
Folder Structure
This repo is organized into 2 folders, documents
and fragments
.
documents
Files in this folder will export "fetch" functions that use a SanityClient
object to retrieve documents from the content lake. Their function signature should generally be as follows:
// DocumentTypeParams would be the interface to describe the expected params that should be passed to the client to satisfy the groq query.
function fetchPage (client: SanityClient, params: DocumentTypeParams)
fragments
Files in this folder should be small and concise. They're the building blocks used to compose an entire groq query. In general they correspond to the schema definitions created in the sanity-cms
repo, but they don't need to be. For example, a schema could have the firstName
and lastName
attributes, but our groq fragment could choose to query fullName
by having groq concatenate both attributes.
In these files we utilize zod to do two things: Define the output schema for our query, and make that schema type-safe.
These files must export 2 objects:
- The
schema
in the format of <fragmentName>Schema
. For example: breadcrumbSchema
. This object is the zod
definition. And it in itself can be composed of other schema
fragments. - The
query
in the format of <fragmentName>Query
. For example: breadcrumbsQuery
. This is the partial groq query. And it in itself can be composed of other query
fragments.
See breadcrumbs.ts for an example of composition.
Document Fragments
These fragments are used in the fetch functions defined within the documents
folder. So they should also export 3 more items:
- The
filter
in the format of <fragement name>Filter
. For example pageFilter
. This is used in the groq query to filter down or search the specific document. - The
params
in the format of <FragmentName>Params
. For example PageParams
. (Note: capital case). This is a type-safe interface
that specifies the required params that will be passed to SanityClient
to satisfy the groq query. - The
queryParams
validator function. This is to help ensure the params used in sanityClient.fetch
match the schema defined by the interface
above. This function should usually always look like this: export const queryParams = (params: <FragmentName>Params) => params;
. Then the "fetch" function defined in the documents
folder should validate the params like so: sanityClient.fetch(query, queryParams(params))
.
See fragments/page.ts and documents/page.ts for an example.
sectionSchema
To accept new types for sections field in sectionSchema
, add the new type in baseSectionListSchema
and not in the sections field itself. This is so that schemas using baseSectionListSchema
like orTab
will be able to accept this new section type.
GROQ Methods
Referencing Fields
When constructing a query, you are able to access it directly using the name of the value in a shorthand notation.
export const query = groq`{
name,
content
}`;
If you need to conduct a more indepth query or rename the key, you need to wrap in double quotes.
export const query = groq`{
name,
"contentArray":content
}`;
Coalesce
There are times when your query may not have any data. This will typically return null for your value. To ensure
that you return a value, you can use the coalesce function. This is valuable when creating a schema using an array; if there are no items in the array, return an empty array rather than null.
export const query = groq`{
name,
"content": coalesce(content, [])
}`;
Optional Fields
There are times when you want the output of your data from Sanity to include optional fields. An example would be the inclusion of a component in certain scenarios. If the component is to be included in the view, you would add it in sanity and expect that key to be present in the response. If the component is not to be included, than that key will be omitted from the response.
Example:
export const query = groq`{
name,
content != null => {
content
}
}`;
In this case, if content exists in the query than it will be retreived and returned in the response.
const response = {name:'', content:{...}}
If there is no content in the query, then it will be omitted from the response
const response = {name:'',}
Another option would be to use the coalesce function and return a default value
export const query = groq`{
name,
"content" : coalesce(content, {})
}`;
If there is no content in the query, then the fallback will be used. This method will also require a more complicated Zod schema since
you will need to account for the nested undefined values.
const response = {name:'', content:{}}
However, this approach will produce a type signature that will contain multiple undefined fields. This may require
adapting in the front end.
Zod Methods
Strictly Defined a String
To ensure that a value coming from the Content Lake is strictly equal, you can use the z.literal method.
export const schema = z.object({
type: z.literal("button"),
});
If this schema is called and the type does not equal "button", the validation will fail.
Type Clarity
When using Zod data types, it helps to distinguish Zod types against Javascript data types when it's used like this:
z.object()
z.array()
z.string()
But not this:
const { object, array, string } = z;
object()
array()
string()
Releases
This repo will automatically publish releases to @arcteryx/sanity-content on npm when there is a merge to main
. We utilize the semantic-release package to handle this for us. It also depends on our commit messages following conventional commits.