How to add specific fields to each page in a website built with Sanity, using arrays of objects or reserved IDs, and handling validation with custom rules.

11 replies
Last updated: Jul 21, 2021
I need some advice I am working on a new website for the company which I’m working for. I have finished my studio with all schema’s, settings etc etc.
Now I have a schema “page” which obviously is for normal pages. But the problem here is that all the pages will look the same based on the predefined fieds.. take for instance pages like: bout, team, projects, contact etc etc.

All the fields are always the same. My question would be how can I add specific fields to each page instead of having all the same fields over and over again. Something like what you would have in WordPress with Advanced Custom Fields where you can add a field to a specific page, custom post type etc.

Does someone has a tip or example how to deal with this?
Jul 20, 2021, 11:03 PM
In our case, what we do is that each page is built using an array of objects. Basically we have one object for Team Section, Projects section, Hero Section, etc. This way, these special fields are more handled like “page sections”, with each their own settings.
Jul 21, 2021, 12:14 AM
That's where we're going too. Building more flexible components and using those to compose sections in a page builder seems challenging for multiple reasons.
Jul 21, 2021, 6:32 AM
I totally understand how to add like an array of objects to the “page.js” this way you can add sections to each page when needed. But the problem is all are the same from examples i’v seen.
Basically you can select all the same sections on each page. Take for instance a section of team members. I don’t want users to be able to pick that section from sanity on the homepage.

Is there a logic on how to deal with this?
user F
Jul 21, 2021, 7:53 AM
Hmmm I see, on our side we let the user add what they want in this array, no matter which page they are on.
One other way I used to handle such scenarios is to have pages with “reserved IDs”. Basically, I create those pages manually from the command line so they have a deterministic ID that I chose. e.g.
page_blog_posts
, and then I treat it as a Singleton on Sanity side. I also add a Singleton Settings document to configure for example all the blog functionality. It is not so great in terms of structure, but it works well for us.
Maybe you can push it even further and have your settings singleton handled as a separate tab next to the editor Form of your page…

One other option that I didn’t try is to use a plugin like this
https://github.com/hdoro/sanity-plugin-conditional-field
user B
Using this you could maybe use the same deterministic IDs and based on that ID, show or hide some fields.
Jul 21, 2021, 11:43 AM
user F
That’s right! Nice find would be great if you can hide specific array’s / types on specific pages. Letting clients pick and let them build a page is a little bit tricky most of the time in the past I had clients messing up the layout and asking you to fix it that’s actually a downside. Thanks for the plugin will check it!
Jul 21, 2021, 11:49 AM
user Q
although not optimal, I'd probably tackle this with a custom validation to warn editors when they insert a block that shouldn't be added to a specific page type. This way, they can still add, say, a team section to a blog post if they really need it, but the UI will let them know they shouldn't.
You
could do conditional
body/content
fields according to the page type (
teamBody
,
aboutBody
,
articleBody
, etc.), but I imagine your front-end would become more complex and it'd be impossible to switch from one page type to another without having to copy and paste the content. This would make it harder for editors to, for example, "graduate" a project page into an evergreen, top-level page that deserves more attention.
Does any of this make sense to you? Let me know if you need help with custom validation rules!
Jul 21, 2021, 2:50 PM
user B
Thanks for the good explanation. Actually I am facing more like a logical issue on a best way to setup multiple pages for our company website. I was thinking about manual pages inside my Next -> pages folder like about, team so this way I have different layouts and all generic pages create them with a page builder with section blocks which can be selected from an array. It’s not really a technical issue but more like a clean way on to setup and ordering website pages. For blogs, articles, etc etc it’s pretty easy all have the same detail page.
Sure thing, and yes I don’t want to overcomplicate. Do you have an example for instance if “editor” is on page “contact” I would like to show an custom validation that one specific section from the array should not be here. If that makes sense?

