Structuring a schema to reference content within another schema's nested array in Next.js

26 replies
Last updated: Sep 29, 2022
Hi all,
I have a bit of a convoluted question about structuring a schema to reference content within another schema's nested array. Bear with me please, I hope to make this as clear as possible!

At the highest level, within
_type == "residences"
, I have an array called
residencesData
which defines attributes associated to a given artist in residence ("person"), such as a title, slug, bio, etc. On
/residences
, I map all of the objects in this array (aka the list of artists).
Once a user navigates to a particular artist's page,
/residences/[artistName]
, I query for this given artist's info on that page using the query below:
export const residenciesPageQuery = groq`
  *[$slug in residencesData[].slug.current][0] {
    _id,
    "person": residencesData[slug.current == $slug][0] {
      _key,
      title, 
      type,
      bio,
      photoCredit,
      contributions[]->{
        "imageUrl": mainImage.asset->url,
        "lqip": mainImage.asset->metadata.lqip,
        slug,
      },
      portfolio, 
      instagram,
      "imageUrl": mainImage.asset->url,
      "lqip": mainImage.asset->metadata.lqip,
      "slug": slug.current,
    }
  }
`;
Crucially, this person (the artist in residence) also contributes to a presentation text,
texteDePresentation
, which I'll need to place on
/residences/[artistName]/[texteDePresentation]


My question is as follows: How can I create a reference to a given
person
within my
texteDePresentation
schema?

    {
      name: "associatedArtist",
      title: "Artiste en résidence associé",
      description:
        "Ceci est nécessaire pour le bouton 'retour' trouvé à la fin du texte",
      type: "array",
      of: [{ type: "reference", to: { type: " ????? " } }],
      validation: (Rule) => Rule.required(),
    },
AI Update

I understand your challenge! You have artists stored as objects within a residencesData array on a residences document, and you want to reference those nested array items from your texteDePresentation schema. Unfortunately, Sanity references can only point to top-level documents, not to objects nested within arrays.

Here are your best options:

The most Sanity-idiomatic approach is to restructure your schema so each artist is its own document:

// schemas/artist.js
{
  name: 'artist',
  type: 'document',
  fields: [
    { name: 'title', type: 'string' },
    { name: 'slug', type: 'slug' },
    { name: 'bio', type: 'text' },
    { name: 'photoCredit', type: 'string' },
    { name: 'portfolio', type: 'url' },
    { name: 'instagram', type: 'url' },
    { name: 'mainImage', type: 'image' },
    // ... other fields
  ]
}

// schemas/residences.js
{
  name: 'residences',
  type: 'document',
  fields: [
    {
      name: 'residencesData',
      type: 'array',
      of: [{ type: 'reference', to: [{ type: 'artist' }] }]
    }
  ]
}

// schemas/texteDePresentation.js
{
  name: 'texteDePresentation',
  type: 'document',
  fields: [
    {
      name: 'associatedArtist',
      title: 'Artiste en résidence associé',
      type: 'reference', // single reference, not array
      to: [{ type: 'artist' }],
      validation: (Rule) => Rule.required(),
    }
  ]
}

This gives you proper references and makes your queries cleaner.

Option 2: Store the _key Value

If restructuring isn't feasible, you can store the _key of the array item and manually join the data in your queries:

// In texteDePresentation schema
{
  name: 'associatedArtistKey',
  title: 'Artiste en résidence associé',
  type: 'string', // Store the _key as a string
  validation: (Rule) => Rule.required(),
}

Then in your GROQ query:

*[_type == "texteDePresentation" && slug.current == $slug][0] {
  ...,
  "associatedArtist": *[_type == "residences"][0]
    .residencesData[_key == ^.associatedArtistKey][0]
}

This is more fragile and harder to maintain, but it works if you can't restructure.

Option 3: Duplicate the Reference

Store a reference to the parent residences document plus the artist's slug:

{
  name: 'associatedArtist',
  type: 'object',
  fields: [
    {
      name: 'residencesDoc',
      type: 'reference',
      to: [{ type: 'residences' }]
    },
    {
      name: 'artistSlug',
      type: 'string'
    }
  ]
}

The first option is really the way to go if possible. Sanity's reference system is designed for document-to-document relationships, and fighting against that pattern usually creates more complexity down the road. Making artists top-level documents also gives you better content management, easier querying, and the ability to reference artists from anywhere in your content.

Do artists only ever belong under a single
residences
or can they be in multiple with different details (title, bio, etc.)? (I ask because I think your life might be easier if you had a
person
like document to handle this)
This is the structure. At the highest level, you define a collection (ie the year and the artists associated with that year)
then within that array you have everything you need to generate pages for that given artist
the artists change every year, if that answers your question - you won't see an artist listed twice
and they need to be organized under a year because there's also an archive section for "non-active" residencies which is organized by year in a drop-down style
I've placed the artists in the same document as the year because I already have 13 schema types, so adding any more would make the dashboard quite busy (especially if i have to create another document just for the presentation texts)
I don’t think what you’re trying to do is possible without some sort of custom component, and even then I’m not sure how well it would work.
Also you can change the Studio structure to clean things up. I wouldn’t let that effect how you’re modelling data.

