Add Page Type with Pagebuilder

/add-page-type-with-pagebuilderCreated: 15 Sept 2025, 12:11Updated: 16 Sept 2025, 12:13
Variables
2
Goals
15
Path Groups
2
Nodes
22
Compact File Tree
Quick overview of planned files
Base Path
frontend
β”œβ”€ πŸ“ app
β”‚  β”œβ”€ πŸ“ {{.KebabCasePageTypePlural}}
β”‚  β”‚  β”œβ”€ πŸ“ (index)
β”‚  β”‚  β”‚  └─ πŸ“„ page.tsx
β”‚  β”‚  └─ πŸ“ [slug]
β”‚  β”‚     └─ πŸ“„ page.tsx
β”‚  └─ πŸ“ components
β”‚     β”œβ”€ πŸ“„ {{.PascalCasePageTypePlural}}.tsx
β”‚     └─ πŸ“„ Header.tsx
└─ πŸ“ sanity
   └─ πŸ“ lib
      β”œβ”€ πŸ“ pagetype-queries
      β”‚  └─ πŸ“„ {{.KebabCasePageTypeSingular}}.queries.ts
      β”œβ”€ πŸ“„ queries.ts
      └─ πŸ“„ utils.ts
Base Path
studio/src/schemaTypes
β”œβ”€ πŸ“ documents
β”‚  └─ πŸ“„ {{.KebabCasePageTypeSingular}}.ts
β”œβ”€ πŸ“ objects
β”‚  β”œβ”€ πŸ“„ blockContent.tsx
β”‚  β”œβ”€ πŸ“„ link.ts
β”‚  └─ πŸ“„ pageBuilder.tsx
└─ πŸ“„ index.ts
Variables
Argument-driven inputs used by your generator
  • PageTypeSingular
    Name your page type
    P1

    This is the name of the "_type" we use in Sanity. This dictates a lot of the naming conventions elsewhere.

    authoreventproductservice
  • PageTypePlural
    Pluralize your page type
    P2

    This helps cases where we need to pluralize. No worries if it's the same as the singular.

    authorseventsproductsservices
Ignored Patterns
Globs/paths skipped by the executor
No ignored patterns.
Goals
What this command is trying to accomplish
  1. Create Files and Folders for the Page Type routing

    Creates folder for the route, with dedicated page.tsx for both (index) and [slug].

  2. Create List Component which output posts from new PageType
  3. Create dedicated query file for the new PageType
  4. Register PageBuilder object in Studio schema index
    studio/src/schemaTypes/index.ts

    Makes the Page Builder UI available in Studio so the new PageType can actually use its blocks.

    How-To Tips
    • Add the import near other object imports (use an addMarkerAboveTarget-style insertion; guard with requireAbsent).
    • Include 'pageBuilder' in the schemaTypes array close to other objects (addMarkerAboveTarget; guard with requireAbsent).
  5. Register PageType in Studio schema index
    studio/src/schemaTypes/index.ts

    Allows Studio to recognize and edit the new document type, which is foundational for all other steps.

    How-To Tips
    • Place the import alongside other document imports (addMarkerBelowTarget fits well).
    • Append the type in the documents section of schemaTypes (addMarkerAboveTarget).
  6. Export shared GROQ fragments (postFields, linkFields, linkReference)
    frontend/sanity/lib/queries.ts

    Turns common fragments into exports so queries across files reuse them cleanly without re-definitions.

    How-To Tips
    • Find the primary 'const' definitions and convert to 'export const' (replaceIfMissing).
    • Change only the first canonical definition and guard with requireAbsent to avoid double-exports.
  7. Export pageBuilderFields and reuse in getPageQuery
    frontend/sanity/lib/queries.ts

    Centralizes Page Builder selections into a shared fragment and references it in queries to prevent drift.

    How-To Tips
    • Introduce an exported fragment near other exports and rebind the query to use it (replaceBetween).
    • Guard with requireAbsent so you don’t redefine an existing export.
  8. Add PageType to linkReference mapping (rich-text links)
    frontend/sanity/lib/queries.ts

    Lets rich-text links resolve this new type by exposing its slug in the mapping used across queries.

    How-To Tips
    • Locate the object that maps linkable types to slugs.
    • Insert a new key for the PageType using an addMarkerBelowTarget-style placement near the start of the mapping.
  9. Add PageType route case to linkResolver
    frontend/sanity/lib/utils.ts

    Teaches the client resolver how to build canonical URLs for the new type so internal links work end-to-end.

    How-To Tips
    • Find the switch/branch that handles link types.
    • Add a case for the PageType before the default (addMarkerAboveTarget).
  10. Include PageType in sitemapData query
    frontend/sanity/lib/queries.ts

    Ensures the new type is included in the sitemap so search engines can discover it.

    How-To Tips
    • Identify the allowed _type list in the sitemap filter.
    • Insert the new _type right before the slug guard (insertBeforeInline; fallbackOnly to keep it idempotent).
  11. Add archive link to Header nav (idempotent)
    frontend/app/components/Header.tsx

    Surfaces the new type in the main navigation so users can find its listing page.

    How-To Tips
    • Locate the first nav list item and place the archive link alongside similar internal links.
    • Use addMarkerBelowTarget anchored to the earliest stable <li>.
    • Keep it idempotent (occurrence 'first', fallbackOnly) and skip if an equivalent link exists.
  12. Expose PageType as a link type in blockContent
    studio/src/schemaTypes/objects/blockContent.tsx

    Lets editors choose the new type directly in rich-text link options for portable text blocks.

    How-To Tips
    • Find the editor-facing list of link type options.
    • Add an option entry using addMarkerAboveTarget near the end of the list.
  13. Add conditional PageType reference field in blockContent
    studio/src/schemaTypes/objects/blockContent.tsx

    Shows a reference picker only when the new link type is selected, keeping forms tidy and valid.

    How-To Tips
    • Locate the group of link-related reference fields.
    • Insert a conditional reference field using addMarkerAboveTarget at the final field cluster.
  14. Expose PageType as a link type in link object
    studio/src/schemaTypes/objects/link.ts

    Adds the new type to the generic link object so any schema reusing it can target this type as well.

    How-To Tips
    • Find the radio/option list for link types.
    • Add a new entry with addMarkerAboveTarget following the existing structure and ordering.
  15. Add conditional PageType reference field in link object
    studio/src/schemaTypes/objects/link.ts

    Adds a reference field tied to the new option and required only when that option is chosen.

    How-To Tips
    • Identify where other type-specific reference fields are declared.
    • Place the new conditional field alongside them (addMarkerAboveTarget).