Thanks in advance Henrique
Jul 21, 2021, 5:32 PM
Hm... gotcha! Usually what I do is I have a single
[...slug].jsx
route in Next (that matches
/about
but also
/locations/seattle
,
/team/people/nino
, etc.) and a single
page
document type, which can have a
layout
property. The layout could be a string field with a
list
of values (see docs ) such as "contact", "regularPage", etc., which you'd use in the front-end to set the general layout of the page.
You can then use this layout value in the
body/content
validation. Here's some pseudo code:
const bodyField = {
  name: 'body',
  title: 'Content',
  type: 'array',
  of: [
    { type: 'blocks' },
    { type: 'contactForm' },
  ],
  validation: Rule => [
    Rule.required()
      .min(1)
      .error('Required field with at least 1 paragraph.'),
    Rule.unique(),
    Rule.custom((blocks, context) => {
      if (context.document.layout === "teamPage") {
        // Warn for contact forms in team pages
        if (!blocks.some(block => block.type === "contactForm")) {
          return "Contact forms don't go well in a team page. Are you sure you want to keep it?"
        }
      }

      return true
    }).warning(),
  ]
}

This tweet gets into why I've used
.warning()
above 😉
You could also test for the page's path instead of having a layout property: if `context.document.layout`
starts with
/team/
then run those checks.
Jul 21, 2021, 6:03 PM
Nice will try this. Thank you!
Jul 21, 2021, 6:08 PM
user B
Hey, that’s a nice one. so creating a [slug].js inside the root page folder like […slug].js will generate multiple pages & paths with one file? Instead of doing => pages/team, pages/about, pages/peope etc. I do use the schema “routes” where i specify the page routes and inside my [slug].js currently I am looping through all pages like so:

// Next
import { useRouter } from 'next/router'
import Img from 'next/image'

// Plugins
import { useNextSanityImage } from 'next-sanity-image'
import { NextSeo } from 'next-seo'

// Sanity
import { sanityClient, getClient } from '@lib/sanity.server'
import { usePreviewSubscription, PortableText, urlForImage } from '@lib/sanity'

// Queries
import { routesQuery, routeCurrentQuery } from '@lib/queries'

// Helpers
import { formatDate, formatDateTime } from '@lib/helpers'

const Page = ({ pageData, preview, slug }) => {
   const router = useRouter()

   const { data: { page = {} } = {} } = usePreviewSubscription(routesQuery, {
      params: { slug },
      initialData: pageData,
      enabled: preview || router.query.preview !== null
   })

   const {
      body,
      publishedAt,
      _updatedAt,
      title,
      featuredImage,
      openGraphImage,
      openGraphDescription,
      openGraphAlt,
      author
   } = page

   const SEO = {
      title: title,
      openGraph: {
         title: title,
         description: openGraphDescription,
         url: `<https://wwww/${slug}/>`,
         images: openGraphImage && [
            { url: urlForImage(openGraphImage).url(), alt: openGraphAlt }
         ]
      }
   }

   const imageAttributes = {
      alt: featuredImage?.alt ? featuredImage.alt : '',
      layout: 'responsive',
      sizes: '(max-width: 900px) 100vw, 900px'
   }

   const imageOptions = {
      blurUpImageWidth: 124,
      blurUpImageQuality: 40,
      blurUpAmount: 24
   }

   const imageProps = useNextSanityImage(sanityClient, featuredImage, {
      ...imageOptions
   })

   return (
      <>
         <NextSeo {...SEO} />
         {featuredImage && <Img {...imageProps} {...imageAttributes} />}
         {author && <p>Written by: {author.name}</p>}
         {_updatedAt && <p>Last updated: {formatDateTime(_updatedAt)}</p>}
         {publishedAt && <p>Publish date: {formatDate(publishedAt)}</p>}
         {title && <h1>{title}</h1>}
         {body && <PortableText blocks={body} />}
      </>
   )
}

export const getStaticProps = async ({ params = {}, preview = false }) => {
   const { slug } = params
   const { page: pageData } = await getClient(preview).fetch(routesQuery, {
      slug
   })

   return {
      props: { preview, pageData, slug }
   }
}

export const getStaticPaths = async () => {
   const routes = await getClient().fetch(routeCurrentQuery)

   return {
      paths: routes || null,
      fallback: false
   }
}

export default Page
Jul 21, 2021, 6:10 PM
That's the idea! Take a look at Next's catch all routes , this is the formal name of
[...slug]

From the code you've shown, I think the only thing that changes is that
slug
can be an array (
/about/team
would be
["about", "team"]
or similar. This means you'll need to normalize the parameter to always return the full slug 😉
Jul 21, 2021, 6:16 PM

Sanity– build remarkable experiences at scale

Sanity is a modern headless CMS that treats content as data to power your digital business. Free to get started, and pay-as-you-go on all plans.

Was this answer helpful?