This is changing your schema, however if it was me: I’d create an
artist
document that takes care of the title, slug, bio, etc. for the person.
Then
residences
has a field with an array of artists, ie.
residencesData
. Anywhere else you need to know about an artist, eg
associatedArtist
you can reference the
artist
.

and they need to be organized under a year because there’s also an archive section for “non-active” residencies which is organized by year in a drop-down style
You can solve this with a groq query to include whether the artists is under an active year or not
fair, that works, but then I guess where I'm stuck here would be how to have the pages follow this structure:
/residences/[artistName]/[texteDePresentation]
let me give this a shot -- I'll let you know if I run into any roadblocks 🙂
Hey so, it's going mostly smoothly but I got stuck with an error on the artist's page that I'm not quite sure how to solve. Any advice?
(commented out is the old query for when I created the artists as an array within residenceData)
this is my
getStaticPaths
btw, is there a way to hide a collection from the dashboard? Considering how you can create an artist entry from within the
residencesData
(year collection group) reference (using the "create new" button), I don't see a reason to also have this displayed on the far left hand side
All of the Sanity side logic should be set up correctly, I'm just struggling now to create a page at the ``/residences/[artistName]/[texteDePresentation]`` route that doesn't 404.
How should I configure my
getStaticPaths
in this case, particularily on line 31 in the screenshot below?
Here is my query:


export const residenciesPageQuery = groq`
  *[_type == "artistesEnResidence" && slug.current == $slug][0] {
    _id,
    title, 
    bio,
    texteDePresentationData[0]->{
      _id,
      title,
      author,
      body,
      "slug": slug.current,
    },
    "imageUrl": mainImage.asset->url,
    "slug": slug.current,
  }`
This is the shape of the output, and since it's an object rather than an array i'm sure I'd have to change the
paths: data.map((slug) => ({ params: { slug } })),
line as well, but I haven't been able to figure it out
I think you need to provide the
[artistName]
and the
[texteDePresentation]
for this to work. There's a good explanation here .
I'm not sure I follow - why would I need both when my query typed above contains a reference to the
texteDePresentation
?
The only thing I need to render on this page is the contents of
texteDePresentation
That's how dynamic routing works in Next. You're providing the path to that site page, which includes
[artistName]
.
Right, but I'm accessing /texteDePresentation from the [artistName] route, does that change things?
I'm able to navigate to the route properly using the anchor tag seen below, but once I get there it 404's.
// [artistName]

<Link
  scroll={false}
  href={`/residences/${data.slug}/${data.texteDePresentationData.slug}`}
>
  <small>Texte de présentation</small>
</Link>
Are you suggesting that I need to provide both to
getStaticPaths
at the
residences/[artistName]/[texteDePresentation]
route ?
That is precisely what I'm saying. There's a pretty good explanation in the link I shared.
Hey RD! Thanks so much again for the link you sent yesterday - It really helped set me on the right path.
I looked at it again this morning and got it to work by hard-coding the array data! However with my provided query (that should in theory follow the correct structure) it still fails.

I'm attaching a screenshot of my query output. Based on the link you sent, it seems like
getStaticPaths
is complaining because
texteDePresentation
is null in some cases (
TypeError: Cannot read property 'slug' of null
)
Not every
artisteEnResidence
will have a
texteDePresentation
, so is this error something that I can fix with a groq query?

export async function getStaticPaths() {
  const data = await client.fetch(
    groq`*[_type == "artistesEnResidence"] {
      "slug": slug.current,
      texteDePresentationData[0]->{
        "slug": slug.current,
      },
    }`
  );

  // const data = [
  //   {
  //     slug: "manuel-mathieu",
  //     texte: "le-gout-des-contradictions",
  //   },
  // ];

  return {
    paths: data.map((item) => {
      return {
        params: {
          slug: item.slug,
          texte: item.texteDePresentationData.slug,
          // slug: item.slug,
          // texte: item.texte,
        },
      };
    }),
    fallback: true,
  };
}
at the moment the only solution I managed to find was this, using a ternary operation (which doesn't feel ideal):

export async function getStaticPaths() {
  const data = await client.fetch(
    groq`*[_type == "artistesEnResidence"] {
      "slug": slug.current,
      texteDePresentationData[0]->{
        "slug": slug.current,
      },
    }`
  );  

 return {
    paths: data.map((item) => {
      return {
        params: {
          slug: item.slug,
          texte: item.texteDePresentationData?.slug
            ? item.texteDePresentationData.slug
            : "invalid-url",
        },
      };
    }),
    fallback: true,
  };
}
You could also filter it in js when constructing the paths, something like this:
return {
  paths: data
    .filter(item => item.texteDePresentationData)
    .map(item => {
      return {
        params: {
          slug: item.slug,
          texte: item.texteDePresentationData.slug,
        },
      };
    }),
  fallback: true,
};

Nice! Everything works as intended now. Your help is invaluable:)
Awesome! Glad we got it sorted out together! 🤜 🤛

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?