File Tree
Detailed view with actions
Base Path
frontend
Detailed view
  • app
    Folder
    • {{.KebabCasePageTypePlural}}
      Folder
      • (index)
        Folder
        • page.tsx
          File
          View Code
          import Link from "next/link";
          import type { Metadata } from "next";
          import { client } from "@/sanity/lib/client";
          import { all{{.PascalCasePageTypePlural}}Query } from "@/sanity/lib/pagetype-queries/{{.KebabCasePageTypeSingular}}.queries";
          import { All{{.PascalCasePageTypePlural}} } from "@/app/components/{{.PascalCasePageTypePlural}}";
          
          export const metadata: Metadata = {
            title: "{{.PascalCasePageTypePlural}}",
            description: "All {{.LowerCasePageTypePlural}}"
          };
          
          export default async function {{.PascalCasePageTypeSingular}}IndexPage() {
            const items = await client.fetch(all{{.PascalCasePageTypePlural}}Query);
          
            if (!items?.length) {
              return (
                <main className="container mx-auto p-6">
                  <h1 className="text-2xl font-semibold">{{.PascalCasePageTypePlural}}</h1>
                  <p className="opacity-70 mt-2">No {{.LowerCasePageTypePlural}} yet.</p>
                </main>
              );
            }
          
            return (
              <main className="container mx-auto p-6">
              <All{{.PascalCasePageTypePlural}} />
              </main>
            );
          }
          
      • [slug]
        Folder
        • page.tsx
          File
          View Code
          import type { Metadata, ResolvingMetadata } from 'next'
          import { sanityFetch } from '@/sanity/lib/live'
          import { {{.LowerCasePageTypeSingular}}Slugs, {{.LowerCasePageTypeSingular}}BySlugQuery } from '@/sanity/lib/pagetype-queries/{{.KebabCasePageTypeSingular}}.queries'
          import PageBuilderPage from '@/app/components/PageBuilder'
          import { PageOnboarding } from '@/app/components/Onboarding'
          
          type Props = {
            params: Promise<{ slug: string }>
          }
          
          /**
           * Generate the static params for {{.LowerCasePageTypeSingular}}.
           */
          export async function generateStaticParams() {
            const { data } = await sanityFetch({
              query: {{.LowerCasePageTypeSingular}}Slugs,
              perspective: 'published',
              stega: false,
            })
            return data
          }
          
          /**
           * Generate metadata for the {{.LowerCasePageTypeSingular}} page.
           */
          export async function generateMetadata(props: Props, _parent: ResolvingMetadata): Promise<Metadata> {
            const params = await props.params
            const { data: doc } = await sanityFetch({
              query: {{.LowerCasePageTypeSingular}}BySlugQuery,
              params,
              stega: false,
            })
          
            // Title/description fallbacks so this template works across different field sets
            const title = (doc?.name ?? doc?.title ?? '{{.PascalCasePageTypeSingular}}') as string | undefined
            const description = (doc?.heading ?? doc?.subheading ?? undefined) as string | undefined
          
            return {
              title,
              description,
            } satisfies Metadata
          }
          
          export default async function {{.PascalCasePageTypeSingular}}Page(props: Props) {
            const params = await props.params
          
            const [{ data: doc }] = await Promise.all([
              sanityFetch({ query: {{.LowerCasePageTypeSingular}}BySlugQuery, params }),
            ])
          
            if (!doc?._id) {
              return (
                <div className="py-40">
                  <PageOnboarding />
                </div>
              )
            }
          
            return (
              <div className="my-12 lg:my-24">
                <div className="">
                  <div className="container">
                    <div className="pb-6 border-b border-gray-100">
                      <div className="max-w-3xl">
                        <h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl lg:text-7xl">
                          {doc?.heading ?? doc?.name ?? doc?.title}
                        </h2>
                        {(doc?.subheading ?? doc?.excerpt) && (
                          <p className="mt-4 text-base lg:text-lg leading-relaxed text-gray-600 uppercase font-light">
                            {doc?.subheading ?? doc?.excerpt}
                          </p>
                        )}
                      </div>
                    </div>
                  </div>
                </div>
          
                {/* Keep your existing renderer */}
                <PageBuilderPage page={doc as any} />
              </div>
            )
          }
          
    • components
      Folder
      • {{.PascalCasePageTypePlural}}.tsx
        File
        View Code
        import Link from 'next/link'
        
        import { sanityFetch } from '@/sanity/lib/live'
        import { all{{.PascalCasePageTypePlural}}Query } from '@/sanity/lib/pagetype-queries/{{.KebabCasePageTypeSingular}}.queries'
        import DateComponent from '@/app/components/Date'
        import OnBoarding from '@/app/components/Onboarding'
        import Avatar from '@/app/components/Avatar'
        import { createDataAttribute } from 'next-sanity'
        
        type {{.PascalCasePageTypeSingular}}ListItem = {
          _id: string
          title?: string
          name?: string
          slug: string
          excerpt?: string | null
          subheading?: string | null
          coverImage?: unknown
          date?: string
          author?:
            | {
                firstName?: string
                lastName?: string
                picture?: unknown
              }
            | null
        }
        
        const {{.PascalCasePageTypeSingular}}Card = ({ item }: { item: {{.PascalCasePageTypeSingular}}ListItem }) => {
          const { _id, slug, date, author } = item
          const title = item.title ?? item.name ?? 'Untitled'
          const excerpt = (item.excerpt ?? item.subheading) ?? null
        
          const attr = createDataAttribute({
            id: _id,
            type: '{{.LowerCasePageTypeSingular}}',
            path: (item.title ? 'title' : 'name') as 'title' | 'name',
          })
        
          return (
            <article
              data-sanity={attr()}
              key={_id}
              className="border border-gray-200 rounded-sm p-6 bg-gray-50 flex flex-col justify-between transition-colors hover:bg-white relative"
            >
              <Link className="hover:text-brand underline transition-colors" href={`/{{.LowerCasePageTypePlural}}/${slug}`}>
                <span className="absolute inset-0 z-10" />
              </Link>
        
              <div>
                <h3 className="text-2xl font-bold mb-4 leading-tight">{title}</h3>
        
                {excerpt && (
                  <p className="line-clamp-3 text-sm leading-6 text-gray-600 max-w-[70ch]">{excerpt}</p>
                )}
              </div>
        
              <div className="flex items-center justify-between mt-6 pt-4 border-t border-gray-100">
                {author?.firstName && author?.lastName && (
                  <div className="flex items-center">
                    <Avatar person={author as any} small={true} />
                  </div>
                )}
                {date && (
                  <time className="text-gray-500 text-xs font-mono" dateTime={date}>
                    <DateComponent dateString={date} />
                  </time>
                )}
              </div>
            </article>
          )
        }
        
        const {{.PascalCasePageTypePlural}} = ({
          children,
          heading,
          subHeading,
        }: {
          children: React.ReactNode
          heading?: string
          subHeading?: string
        }) => (
          <div>
            {heading && (
              <h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl lg:text-5xl">
                {heading}
              </h2>
            )}
            {subHeading && <p className="mt-2 text-lg leading-8 text-gray-600">{subHeading}</p>}
        
            <div className="pt-6 space-y-6">{children}</div>
          </div>
        )
        
        export const All{{.PascalCasePageTypePlural}} = async () => {
          const { data } = await sanityFetch({ query: all{{.PascalCasePageTypePlural}}Query })
        
          if (!data || data.length === 0) {
            return <OnBoarding />
          }
        
          const list = data as unknown as {{.PascalCasePageTypeSingular}}ListItem[]
        
          return (
            <{{.PascalCasePageTypePlural}}
              heading="{{.PascalCasePageTypePlural}}"
              subHeading="{{.PascalCasePageTypePlural}} populated from your Sanity Studio."
            >
              {list.map((item) => (
                <{{.PascalCasePageTypeSingular}}Card key={item._id} item={item} />
              ))}
            </{{.PascalCasePageTypePlural}}>
          )
        }
        
      • Header.tsx
        File β€’ Action File
        Actions
        1. PAGETYPE ARCHIVE LINK
          Behaviour: addMarkerBelowTarget
          Occurrence: first
          Target: <li>
          Content
          <Link href="/{{.LowerCasePageTypePlural}}" className="mr-6 hover:underline">{{.PascalCasePageTypePlural}}</Link>
        View Code
        import Link from 'next/link'
        import {settingsQuery} from '@/sanity/lib/queries'
        import {sanityFetch} from '@/sanity/lib/live'
        
        export default async function Header() {
          const {data: settings} = await sanityFetch({
            query: settingsQuery,
          })
        
          return (
            <header className="fixed z-50 h-24 inset-0 bg-white/80 flex items-center backdrop-blur-lg">
              <div className="container py-6 px-2 sm:px-6">
                <div className="flex items-center justify-between gap-5">
                  <Link className="flex items-center gap-2" href="/">
                    <span className="text-lg sm:text-2xl pl-2 font-semibold">
                      {settings?.title || 'Sanity + Next.js'}
                    </span>
                  </Link>
        
                  <nav>
                    <ul
                      role="list"
                      className="flex items-center gap-4 md:gap-6 leading-5 text-xs sm:text-base tracking-tight font-mono"
                    >
                      <li>
                        <Link href="/about" className="hover:underline">
                          About
                        </Link>
                      </li>
        
                      <li className="sm:before:w-[1px] sm:before:bg-gray-200 before:block flex sm:gap-4 md:gap-6">
                        <Link
                          className="rounded-full flex gap-4 items-center bg-black hover:bg-blue focus:bg-blue py-2 px-4 justify-center sm:py-3 sm:px-6 text-white transition-colors duration-200"
                          href="https://github.com/sanity-io/sanity-template-nextjs-clean"
                          target="_blank"
                          rel="noopener noreferrer"
                        >
                          <span className="whitespace-nowrap">View on GitHub</span>
                          <svg
                            xmlns="http://www.w3.org/2000/svg"
                            viewBox="0 0 24 24"
                            fill="currentColor"
                            className="hidden sm:block h-4 sm:h-6"
                          >
                            <path d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z"></path>
                          </svg>
                        </Link>
                      </li>
                    </ul>
                  </nav>
                </div>
              </div>
            </header>
          )
        }
        
  • sanity
    Folder
    • lib
      Folder
      • pagetype-queries
        Folder
        • {{.KebabCasePageTypeSingular}}.queries.ts
          File
          View Code
          import { defineQuery } from "next-sanity";
          import { linkFields, linkReference } from "../queries";
          import { pageBuilderFields } from "../queries";
          
          export const listFields = /* groq */ `
            _id,
            "name": coalesce(name, "Untitled"),
            "slug": slug.current,
            heading,
            subheading
          `;
          
          // List (plural)
          export const all{{.PascalCasePageTypePlural}}Query = defineQuery(`
            *[_type == "{{.LowerCasePageTypeSingular}}" && defined(slug.current)] | order(_updatedAt desc) {
              ${listFields}
            }
          `);
          
          // By slug (singular)
          export const {{.LowerCasePageTypeSingular}}BySlugQuery = defineQuery(`
            *[_type == "{{.LowerCasePageTypeSingular}}" && slug.current == $slug][0]{
              _id,
              _type,
              name,
              slug,
              heading,
              subheading,
              "pageBuilder": pageBuilder[]{
               ${pageBuilderFields}
              },
              
          
              },
            }
          `);
          
          
          // Slugs only
          export const {{.LowerCasePageTypeSingular}}Slugs = defineQuery(`
            *[_type == "{{.LowerCasePageTypeSingular}}" && defined(slug.current)]{ "slug": slug.current }
          `);
          
      • queries.ts
        File β€’ Action File
        Actions
        1. set linkFields to Export
          Behaviour: replaceIfMissing
          Occurrence: first
          Target: const linkFields = /* groq */ `
        2. Set postFields to Export
          Behaviour: replaceIfMissing
          Occurrence: first
          Target: const postFields = /* groq */ `
        3. Set linkReference to Export
          Behaviour: replaceIfMissing
          Occurrence: first
          Target: const linkReference = /* groq */ `
        4. connect up PageType as linkReference
          Behaviour: addMarkerBelowTarget
          Occurrence: first
          Target: _type == "link" => {
          Content
          "{{.LowerCasePageTypeSingular}}": {{.LowerCasePageTypeSingular}}->slug.current,
        5. adding PageType to Sitemap
          Behaviour: insertBeforeInline
          Occurrence: first
          Target: && defined(slug.current)] | order(_type asc) {
          Content
           || _type == "{{.LowerCasePageTypeSingular}}"
        6. Exportable PageBuilder Fields
          Behaviour: replaceBetween
          Occurrence: first
        View Code
        import {defineQuery} from 'next-sanity'
        
        export const settingsQuery = defineQuery(`*[_type == "settings"][0]`)
        
        export const postFields = /* groq */ `
          _id,
          "status": select(_originalId in path("drafts.**") => "draft", "published"),
          "title": coalesce(title, "Untitled"),
          "slug": slug.current,
          excerpt,
          coverImage,
          "date": coalesce(date, _updatedAt),
          "author": author->{firstName, lastName, picture},
        `
        
        export const linkReference = /* groq */ `
          _type == "link" => {
            "page": page->slug.current,
            "post": post->slug.current,
          }
        `
        
        export const linkFields = /* groq */ `
          link {
              ...,
              ${linkReference}
              }
        `
        
        export const pageBuilderFields = /* groq */ `
          ...,
          _type == "callToAction" => {
            ${linkFields},
          },
          _type == "infoSection" => {
            content[]{
              ...,
              titleDefs[]{
                ...,
                ${linkReference}
              }
            }
          }
        `
        
        export const getPageQuery = defineQuery(`
          *[_type == 'page' && slug.current == $slug][0]{
            _id,
            _type,
            name,
            slug,
            heading,
            subheading,
            "pageBuilder": pageBuilder[]{
              ${pageBuilderFields}
            },
          }
        `)
        
        
        
        export const sitemapData = defineQuery(`
          *[_type == "page" || _type == "post" && defined(slug.current)] | order(_type asc) {
            "slug": slug.current,
            _type,
            _updatedAt,
          }
        `)
        
        export const allPostsQuery = defineQuery(`
          *[_type == "post" && defined(slug.current)] | order(date desc, _updatedAt desc) {
            ${postFields}
          }
        `)
        
        export const morePostsQuery = defineQuery(`
          *[_type == "post" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] {
            ${postFields}
          }
        `)
        
        export const postQuery = defineQuery(`
          *[_type == "post" && slug.current == $slug] [0] {
            content[]{
            ...,
            titleDefs[]{
              ...,
              ${linkReference}
            }
          },
            ${postFields}
          }
        `)
        
        export const postPagesSlugs = defineQuery(`
          *[_type == "post" && defined(slug.current)]
          {"slug": slug.current}
        `)
        
        export const pagesSlugs = defineQuery(`
          *[_type == "page" && defined(slug.current)]
          {"slug": slug.current}
        `)
        
      • utils.ts
        File β€’ Action File
        Actions
        1. Setting up routing for PageType
          Behaviour: addMarkerAboveTarget
          Occurrence: first
          Target: default:
          Content
              case '{{.LowerCasePageTypeSingular}}': {
                const slug = (link as any)?.['{{.LowerCasePageTypeSingular}}']
                return typeof slug === 'string' ? `/{{.LowerCasePageTypePlural}}/${slug}` : null
              }
        View Code
        import createImageUrlBuilder from '@sanity/image-url'
        import {Link} from '@/sanity.types'
        import {dataset, projectId, studioUrl} from '@/sanity/lib/api'
        import {createDataAttribute, CreateDataAttributeProps} from 'next-sanity'
        import {getImageDimensions} from '@sanity/asset-utils'
        
        const imageBuilder = createImageUrlBuilder({
          projectId: projectId || '',
          dataset: dataset || '',
        })
        
        export const urlForImage = (source: any) => {
          // Ensure that source image contains a valid reference
          if (!source?.asset?._ref) {
            return undefined
          }
        
          const imageRef = source?.asset?._ref
          const crop = source.crop
        
          // get the image's og dimensions
          const {width, height} = getImageDimensions(imageRef)
        
          if (Boolean(crop)) {
            // compute the cropped image's area
            const croppedWidth = Math.floor(width * (1 - (crop.right + crop.left)))
        
            const croppedHeight = Math.floor(height * (1 - (crop.top + crop.bottom)))
        
            // compute the cropped image's position
            const left = Math.floor(width * crop.left)
            const top = Math.floor(height * crop.top)
        
            // gather into a url
            return imageBuilder?.image(source).rect(left, top, croppedWidth, croppedHeight).auto('format')
          }
        
          return imageBuilder?.image(source).auto('format')
        }
        
        export function resolveOpenGraphImage(image: any, width = 1200, height = 627) {
          if (!image) return
          const url = urlForImage(image)?.width(1200).height(627).fit('crop').url()
          if (!url) return
          return {url, alt: image?.alt as string, width, height}
        }
        
        // Depending on the type of link, we need to fetch the corresponding page, post, or URL.  Otherwise return null.
        export function linkResolver(link: Link | undefined) {
          if (!link) return null
        
          // If linkType is not set but href is, lets set linkType to "href".  This comes into play when pasting links into the portable text editor because a link type is not assumed.
          if (!link.linkType && link.href) {
            link.linkType = 'href'
          }
        
          switch (link.linkType) {
            case 'href':
              return link.href || null
            case 'page':
              if (link?.page && typeof link.page === 'string') {
                return `/${link.page}`
              }
            case 'post':
              if (link?.post && typeof link.post === 'string') {
                return `/posts/${link.post}`
              }
        
            default:
              return null
          }
        }
        
        type DataAttributeConfig = CreateDataAttributeProps &
          Required<Pick<CreateDataAttributeProps, 'id' | 'type' | 'path'>>
        
        export function dataAttr(config: DataAttributeConfig) {
          return createDataAttribute({
            projectId,
            dataset,
            baseUrl: studioUrl,
          }).combine(config)
        }
        
Base Path
studio/src/schemaTypes
Detailed view
  • documents
    Folder
    • {{.KebabCasePageTypeSingular}}.ts
      File
      View Code
      import {defineField, defineType} from 'sanity'
      import { DocumentIcon } from '@sanity/icons'
      
      export const {{.LowerCasePageTypeSingular}} = defineType({
        name: '{{.LowerCasePageTypeSingular}}',
        title: '{{.PascalCasePageTypeSingular}}',
        type: 'document',
        icon: DocumentIcon,
        fields: [
          defineField({
            name: 'name',
            title: 'Name',
            type: 'string',
            validation: (Rule) => Rule.required(),
          }),
      
          defineField({
            name: 'slug',
            title: 'Slug',
            type: 'slug',
            validation: (Rule) => Rule.required(),
            options: {
              source: 'name',
              maxLength: 96,
            },
          }),
          defineField({
            name: 'heading',
            title: 'Heading',
            type: 'string',
            validation: (Rule) => Rule.required(),
          }),
          defineField({
            name: 'subheading',
            title: 'Subheading',
            type: 'string',
          }),
          defineField({
            name: 'pageBuilder',
            type: "pageBuilder"
          }),
        ],
      })
      
  • objects
    Folder
    • blockContent.tsx
      File β€’ Action File
      Actions
      1. Adding PageType as a LinkType in BlockContent
        Behaviour: addMarkerAboveTarget
        Occurrence: first
        Target: ],
        Content
                            {title: '{{.PascalCasePageTypeSingular}}', value: '{{.LowerCasePageTypeSingular}}'},
      2. EXTRA INTERNAL LINK FIELD
        Behaviour: addMarkerAboveTarget
        Occurrence: last
        Target: defineField({
        Content
                      defineField({
                        name: '{{.LowerCasePageTypeSingular}}',
                        title: '{{.PascalCasePageTypeSingular}}',
                        type: 'reference',
                        to: [{type: '{{.LowerCasePageTypeSingular}}'}],
                        hidden: ({parent}) => parent?.linkType !== '{{.LowerCasePageTypeSingular}}',
                        validation: (Rule) =>
                          Rule.custom((value, context: any) => {
                            if (context.parent?.linkType === '{{.LowerCasePageTypeSingular}}' && !value) {
                              return '{{.PascalCasePageTypeSingular}} reference is required when Link Type is {{.PascalCasePageTypeSingular}}'
                            }
                            return true
                          }),
                      }),
      View Code
      import {defineArrayMember, defineType, defineField} from 'sanity'
      
      /**
       * This is the schema definition for the rich text fields used for
       * for this blog studio. When you import it in schemas.js it can be
       * reused in other parts of the studio with:
       *  {
       *    name: 'someName',
       *    title: 'Some title',
       *    type: 'blockContent'
       *  }
       *
       * Learn more: https://www.sanity.io/docs/block-content
       */
      export const blockContent = defineType({
        title: 'Block Content',
        name: 'blockContent',
        type: 'array',
        of: [
          defineArrayMember({
            type: 'block',
            titles: {
              annotations: [
                {
                  name: 'link',
                  type: 'object',
                  title: 'Link',
                  fields: [
                    defineField({
                      name: 'linkType',
                      title: 'Link Type',
                      type: 'string',
                      initialValue: 'href',
                      options: {
                        list: [
                          {title: 'URL', value: 'href'},
                          {title: 'Page', value: 'page'},
                          {title: 'Post', value: 'post'},
                        ],
                        layout: 'radio',
                      },
                    }),
      
                    defineField({
                      name: 'href',
                      title: 'URL',
                      type: 'url',
                      hidden: ({parent}) => parent?.linkType !== 'href' && parent?.linkType != null,
                      validation: (Rule) =>
                        Rule.custom((value, context: any) => {
                          if (context.parent?.linkType === 'href' && !value) {
                            return 'URL is required when Link Type is URL'
                          }
                          return true
                        }),
                    }),
      
                    defineField({
                      name: 'page',
                      title: 'Page',
                      type: 'reference',
                      to: [{type: 'page'}],
                      hidden: ({parent}) => parent?.linkType !== 'page',
                      validation: (Rule) =>
                        Rule.custom((value, context: any) => {
                          if (context.parent?.linkType === 'page' && !value) {
                            return 'Page reference is required when Link Type is Page'
                          }
                          return true
                        }),
                    }),
      
                    defineField({
                      name: 'post',
                      title: 'Post',
                      type: 'reference',
                      to: [{type: 'post'}],
                      hidden: ({parent}) => parent?.linkType !== 'post',
                      validation: (Rule) =>
                        Rule.custom((value, context: any) => {
                          if (context.parent?.linkType === 'post' && !value) {
                            return 'Post reference is required when Link Type is Post'
                          }
                          return true
                        }),
                    }),
      
                    defineField({
                      name: 'openInNewTab',
                      title: 'Open in new tab',
                      type: 'boolean',
                      initialValue: false,
                    }),
                  ],
                },
              ],
            },
          }),
        ],
      })
      
      
    • link.ts
      File β€’ Action File
      Actions
      1. Adding PageType as a LinkType option
        Behaviour: addMarkerAboveTarget
        Occurrence: first
        Target: ],
        Content
         {title: '{{.PascalCasePageTypeSingular}}', value: '{{.LowerCasePageTypeSingular}}'},
      2. Adding PageType as a Link Field
        Behaviour: addMarkerAboveTarget
        Occurrence: last
        Target: defineField({
        Content
            defineField({
              name: '{{.LowerCasePageTypeSingular}}',
              title: '{{.PascalCasePageTypeSingular}}',
              type: 'reference',
              to: [{type: '{{.LowerCasePageTypeSingular}}'}],
              hidden: ({parent}) => parent?.linkType !== '{{.LowerCasePageTypeSingular}}',
              validation: (Rule) =>
                Rule.custom((value, context: any) => {
                  if (context.parent?.linkType === '{{.LowerCasePageTypeSingular}}' && !value) {
                    return '{{.PascalCasePageTypeSingular}} reference is required when Link Type is {{.PascalCasePageTypeSingular}}'
                  }
                  return true
                }),
            }),
      View Code
      import {defineField, defineType} from 'sanity'
      import {LinkIcon} from '@sanity/icons'
      
      /**
       * Link schema object. This link object lets the user first select the type of link and then
       * then enter the URL, page reference, or post reference - depending on the type selected.
       * Learn more: https://www.sanity.io/docs/object-type
       */
      export const link = defineType({
        name: 'link',
        title: 'Link',
        type: 'object',
        icon: LinkIcon,
        fields: [
          defineField({
            name: 'linkType',
            title: 'Link Type',
            type: 'string',
            initialValue: 'url',
            options: {
              list: [
                {title: 'URL', value: 'href'},
                {title: 'Page', value: 'page'},
                {title: 'Post', value: 'post'},
              ],
              layout: 'radio',
            },
          }),
      
          // URL
          defineField({
            name: 'href',
            title: 'URL',
            type: 'url',
            hidden: ({parent}) => parent?.linkType !== 'href',
            validation: (Rule) =>
              Rule.custom((value, context: any) => {
                if (context.parent?.linkType === 'href' && !value) {
                  return 'URL is required when Link Type is URL'
                }
                return true
              }),
          }),
      
          // Page
          defineField({
            name: 'page',
            title: 'Page',
            type: 'reference',
            to: [{type: 'page'}],
            hidden: ({parent}) => parent?.linkType !== 'page',
            validation: (Rule) =>
              Rule.custom((value, context: any) => {
                if (context.parent?.linkType === 'page' && !value) {
                  return 'Page reference is required when Link Type is Page'
                }
                return true
              }),
          }),
      
          // Post
          defineField({
            name: 'post',
            title: 'Post',
            type: 'reference',
            to: [{type: 'post'}],
            hidden: ({parent}) => parent?.linkType !== 'post',
            validation: (Rule) =>
              Rule.custom((value, context: any) => {
                if (context.parent?.linkType === 'post' && !value) {
                  return 'Post reference is required when Link Type is Post'
                }
                return true
              }),
          }),
      
      
          defineField({
            name: 'openInNewTab',
            title: 'Open in new tab',
            type: 'boolean',
            initialValue: false,
          }),
        ],
      })
      
      
      
      
    • pageBuilder.tsx
      File
      View Code
      import { defineType } from "sanity";
      
      export const pageBuilder = defineType({
          name: 'pageBuilder',
          title: 'Page builder',
          type: 'array',
          of: [{type: 'callToAction'}, {type: 'infoSection'}],
          options: {
              insertMenu: {
                // Configure the "Add Item" menu to display a thumbnail preview of the content type. https://www.sanity.io/docs/array-type#efb1fe03459d
                views: [
                  {
                    name: 'grid',
                    previewImageUrl: (schemaTypeName) =>
                      `/static/page-builder-thumbnails/${schemaTypeName}.webp`,
                  },
                ],
              },
          }
      })
              
  • index.ts
    File β€’ Action File
    Actions
    1. Importing PageType to SchemaTypos Index
      Behaviour: addMarkerBelowTarget
      Occurrence: first
      Target: import {post} from './documents/post'
      Content
      import { {{.LowerCasePageTypeSingular}} } from './documents/{{.KebabCasePageTypeSingular}}'
    2. Adding DocumentType to SchemaType array
      Behaviour: addMarkerAboveTarget
      Occurrence: last
      Target: // Objects
      Content
      {{.LowerCasePageTypeSingular}},
    3. Add Pagebuilder Object to SchemaTypes
      Behaviour: addMarkerAboveTarget
      Occurrence: last
      Target: ]
      Content
      pageBuilder,
    4. Import PageBuilder Object to schemaTypes index
      Behaviour: addMarkerAboveTarget
      Occurrence: first
      Target: export const schemaTypes = [
      Content
      import {pageBuilder} from './objects/pageBuilder'
    View Code
    import {person} from './documents/person'
    import {page} from './documents/page'
    import {post} from './documents/post'
    import {callToAction} from './objects/callToAction'
    import {infoSection} from './objects/infoSection'
    import {settings} from './singletons/settings'
    import {link} from './objects/link'
    import {blockContent} from './objects/blockContent'
    
    export const schemaTypes = [
      // Singletons
      settings,
      // Documents
      page,
      post,
      person,
      // Objects
      blockContent,
      infoSection,
      callToAction,
      link,
    ]
    
Raw JSON
Debug view of the fetched document
{
  "_createdAt": "2025-09-15T12:11:26Z",
  "_id": "17963bea-94ef-4877-bec5-bdd2de85a31c",
  "_rev": "fzxCIsOYL58G2O1cjyQMwa",
  "_system": {
    "base": {
      "id": "17963bea-94ef-4877-bec5-bdd2de85a31c",
      "rev": "G2Y6JXdwXQRnotb29gv4fr"
    }
  },
  "_type": "command-slug",
  "_updatedAt": "2025-09-16T12:13:24Z",
  "description": "Instantly add a PageType with Page Builder across your Next.js frontend and Sanity Studio β€” generating a listing page (index / archive) and per-item detail pages (slug), wiring GROQ queries and client-side routing, and updating navigation and the sitemap for ready-to-publish coverage. ",
  "filePaths": [
    {
      "id": "path-1757177493327-sb16mt1",
      "nodes": [
        {
          "_key": "1757940166544-ae8vktjvy",
          "_type": "treeNode",
          "actionFile": false,
          "actions": [],
          "children": [
            {
              "_key": "20250903-front-index-folder",
              "_type": "treeNode",
              "actionFile": false,
              "actions": [],
              "children": [
                {
                  "_key": "1757177532693-d16ts7xzg",
                  "_type": "treeNode",
                  "actionFile": false,
                  "actions": [],
                  "children": [
                    {
                      "_key": "20250903-front-index-file",
                      "_type": "treeNode",
                      "actionFile": false,
                      "actions": [
                        {
                          "_key": "kWkIvI7T6XFbABGiWe5cGp",
                          "logic": {},
                          "mark": ""
                        }
                      ],
                      "children": [],
                      "code": "import Link from \"next/link\";\r\nimport type { Metadata } from \"next\";\r\nimport { client } from \"@/sanity/lib/client\";\r\nimport { all{{.PascalCasePageTypePlural}}Query } from \"@/sanity/lib/pagetype-queries/{{.KebabCasePageTypeSingular}}.queries\";\r\nimport { All{{.PascalCasePageTypePlural}} } from \"@/app/components/{{.PascalCasePageTypePlural}}\";\r\n\r\nexport const metadata: Metadata = {\r\n  title: \"{{.PascalCasePageTypePlural}}\",\r\n  description: \"All {{.LowerCasePageTypePlural}}\"\r\n};\r\n\r\nexport default async function {{.PascalCasePageTypeSingular}}IndexPage() {\r\n  const items = await client.fetch(all{{.PascalCasePageTypePlural}}Query);\r\n\r\n  if (!items?.length) {\r\n    return (\r\n      <main className=\"container mx-auto p-6\">\r\n        <h1 className=\"text-2xl font-semibold\">{{.PascalCasePageTypePlural}}</h1>\r\n        <p className=\"opacity-70 mt-2\">No {{.LowerCasePageTypePlural}} yet.</p>\r\n      </main>\r\n    );\r\n  }\r\n\r\n  return (\r\n    <main className=\"container mx-auto p-6\">\r\n    <All{{.PascalCasePageTypePlural}} />\r\n    </main>\r\n  );\r\n}\r\n",
                      "id": "file-frontend-index",
                      "name": "page.tsx",
                      "nodeType": "file"
                    }
                  ],
                  "code": "",
                  "id": "folder-1757177532693",
                  "name": "(index)",
                  "nodeType": "folder"
                },
                {
                  "_key": "20250903-front-slug-param",
                  "_type": "treeNode",
                  "actionFile": false,
                  "actions": [],
                  "children": [
                    {
                      "_key": "20250903-front-slug-page",
                      "_type": "treeNode",
                      "actionFile": false,
                      "actions": [
                        {
                          "_key": "kWkIvI7T6XFbABGiWe5cLM",
                          "logic": {},
                          "mark": ""
                        }
                      ],
                      "children": [],
                      "code": "import type { Metadata, ResolvingMetadata } from 'next'\r\nimport { sanityFetch } from '@/sanity/lib/live'\r\nimport { {{.LowerCasePageTypeSingular}}Slugs, {{.LowerCasePageTypeSingular}}BySlugQuery } from '@/sanity/lib/pagetype-queries/{{.KebabCasePageTypeSingular}}.queries'\r\nimport PageBuilderPage from '@/app/components/PageBuilder'\r\nimport { PageOnboarding } from '@/app/components/Onboarding'\r\n\r\ntype Props = {\r\n  params: Promise<{ slug: string }>\r\n}\r\n\r\n/**\r\n * Generate the static params for {{.LowerCasePageTypeSingular}}.\r\n */\r\nexport async function generateStaticParams() {\r\n  const { data } = await sanityFetch({\r\n    query: {{.LowerCasePageTypeSingular}}Slugs,\r\n    perspective: 'published',\r\n    stega: false,\r\n  })\r\n  return data\r\n}\r\n\r\n/**\r\n * Generate metadata for the {{.LowerCasePageTypeSingular}} page.\r\n */\r\nexport async function generateMetadata(props: Props, _parent: ResolvingMetadata): Promise<Metadata> {\r\n  const params = await props.params\r\n  const { data: doc } = await sanityFetch({\r\n    query: {{.LowerCasePageTypeSingular}}BySlugQuery,\r\n    params,\r\n    stega: false,\r\n  })\r\n\r\n  // Title/description fallbacks so this template works across different field sets\r\n  const title = (doc?.name ?? doc?.title ?? '{{.PascalCasePageTypeSingular}}') as string | undefined\r\n  const description = (doc?.heading ?? doc?.subheading ?? undefined) as string | undefined\r\n\r\n  return {\r\n    title,\r\n    description,\r\n  } satisfies Metadata\r\n}\r\n\r\nexport default async function {{.PascalCasePageTypeSingular}}Page(props: Props) {\r\n  const params = await props.params\r\n\r\n  const [{ data: doc }] = await Promise.all([\r\n    sanityFetch({ query: {{.LowerCasePageTypeSingular}}BySlugQuery, params }),\r\n  ])\r\n\r\n  if (!doc?._id) {\r\n    return (\r\n      <div className=\"py-40\">\r\n        <PageOnboarding />\r\n      </div>\r\n    )\r\n  }\r\n\r\n  return (\r\n    <div className=\"my-12 lg:my-24\">\r\n      <div className=\"\">\r\n        <div className=\"container\">\r\n          <div className=\"pb-6 border-b border-gray-100\">\r\n            <div className=\"max-w-3xl\">\r\n              <h2 className=\"text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl lg:text-7xl\">\r\n                {doc?.heading ?? doc?.name ?? doc?.title}\r\n              </h2>\r\n              {(doc?.subheading ?? doc?.excerpt) && (\r\n                <p className=\"mt-4 text-base lg:text-lg leading-relaxed text-gray-600 uppercase font-light\">\r\n                  {doc?.subheading ?? doc?.excerpt}\r\n                </p>\r\n              )}\r\n            </div>\r\n          </div>\r\n        </div>\r\n      </div>\r\n\r\n      {/* Keep your existing renderer */}\r\n      <PageBuilderPage page={doc as any} />\r\n    </div>\r\n  )\r\n}\r\n",
                      "id": "file-frontend-slug-page",
                      "name": "page.tsx",
                      "nodeType": "file"
                    }
                  ],
                  "code": "",
                  "id": "folder-frontend-slug",
                  "name": "[slug]",
                  "nodeType": "folder"
                }
              ],
              "code": "",
              "id": "folder-frontend-index",
              "name": "{{.KebabCasePageTypePlural}}",
              "nodeType": "folder"
            },
            {
              "_key": "1757939590529-1aa1hehxl",
              "_type": "treeNode",
              "actionFile": false,
              "actions": [],
              "children": [
                {
                  "_key": "1757176137674-q3yigc355",
                  "_type": "treeNode",
                  "actionFile": false,
                  "actions": [
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5cPt",
                      "logic": {},
                      "mark": ""
                    }
                  ],
                  "children": [],
                  "code": "import Link from 'next/link'\r\n\r\nimport { sanityFetch } from '@/sanity/lib/live'\r\nimport { all{{.PascalCasePageTypePlural}}Query } from '@/sanity/lib/pagetype-queries/{{.KebabCasePageTypeSingular}}.queries'\r\nimport DateComponent from '@/app/components/Date'\r\nimport OnBoarding from '@/app/components/Onboarding'\r\nimport Avatar from '@/app/components/Avatar'\r\nimport { createDataAttribute } from 'next-sanity'\r\n\r\ntype {{.PascalCasePageTypeSingular}}ListItem = {\r\n  _id: string\r\n  title?: string\r\n  name?: string\r\n  slug: string\r\n  excerpt?: string | null\r\n  subheading?: string | null\r\n  coverImage?: unknown\r\n  date?: string\r\n  author?:\r\n    | {\r\n        firstName?: string\r\n        lastName?: string\r\n        picture?: unknown\r\n      }\r\n    | null\r\n}\r\n\r\nconst {{.PascalCasePageTypeSingular}}Card = ({ item }: { item: {{.PascalCasePageTypeSingular}}ListItem }) => {\r\n  const { _id, slug, date, author } = item\r\n  const title = item.title ?? item.name ?? 'Untitled'\r\n  const excerpt = (item.excerpt ?? item.subheading) ?? null\r\n\r\n  const attr = createDataAttribute({\r\n    id: _id,\r\n    type: '{{.LowerCasePageTypeSingular}}',\r\n    path: (item.title ? 'title' : 'name') as 'title' | 'name',\r\n  })\r\n\r\n  return (\r\n    <article\r\n      data-sanity={attr()}\r\n      key={_id}\r\n      className=\"border border-gray-200 rounded-sm p-6 bg-gray-50 flex flex-col justify-between transition-colors hover:bg-white relative\"\r\n    >\r\n      <Link className=\"hover:text-brand underline transition-colors\" href={`/{{.LowerCasePageTypePlural}}/${slug}`}>\r\n        <span className=\"absolute inset-0 z-10\" />\r\n      </Link>\r\n\r\n      <div>\r\n        <h3 className=\"text-2xl font-bold mb-4 leading-tight\">{title}</h3>\r\n\r\n        {excerpt && (\r\n          <p className=\"line-clamp-3 text-sm leading-6 text-gray-600 max-w-[70ch]\">{excerpt}</p>\r\n        )}\r\n      </div>\r\n\r\n      <div className=\"flex items-center justify-between mt-6 pt-4 border-t border-gray-100\">\r\n        {author?.firstName && author?.lastName && (\r\n          <div className=\"flex items-center\">\r\n            <Avatar person={author as any} small={true} />\r\n          </div>\r\n        )}\r\n        {date && (\r\n          <time className=\"text-gray-500 text-xs font-mono\" dateTime={date}>\r\n            <DateComponent dateString={date} />\r\n          </time>\r\n        )}\r\n      </div>\r\n    </article>\r\n  )\r\n}\r\n\r\nconst {{.PascalCasePageTypePlural}} = ({\r\n  children,\r\n  heading,\r\n  subHeading,\r\n}: {\r\n  children: React.ReactNode\r\n  heading?: string\r\n  subHeading?: string\r\n}) => (\r\n  <div>\r\n    {heading && (\r\n      <h2 className=\"text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl lg:text-5xl\">\r\n        {heading}\r\n      </h2>\r\n    )}\r\n    {subHeading && <p className=\"mt-2 text-lg leading-8 text-gray-600\">{subHeading}</p>}\r\n\r\n    <div className=\"pt-6 space-y-6\">{children}</div>\r\n  </div>\r\n)\r\n\r\nexport const All{{.PascalCasePageTypePlural}} = async () => {\r\n  const { data } = await sanityFetch({ query: all{{.PascalCasePageTypePlural}}Query })\r\n\r\n  if (!data || data.length === 0) {\r\n    return <OnBoarding />\r\n  }\r\n\r\n  const list = data as unknown as {{.PascalCasePageTypeSingular}}ListItem[]\r\n\r\n  return (\r\n    <{{.PascalCasePageTypePlural}}\r\n      heading=\"{{.PascalCasePageTypePlural}}\"\r\n      subHeading=\"{{.PascalCasePageTypePlural}} populated from your Sanity Studio.\"\r\n    >\r\n      {list.map((item) => (\r\n        <{{.PascalCasePageTypeSingular}}Card key={item._id} item={item} />\r\n      ))}\r\n    </{{.PascalCasePageTypePlural}}>\r\n  )\r\n}\r\n",
                  "id": "file-1757176137674",
                  "name": "{{.PascalCasePageTypePlural}}.tsx",
                  "nodeType": "file"
                },
                {
                  "_key": "1756989436755-t8zcsbyvk",
                  "_type": "treeNode",
                  "actionFile": true,
                  "actions": [
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5cUQ",
                      "logic": {
                        "behaviour": "addMarkerBelowTarget",
                        "content": "<Link href=\"/{{.LowerCasePageTypePlural}}\" className=\"mr-6 hover:underline\">{{.PascalCasePageTypePlural}}</Link>",
                        "fallbackOnly": true,
                        "occurrence": "first",
                        "target": "<li>"
                      },
                      "mark": "",
                      "title": "PAGETYPE ARCHIVE LINK"
                    }
                  ],
                  "children": [],
                  "code": "import Link from 'next/link'\r\nimport {settingsQuery} from '@/sanity/lib/queries'\r\nimport {sanityFetch} from '@/sanity/lib/live'\r\n\r\nexport default async function Header() {\r\n  const {data: settings} = await sanityFetch({\r\n    query: settingsQuery,\r\n  })\r\n\r\n  return (\r\n    <header className=\"fixed z-50 h-24 inset-0 bg-white/80 flex items-center backdrop-blur-lg\">\r\n      <div className=\"container py-6 px-2 sm:px-6\">\r\n        <div className=\"flex items-center justify-between gap-5\">\r\n          <Link className=\"flex items-center gap-2\" href=\"/\">\r\n            <span className=\"text-lg sm:text-2xl pl-2 font-semibold\">\r\n              {settings?.title || 'Sanity + Next.js'}\r\n            </span>\r\n          </Link>\r\n\r\n          <nav>\r\n            <ul\r\n              role=\"list\"\r\n              className=\"flex items-center gap-4 md:gap-6 leading-5 text-xs sm:text-base tracking-tight font-mono\"\r\n            >\r\n              <li>\r\n                <Link href=\"/about\" className=\"hover:underline\">\r\n                  About\r\n                </Link>\r\n              </li>\r\n\r\n              <li className=\"sm:before:w-[1px] sm:before:bg-gray-200 before:block flex sm:gap-4 md:gap-6\">\r\n                <Link\r\n                  className=\"rounded-full flex gap-4 items-center bg-black hover:bg-blue focus:bg-blue py-2 px-4 justify-center sm:py-3 sm:px-6 text-white transition-colors duration-200\"\r\n                  href=\"https://github.com/sanity-io/sanity-template-nextjs-clean\"\r\n                  target=\"_blank\"\r\n                  rel=\"noopener noreferrer\"\r\n                >\r\n                  <span className=\"whitespace-nowrap\">View on GitHub</span>\r\n                  <svg\r\n                    xmlns=\"http://www.w3.org/2000/svg\"\r\n                    viewBox=\"0 0 24 24\"\r\n                    fill=\"currentColor\"\r\n                    className=\"hidden sm:block h-4 sm:h-6\"\r\n                  >\r\n                    <path d=\"M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z\"></path>\r\n                  </svg>\r\n                </Link>\r\n              </li>\r\n            </ul>\r\n          </nav>\r\n        </div>\r\n      </div>\r\n    </header>\r\n  )\r\n}\r\n",
                  "id": "file-1756989436755",
                  "name": "Header.tsx",
                  "nodeType": "file"
                }
              ],
              "code": "",
              "id": "folder-1757939590529",
              "name": "components",
              "nodeType": "folder"
            }
          ],
          "code": "",
          "id": "folder-1757940166544",
          "name": "app",
          "nodeType": "folder"
        },
        {
          "_key": "1757940188389-c6z21ksgn",
          "_type": "treeNode",
          "actionFile": false,
          "actions": [],
          "children": [
            {
              "_key": "1757940203908-vor0y4ba6",
              "_type": "treeNode",
              "actionFile": false,
              "actions": [],
              "children": [
                {
                  "_key": "1757064566263-k9lklr230",
                  "_type": "treeNode",
                  "actionFile": false,
                  "actions": [],
                  "children": [
                    {
                      "_key": "20250903-front-queries-file",
                      "_type": "treeNode",
                      "actionFile": false,
                      "actions": [
                        {
                          "_key": "kWkIvI7T6XFbABGiWe5cYx",
                          "logic": {},
                          "mark": ""
                        }
                      ],
                      "children": [],
                      "code": "import { defineQuery } from \"next-sanity\";\r\nimport { linkFields, linkReference } from \"../queries\";\r\nimport { pageBuilderFields } from \"../queries\";\r\n\r\nexport const listFields = /* groq */ `\r\n  _id,\r\n  \"name\": coalesce(name, \"Untitled\"),\r\n  \"slug\": slug.current,\r\n  heading,\r\n  subheading\r\n`;\r\n\r\n// List (plural)\r\nexport const all{{.PascalCasePageTypePlural}}Query = defineQuery(`\r\n  *[_type == \"{{.LowerCasePageTypeSingular}}\" && defined(slug.current)] | order(_updatedAt desc) {\r\n    ${listFields}\r\n  }\r\n`);\r\n\r\n// By slug (singular)\r\nexport const {{.LowerCasePageTypeSingular}}BySlugQuery = defineQuery(`\r\n  *[_type == \"{{.LowerCasePageTypeSingular}}\" && slug.current == $slug][0]{\r\n    _id,\r\n    _type,\r\n    name,\r\n    slug,\r\n    heading,\r\n    subheading,\r\n    \"pageBuilder\": pageBuilder[]{\r\n     ${pageBuilderFields}\r\n    },\r\n    \r\n\r\n    },\r\n  }\r\n`);\r\n\r\n\r\n// Slugs only\r\nexport const {{.LowerCasePageTypeSingular}}Slugs = defineQuery(`\r\n  *[_type == \"{{.LowerCasePageTypeSingular}}\" && defined(slug.current)]{ \"slug\": slug.current }\r\n`);\r\n",
                      "id": "file-frontend-queries",
                      "name": "{{.KebabCasePageTypeSingular}}.queries.ts",
                      "nodeType": "file"
                    }
                  ],
                  "code": "",
                  "id": "folder-1757064566263",
                  "name": "pagetype-queries",
                  "nodeType": "folder"
                },
                {
                  "_key": "1756919951450-hqircx415",
                  "_type": "treeNode",
                  "actionFile": true,
                  "actions": [
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5cdU",
                      "logic": {
                        "behaviour": "replaceIfMissing",
                        "occurrence": "first",
                        "replacement": "export const linkFields = /* groq */ `",
                        "requireAbsent": "export const linkFields = /* groq */ `",
                        "target": "const linkFields = /* groq */ `"
                      },
                      "mark": "",
                      "title": "set linkFields to Export"
                    },
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5ci1",
                      "logic": {
                        "behaviour": "replaceIfMissing",
                        "occurrence": "first",
                        "replacement": "export const postFields = /* groq */ `",
                        "requireAbsent": "export const postFields = /* groq */ `",
                        "target": "const postFields = /* groq */ `"
                      },
                      "mark": "",
                      "title": "Set postFields to Export"
                    },
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5cmY",
                      "logic": {
                        "behaviour": "replaceIfMissing",
                        "occurrence": "first",
                        "replacement": "export const linkReference = /* groq */ `",
                        "requireAbsent": "export const linkReference = /* groq */ `",
                        "target": "const linkReference = /* groq */ `"
                      },
                      "mark": "",
                      "title": "Set linkReference to Export"
                    },
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5cr5",
                      "logic": {
                        "behaviour": "addMarkerBelowTarget",
                        "content": "\"{{.LowerCasePageTypeSingular}}\": {{.LowerCasePageTypeSingular}}->slug.current,",
                        "occurrence": "first",
                        "target": "_type == \"link\" => {"
                      },
                      "mark": "",
                      "title": "connect up PageType as linkReference"
                    },
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5cvc",
                      "logic": {
                        "behaviour": "insertBeforeInline",
                        "content": " || _type == \"{{.LowerCasePageTypeSingular}}\"",
                        "fallbackOnly": true,
                        "occurrence": "first",
                        "target": "&& defined(slug.current)] | order(_type asc) {"
                      },
                      "mark": "",
                      "title": "adding PageType to Sitemap"
                    },
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5d09",
                      "logic": {
                        "behaviour": "replaceBetween",
                        "occurrence": "first",
                        "replacement": "export const pageBuilderFields = /* groq */ `\n  ...,\n  _type == \"callToAction\" => {\n    ${linkFields},\n  },\n  _type == \"infoSection\" => {\n    content[]{\n      ...,\n      titleDefs[]{\n        ...,\n        ${linkReference}\n      }\n    }\n  }\n`\n\nexport const getPageQuery = defineQuery(`\n  *[_type == 'page' && slug.current == $slug][0]{\n    _id,\n    _type,\n    name,\n    slug,\n    heading,\n    subheading,\n    \"pageBuilder\": pageBuilder[]{\n      ${pageBuilderFields}\n    },\n  }\n`)",
                        "requireAbsent": "export const pageBuilderFields = /* groq */ `",
                        "targetEnd": "`)",
                        "targetStart": "export const getPageQuery = defineQuery(`"
                      },
                      "mark": "",
                      "title": "Exportable PageBuilder Fields"
                    }
                  ],
                  "children": [],
                  "code": "import {defineQuery} from 'next-sanity'\r\n\r\nexport const settingsQuery = defineQuery(`*[_type == \"settings\"][0]`)\r\n\r\nexport const postFields = /* groq */ `\r\n  _id,\r\n  \"status\": select(_originalId in path(\"drafts.**\") => \"draft\", \"published\"),\r\n  \"title\": coalesce(title, \"Untitled\"),\r\n  \"slug\": slug.current,\r\n  excerpt,\r\n  coverImage,\r\n  \"date\": coalesce(date, _updatedAt),\r\n  \"author\": author->{firstName, lastName, picture},\r\n`\r\n\r\nexport const linkReference = /* groq */ `\r\n  _type == \"link\" => {\r\n    \"page\": page->slug.current,\r\n    \"post\": post->slug.current,\r\n  }\r\n`\r\n\r\nexport const linkFields = /* groq */ `\r\n  link {\r\n      ...,\r\n      ${linkReference}\r\n      }\r\n`\r\n\r\nexport const pageBuilderFields = /* groq */ `\r\n  ...,\r\n  _type == \"callToAction\" => {\r\n    ${linkFields},\r\n  },\r\n  _type == \"infoSection\" => {\r\n    content[]{\r\n      ...,\r\n      titleDefs[]{\r\n        ...,\r\n        ${linkReference}\r\n      }\r\n    }\r\n  }\r\n`\r\n\r\nexport const getPageQuery = defineQuery(`\r\n  *[_type == 'page' && slug.current == $slug][0]{\r\n    _id,\r\n    _type,\r\n    name,\r\n    slug,\r\n    heading,\r\n    subheading,\r\n    \"pageBuilder\": pageBuilder[]{\r\n      ${pageBuilderFields}\r\n    },\r\n  }\r\n`)\r\n\r\n\r\n\r\nexport const sitemapData = defineQuery(`\r\n  *[_type == \"page\" || _type == \"post\" && defined(slug.current)] | order(_type asc) {\r\n    \"slug\": slug.current,\r\n    _type,\r\n    _updatedAt,\r\n  }\r\n`)\r\n\r\nexport const allPostsQuery = defineQuery(`\r\n  *[_type == \"post\" && defined(slug.current)] | order(date desc, _updatedAt desc) {\r\n    ${postFields}\r\n  }\r\n`)\r\n\r\nexport const morePostsQuery = defineQuery(`\r\n  *[_type == \"post\" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] {\r\n    ${postFields}\r\n  }\r\n`)\r\n\r\nexport const postQuery = defineQuery(`\r\n  *[_type == \"post\" && slug.current == $slug] [0] {\r\n    content[]{\r\n    ...,\r\n    titleDefs[]{\r\n      ...,\r\n      ${linkReference}\r\n    }\r\n  },\r\n    ${postFields}\r\n  }\r\n`)\r\n\r\nexport const postPagesSlugs = defineQuery(`\r\n  *[_type == \"post\" && defined(slug.current)]\r\n  {\"slug\": slug.current}\r\n`)\r\n\r\nexport const pagesSlugs = defineQuery(`\r\n  *[_type == \"page\" && defined(slug.current)]\r\n  {\"slug\": slug.current}\r\n`)\r\n",
                  "id": "file-1756919951450",
                  "name": "queries.ts",
                  "nodeType": "file"
                },
                {
                  "_key": "node-utils-linktype-case",
                  "_type": "treeNode",
                  "actionFile": true,
                  "actions": [
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5d4g",
                      "logic": {
                        "behaviour": "addMarkerAboveTarget",
                        "content": "    case '{{.LowerCasePageTypeSingular}}': {\n      const slug = (link as any)?.['{{.LowerCasePageTypeSingular}}']\n      return typeof slug === 'string' ? `/{{.LowerCasePageTypePlural}}/${slug}` : null\n    }",
                        "mark": "PAGETYPE ROUTE",
                        "occurrence": "first",
                        "target": "default:"
                      },
                      "mark": "",
                      "title": "Setting up routing for PageType"
                    }
                  ],
                  "children": [],
                  "code": "import createImageUrlBuilder from '@sanity/image-url'\r\nimport {Link} from '@/sanity.types'\r\nimport {dataset, projectId, studioUrl} from '@/sanity/lib/api'\r\nimport {createDataAttribute, CreateDataAttributeProps} from 'next-sanity'\r\nimport {getImageDimensions} from '@sanity/asset-utils'\r\n\r\nconst imageBuilder = createImageUrlBuilder({\r\n  projectId: projectId || '',\r\n  dataset: dataset || '',\r\n})\r\n\r\nexport const urlForImage = (source: any) => {\r\n  // Ensure that source image contains a valid reference\r\n  if (!source?.asset?._ref) {\r\n    return undefined\r\n  }\r\n\r\n  const imageRef = source?.asset?._ref\r\n  const crop = source.crop\r\n\r\n  // get the image's og dimensions\r\n  const {width, height} = getImageDimensions(imageRef)\r\n\r\n  if (Boolean(crop)) {\r\n    // compute the cropped image's area\r\n    const croppedWidth = Math.floor(width * (1 - (crop.right + crop.left)))\r\n\r\n    const croppedHeight = Math.floor(height * (1 - (crop.top + crop.bottom)))\r\n\r\n    // compute the cropped image's position\r\n    const left = Math.floor(width * crop.left)\r\n    const top = Math.floor(height * crop.top)\r\n\r\n    // gather into a url\r\n    return imageBuilder?.image(source).rect(left, top, croppedWidth, croppedHeight).auto('format')\r\n  }\r\n\r\n  return imageBuilder?.image(source).auto('format')\r\n}\r\n\r\nexport function resolveOpenGraphImage(image: any, width = 1200, height = 627) {\r\n  if (!image) return\r\n  const url = urlForImage(image)?.width(1200).height(627).fit('crop').url()\r\n  if (!url) return\r\n  return {url, alt: image?.alt as string, width, height}\r\n}\r\n\r\n// Depending on the type of link, we need to fetch the corresponding page, post, or URL.  Otherwise return null.\r\nexport function linkResolver(link: Link | undefined) {\r\n  if (!link) return null\r\n\r\n  // If linkType is not set but href is, lets set linkType to \"href\".  This comes into play when pasting links into the portable text editor because a link type is not assumed.\r\n  if (!link.linkType && link.href) {\r\n    link.linkType = 'href'\r\n  }\r\n\r\n  switch (link.linkType) {\r\n    case 'href':\r\n      return link.href || null\r\n    case 'page':\r\n      if (link?.page && typeof link.page === 'string') {\r\n        return `/${link.page}`\r\n      }\r\n    case 'post':\r\n      if (link?.post && typeof link.post === 'string') {\r\n        return `/posts/${link.post}`\r\n      }\r\n\r\n    default:\r\n      return null\r\n  }\r\n}\r\n\r\ntype DataAttributeConfig = CreateDataAttributeProps &\r\n  Required<Pick<CreateDataAttributeProps, 'id' | 'type' | 'path'>>\r\n\r\nexport function dataAttr(config: DataAttributeConfig) {\r\n  return createDataAttribute({\r\n    projectId,\r\n    dataset,\r\n    baseUrl: studioUrl,\r\n  }).combine(config)\r\n}\r\n",
                  "id": "node-utils-linktype-case",
                  "name": "utils.ts",
                  "nodeType": "file"
                }
              ],
              "code": "",
              "id": "folder-1757940203908",
              "name": "lib",
              "nodeType": "folder"
            }
          ],
          "code": "",
          "id": "folder-1757940188389",
          "name": "sanity",
          "nodeType": "folder"
        }
      ],
      "path": "frontend"
    },
    {
      "id": "path-1757177493327-r6hgfxf",
      "nodes": [
        {
          "_key": "1757939529202-zdr6de4t6",
          "_type": "treeNode",
          "actionFile": false,
          "actions": [],
          "children": [
            {
              "_key": "20250903-studio-schema-file",
              "_type": "treeNode",
              "actionFile": false,
              "actions": [
                {
                  "logic": {
                    "behaviour": null,
                    "content": null,
                    "mark": null,
                    "occurrence": null,
                    "target": null
                  },
                  "mark": "",
                  "title": null
                }
              ],
              "children": [],
              "code": "import {defineField, defineType} from 'sanity'\r\nimport { DocumentIcon } from '@sanity/icons'\r\n\r\nexport const {{.LowerCasePageTypeSingular}} = defineType({\r\n  name: '{{.LowerCasePageTypeSingular}}',\r\n  title: '{{.PascalCasePageTypeSingular}}',\r\n  type: 'document',\r\n  icon: DocumentIcon,\r\n  fields: [\r\n    defineField({\r\n      name: 'name',\r\n      title: 'Name',\r\n      type: 'string',\r\n      validation: (Rule) => Rule.required(),\r\n    }),\r\n\r\n    defineField({\r\n      name: 'slug',\r\n      title: 'Slug',\r\n      type: 'slug',\r\n      validation: (Rule) => Rule.required(),\r\n      options: {\r\n        source: 'name',\r\n        maxLength: 96,\r\n      },\r\n    }),\r\n    defineField({\r\n      name: 'heading',\r\n      title: 'Heading',\r\n      type: 'string',\r\n      validation: (Rule) => Rule.required(),\r\n    }),\r\n    defineField({\r\n      name: 'subheading',\r\n      title: 'Subheading',\r\n      type: 'string',\r\n    }),\r\n    defineField({\r\n      name: 'pageBuilder',\r\n      type: \"pageBuilder\"\r\n    }),\r\n  ],\r\n})\r\n",
              "id": "file-studio-schema",
              "name": "{{.KebabCasePageTypeSingular}}.ts",
              "nodeType": "file"
            }
          ],
          "code": "",
          "id": "folder-1757939529202",
          "name": "documents",
          "nodeType": "folder"
        },
        {
          "_key": "1757939548968-0jrm2e8z9",
          "_type": "treeNode",
          "actionFile": false,
          "actions": [],
          "children": [
            {
              "_key": "node-blockcontent-fixes",
              "_type": "treeNode",
              "actionFile": true,
              "actions": [
                {
                  "logic": {
                    "behaviour": "addMarkerAboveTarget",
                    "content": "                    {title: '{{.PascalCasePageTypeSingular}}', value: '{{.LowerCasePageTypeSingular}}'},",
                    "mark": "LINKTYPE OPTION",
                    "occurrence": "first",
                    "target": "],"
                  },
                  "mark": "",
                  "title": "Adding PageType as a LinkType in BlockContent"
                },
                {
                  "logic": {
                    "behaviour": "addMarkerAboveTarget",
                    "content": "              defineField({\n                name: '{{.LowerCasePageTypeSingular}}',\n                title: '{{.PascalCasePageTypeSingular}}',\n                type: 'reference',\n                to: [{type: '{{.LowerCasePageTypeSingular}}'}],\n                hidden: ({parent}) => parent?.linkType !== '{{.LowerCasePageTypeSingular}}',\n                validation: (Rule) =>\n                  Rule.custom((value, context: any) => {\n                    if (context.parent?.linkType === '{{.LowerCasePageTypeSingular}}' && !value) {\n                      return '{{.PascalCasePageTypeSingular}} reference is required when Link Type is {{.PascalCasePageTypeSingular}}'\n                    }\n                    return true\n                  }),\n              }),",
                    "mark": "PAGETYPE AS FIELD",
                    "occurrence": "last",
                    "target": "defineField({"
                  },
                  "mark": "",
                  "title": "EXTRA INTERNAL LINK FIELD"
                }
              ],
              "children": [],
              "code": "import {defineArrayMember, defineType, defineField} from 'sanity'\r\n\r\n/**\r\n * This is the schema definition for the rich text fields used for\r\n * for this blog studio. When you import it in schemas.js it can be\r\n * reused in other parts of the studio with:\r\n *  {\r\n *    name: 'someName',\r\n *    title: 'Some title',\r\n *    type: 'blockContent'\r\n *  }\r\n *\r\n * Learn more: https://www.sanity.io/docs/block-content\r\n */\r\nexport const blockContent = defineType({\r\n  title: 'Block Content',\r\n  name: 'blockContent',\r\n  type: 'array',\r\n  of: [\r\n    defineArrayMember({\r\n      type: 'block',\r\n      titles: {\r\n        annotations: [\r\n          {\r\n            name: 'link',\r\n            type: 'object',\r\n            title: 'Link',\r\n            fields: [\r\n              defineField({\r\n                name: 'linkType',\r\n                title: 'Link Type',\r\n                type: 'string',\r\n                initialValue: 'href',\r\n                options: {\r\n                  list: [\r\n                    {title: 'URL', value: 'href'},\r\n                    {title: 'Page', value: 'page'},\r\n                    {title: 'Post', value: 'post'},\r\n                  ],\r\n                  layout: 'radio',\r\n                },\r\n              }),\r\n\r\n              defineField({\r\n                name: 'href',\r\n                title: 'URL',\r\n                type: 'url',\r\n                hidden: ({parent}) => parent?.linkType !== 'href' && parent?.linkType != null,\r\n                validation: (Rule) =>\r\n                  Rule.custom((value, context: any) => {\r\n                    if (context.parent?.linkType === 'href' && !value) {\r\n                      return 'URL is required when Link Type is URL'\r\n                    }\r\n                    return true\r\n                  }),\r\n              }),\r\n\r\n              defineField({\r\n                name: 'page',\r\n                title: 'Page',\r\n                type: 'reference',\r\n                to: [{type: 'page'}],\r\n                hidden: ({parent}) => parent?.linkType !== 'page',\r\n                validation: (Rule) =>\r\n                  Rule.custom((value, context: any) => {\r\n                    if (context.parent?.linkType === 'page' && !value) {\r\n                      return 'Page reference is required when Link Type is Page'\r\n                    }\r\n                    return true\r\n                  }),\r\n              }),\r\n\r\n              defineField({\r\n                name: 'post',\r\n                title: 'Post',\r\n                type: 'reference',\r\n                to: [{type: 'post'}],\r\n                hidden: ({parent}) => parent?.linkType !== 'post',\r\n                validation: (Rule) =>\r\n                  Rule.custom((value, context: any) => {\r\n                    if (context.parent?.linkType === 'post' && !value) {\r\n                      return 'Post reference is required when Link Type is Post'\r\n                    }\r\n                    return true\r\n                  }),\r\n              }),\r\n\r\n              defineField({\r\n                name: 'openInNewTab',\r\n                title: 'Open in new tab',\r\n                type: 'boolean',\r\n                initialValue: false,\r\n              }),\r\n            ],\r\n          },\r\n        ],\r\n      },\r\n    }),\r\n  ],\r\n})\r\n\r\n",
              "id": "node-blockcontent-fixes",
              "name": "blockContent.tsx",
              "nodeType": "file"
            },
            {
              "_key": "node-link-schema-fixes",
              "_type": "treeNode",
              "actionFile": true,
              "actions": [
                {
                  "logic": {
                    "behaviour": "addMarkerAboveTarget",
                    "content": " {title: '{{.PascalCasePageTypeSingular}}', value: '{{.LowerCasePageTypeSingular}}'},",
                    "mark": "PAGETYPE LINK OPTIONS",
                    "occurrence": "first",
                    "target": "],"
                  },
                  "mark": "",
                  "title": "Adding PageType as a LinkType option"
                },
                {
                  "logic": {
                    "behaviour": "addMarkerAboveTarget",
                    "content": "    defineField({\n      name: '{{.LowerCasePageTypeSingular}}',\n      title: '{{.PascalCasePageTypeSingular}}',\n      type: 'reference',\n      to: [{type: '{{.LowerCasePageTypeSingular}}'}],\n      hidden: ({parent}) => parent?.linkType !== '{{.LowerCasePageTypeSingular}}',\n      validation: (Rule) =>\n        Rule.custom((value, context: any) => {\n          if (context.parent?.linkType === '{{.LowerCasePageTypeSingular}}' && !value) {\n            return '{{.PascalCasePageTypeSingular}} reference is required when Link Type is {{.PascalCasePageTypeSingular}}'\n          }\n          return true\n        }),\n    }),",
                    "mark": "PAGETYPE LINK FIELD",
                    "occurrence": "last",
                    "target": "defineField({"
                  },
                  "mark": "",
                  "title": "Adding PageType as a Link Field"
                }
              ],
              "children": [],
              "code": "import {defineField, defineType} from 'sanity'\r\nimport {LinkIcon} from '@sanity/icons'\r\n\r\n/**\r\n * Link schema object. This link object lets the user first select the type of link and then\r\n * then enter the URL, page reference, or post reference - depending on the type selected.\r\n * Learn more: https://www.sanity.io/docs/object-type\r\n */\r\nexport const link = defineType({\r\n  name: 'link',\r\n  title: 'Link',\r\n  type: 'object',\r\n  icon: LinkIcon,\r\n  fields: [\r\n    defineField({\r\n      name: 'linkType',\r\n      title: 'Link Type',\r\n      type: 'string',\r\n      initialValue: 'url',\r\n      options: {\r\n        list: [\r\n          {title: 'URL', value: 'href'},\r\n          {title: 'Page', value: 'page'},\r\n          {title: 'Post', value: 'post'},\r\n        ],\r\n        layout: 'radio',\r\n      },\r\n    }),\r\n\r\n    // URL\r\n    defineField({\r\n      name: 'href',\r\n      title: 'URL',\r\n      type: 'url',\r\n      hidden: ({parent}) => parent?.linkType !== 'href',\r\n      validation: (Rule) =>\r\n        Rule.custom((value, context: any) => {\r\n          if (context.parent?.linkType === 'href' && !value) {\r\n            return 'URL is required when Link Type is URL'\r\n          }\r\n          return true\r\n        }),\r\n    }),\r\n\r\n    // Page\r\n    defineField({\r\n      name: 'page',\r\n      title: 'Page',\r\n      type: 'reference',\r\n      to: [{type: 'page'}],\r\n      hidden: ({parent}) => parent?.linkType !== 'page',\r\n      validation: (Rule) =>\r\n        Rule.custom((value, context: any) => {\r\n          if (context.parent?.linkType === 'page' && !value) {\r\n            return 'Page reference is required when Link Type is Page'\r\n          }\r\n          return true\r\n        }),\r\n    }),\r\n\r\n    // Post\r\n    defineField({\r\n      name: 'post',\r\n      title: 'Post',\r\n      type: 'reference',\r\n      to: [{type: 'post'}],\r\n      hidden: ({parent}) => parent?.linkType !== 'post',\r\n      validation: (Rule) =>\r\n        Rule.custom((value, context: any) => {\r\n          if (context.parent?.linkType === 'post' && !value) {\r\n            return 'Post reference is required when Link Type is Post'\r\n          }\r\n          return true\r\n        }),\r\n    }),\r\n\r\n\r\n    defineField({\r\n      name: 'openInNewTab',\r\n      title: 'Open in new tab',\r\n      type: 'boolean',\r\n      initialValue: false,\r\n    }),\r\n  ],\r\n})\r\n\r\n\r\n\r\n",
              "id": "node-link-schema-fixes",
              "name": "link.ts",
              "nodeType": "file"
            },
            {
              "_key": "1757940742288-hed1iz1uc",
              "_type": "treeNode",
              "actionFile": false,
              "actions": [
                {
                  "logic": {
                    "behaviour": null,
                    "content": null,
                    "mark": null,
                    "occurrence": null,
                    "target": null
                  },
                  "mark": "",
                  "title": null
                }
              ],
              "children": [],
              "code": "import { defineType } from \"sanity\";\r\n\r\nexport const pageBuilder = defineType({\r\n    name: 'pageBuilder',\r\n    title: 'Page builder',\r\n    type: 'array',\r\n    of: [{type: 'callToAction'}, {type: 'infoSection'}],\r\n    options: {\r\n        insertMenu: {\r\n          // Configure the \"Add Item\" menu to display a thumbnail preview of the content type. https://www.sanity.io/docs/array-type#efb1fe03459d\r\n          views: [\r\n            {\r\n              name: 'grid',\r\n              previewImageUrl: (schemaTypeName) =>\r\n                `/static/page-builder-thumbnails/${schemaTypeName}.webp`,\r\n            },\r\n          ],\r\n        },\r\n    }\r\n})\r\n        ",
              "id": "file-1757940742288",
              "name": "pageBuilder.tsx",
              "nodeType": "file"
            }
          ],
          "code": "",
          "id": "folder-1757939548968",
          "name": "objects",
          "nodeType": "folder"
        },
        {
          "_key": "20250903-studio-indexer-file",
          "_type": "treeNode",
          "actionFile": true,
          "actions": [
            {
              "logic": {
                "behaviour": "addMarkerBelowTarget",
                "content": "import { {{.LowerCasePageTypeSingular}} } from './documents/{{.KebabCasePageTypeSingular}}'",
                "mark": "NEXTGEN PAGETYPE IMPORTS",
                "occurrence": "first",
                "target": "import {post} from './documents/post'"
              },
              "mark": "",
              "title": "Importing PageType to SchemaTypos Index"
            },
            {
              "logic": {
                "behaviour": "addMarkerAboveTarget",
                "content": "{{.LowerCasePageTypeSingular}},",
                "mark": "NEXTGEN PAGETYPES",
                "occurrence": "last",
                "target": "// Objects"
              },
              "mark": "",
              "title": "Adding DocumentType to SchemaType array"
            },
            {
              "logic": {
                "behaviour": "addMarkerAboveTarget",
                "content": "pageBuilder,",
                "mark": null,
                "occurrence": "last",
                "target": "]"
              },
              "mark": "",
              "title": "Add Pagebuilder Object to SchemaTypes"
            },
            {
              "logic": {
                "behaviour": "addMarkerAboveTarget",
                "content": "import {pageBuilder} from './objects/pageBuilder'",
                "mark": "",
                "occurrence": "first",
                "target": "export const schemaTypes = ["
              },
              "mark": "",
              "title": "Import PageBuilder Object to schemaTypes index"
            }
          ],
          "children": [],
          "code": "import {person} from './documents/person'\r\nimport {page} from './documents/page'\r\nimport {post} from './documents/post'\r\nimport {callToAction} from './objects/callToAction'\r\nimport {infoSection} from './objects/infoSection'\r\nimport {settings} from './singletons/settings'\r\nimport {link} from './objects/link'\r\nimport {blockContent} from './objects/blockContent'\r\n\r\nexport const schemaTypes = [\r\n  // Singletons\r\n  settings,\r\n  // Documents\r\n  page,\r\n  post,\r\n  person,\r\n  // Objects\r\n  blockContent,\r\n  infoSection,\r\n  callToAction,\r\n  link,\r\n]\r\n",
          "id": "file-studio-indexer",
          "name": "index.ts",
          "nodeType": "file"
        }
      ],
      "path": "studio/src/schemaTypes"
    }
  ],
  "goals": [
    {
      "_key": "cb47035fdfc1",
      "_type": "goal",
      "description": "Creates folder for the route, with dedicated page.tsx for both (index) and [slug]. ",
      "fileHints": null,
      "howToTips": null,
      "title": "Create Files and Folders for the Page Type routing"
    },
    {
      "_key": "4b4596af24a9",
      "_type": "goal",
      "fileHints": null,
      "howToTips": null,
      "title": "Create List Component which output posts from new PageType"
    },
    {
      "_key": "6341bfd73a83",
      "_type": "goal",
      "fileHints": null,
      "howToTips": null,
      "title": "Create dedicated query file for the new PageType"
    },
    {
      "_key": "68671ad62b9507d53e0a3a9884e2e794",
      "description": "Makes the Page Builder UI available in Studio so the new PageType can actually use its blocks.",
      "fileHints": [
        "studio/src/schemaTypes/index.ts"
      ],
      "howToTips": [
        "Add the import near other object imports (use an addMarkerAboveTarget-style insertion; guard with requireAbsent).",
        "Include 'pageBuilder' in the schemaTypes array close to other objects (addMarkerAboveTarget; guard with requireAbsent)."
      ],
      "title": "Register PageBuilder object in Studio schema index"
    },
    {
      "_key": "9e1d52f10c59b5414f5f0024ba198fe8",
      "description": "Allows Studio to recognize and edit the new document type, which is foundational for all other steps.",
      "fileHints": [
        "studio/src/schemaTypes/index.ts"
      ],
      "howToTips": [
        "Place the import alongside other document imports (addMarkerBelowTarget fits well).",
        "Append the type in the documents section of schemaTypes (addMarkerAboveTarget)."
      ],
      "title": "Register PageType in Studio schema index"
    },
    {
      "_key": "422cf6758e0320f0839f79eee12b2a95",
      "description": "Turns common fragments into exports so queries across files reuse them cleanly without re-definitions.",
      "fileHints": [
        "frontend/sanity/lib/queries.ts"
      ],
      "howToTips": [
        "Find the primary 'const' definitions and convert to 'export const' (replaceIfMissing).",
        "Change only the first canonical definition and guard with requireAbsent to avoid double-exports."
      ],
      "title": "Export shared GROQ fragments (postFields, linkFields, linkReference)"
    },
    {
      "_key": "c03f1f5d8050a64002eec3ea5e031172",
      "description": "Centralizes Page Builder selections into a shared fragment and references it in queries to prevent drift.",
      "fileHints": [
        "frontend/sanity/lib/queries.ts"
      ],
      "howToTips": [
        "Introduce an exported fragment near other exports and rebind the query to use it (replaceBetween).",
        "Guard with requireAbsent so you don’t redefine an existing export."
      ],
      "title": "Export pageBuilderFields and reuse in getPageQuery"
    },
    {
      "_key": "1dc155c97974905e795375efda018b5a",
      "description": "Lets rich-text links resolve this new type by exposing its slug in the mapping used across queries.",
      "fileHints": [
        "frontend/sanity/lib/queries.ts"
      ],
      "howToTips": [
        "Locate the object that maps linkable types to slugs.",
        "Insert a new key for the PageType using an addMarkerBelowTarget-style placement near the start of the mapping."
      ],
      "title": "Add PageType to linkReference mapping (rich-text links)"
    },
    {
      "_key": "1632c4adbd71ffd21ff39330e8b72ec9",
      "description": "Teaches the client resolver how to build canonical URLs for the new type so internal links work end-to-end.",
      "fileHints": [
        "frontend/sanity/lib/utils.ts"
      ],
      "howToTips": [
        "Find the switch/branch that handles link types.",
        "Add a case for the PageType before the default (addMarkerAboveTarget)."
      ],
      "title": "Add PageType route case to linkResolver"
    },
    {
      "_key": "73b826971700d336f33b20f88c1a1410",
      "description": "Ensures the new type is included in the sitemap so search engines can discover it.",
      "fileHints": [
        "frontend/sanity/lib/queries.ts"
      ],
      "howToTips": [
        "Identify the allowed _type list in the sitemap filter.",
        "Insert the new _type right before the slug guard (insertBeforeInline; fallbackOnly to keep it idempotent)."
      ],
      "title": "Include PageType in sitemapData query"
    },
    {
      "_key": "a89589cca5044b2737be51c5394e55e3",
      "description": "Surfaces the new type in the main navigation so users can find its listing page.",
      "fileHints": [
        "frontend/app/components/Header.tsx"
      ],
      "howToTips": [
        "Locate the first nav list item and place the archive link alongside similar internal links.",
        "Use addMarkerBelowTarget anchored to the earliest stable <li>.",
        "Keep it idempotent (occurrence 'first', fallbackOnly) and skip if an equivalent link exists."
      ],
      "title": "Add archive link to Header nav (idempotent)"
    },
    {
      "_key": "9429adf6c3cb3cb0223106423389ddbd",
      "description": "Lets editors choose the new type directly in rich-text link options for portable text blocks.",
      "fileHints": [
        "studio/src/schemaTypes/objects/blockContent.tsx"
      ],
      "howToTips": [
        "Find the editor-facing list of link type options.",
        "Add an option entry using addMarkerAboveTarget near the end of the list."
      ],
      "title": "Expose PageType as a link type in blockContent"
    },
    {
      "_key": "b3d0268d929d4d396e20a649f3b91095",
      "description": "Shows a reference picker only when the new link type is selected, keeping forms tidy and valid.",
      "fileHints": [
        "studio/src/schemaTypes/objects/blockContent.tsx"
      ],
      "howToTips": [
        "Locate the group of link-related reference fields.",
        "Insert a conditional reference field using addMarkerAboveTarget at the final field cluster."
      ],
      "title": "Add conditional PageType reference field in blockContent"
    },
    {
      "_key": "e26034f3c344c79b3b84abf6a47879a8",
      "description": "Adds the new type to the generic link object so any schema reusing it can target this type as well.",
      "fileHints": [
        "studio/src/schemaTypes/objects/link.ts"
      ],
      "howToTips": [
        "Find the radio/option list for link types.",
        "Add a new entry with addMarkerAboveTarget following the existing structure and ordering."
      ],
      "title": "Expose PageType as a link type in link object"
    },
    {
      "_key": "869ba08bb79f91d33339a90d6352d98b",
      "description": "Adds a reference field tied to the new option and required only when that option is chosen.",
      "fileHints": [
        "studio/src/schemaTypes/objects/link.ts"
      ],
      "howToTips": [
        "Identify where other type-specific reference fields are declared.",
        "Place the new conditional field alongside them (addMarkerAboveTarget)."
      ],
      "title": "Add conditional PageType reference field in link object"
    }
  ],
  "ignoredPatterns": [],
  "slug": {
    "_type": "slug",
    "current": "add-page-type-with-pagebuilder"
  },
  "slugCurrent": "add-page-type-with-pagebuilder",
  "title": "Add Page Type with Pagebuilder",
  "variables": [
    {
      "_type": "variableDefinition",
      "description": "This is the name of the \"_type\" we use in Sanity. This dictates a lot of the naming conventions elsewhere.",
      "examples": [
        "author",
        "event",
        "product",
        "service"
      ],
      "name": "PageTypeSingular",
      "priority": 1,
      "title": "Name your page type"
    },
    {
      "_type": "variableDefinition",
      "description": "This helps cases where we need to pluralize. No worries if it's the same as the singular.",
      "examples": [
        "authors",
        "events",
        "products",
        "services"
      ],
      "name": "PageTypePlural",
      "priority": 2,
      "title": "Pluralize your page type"
    }
  ]
}