Add Page Type with Block Editor

/add-page-type-with-block-editorCreated: 15 Sept 2025, 10:37Updated: 16 Sept 2025, 12:13
Variables
2
Goals
14
Path Groups
2
Nodes
21
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
└─ πŸ“„ 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 PageType in Studio schema index
    studio/src/schemaTypes/index.ts

    Make the new document type available in Studio so editors can create and edit it right away.

    How-To Tips
    • Add the import near other document imports (addMarkerBelowTarget fits well).
    • Add the type to the documents section of schemaTypes before the objects divider (addMarkerAboveTarget).
  5. Export shared GROQ fragments (postFields, linkFields, linkReference)
    frontend/sanity/lib/queries.ts

    Expose common query fragments once so other queries can reuse them consistently and avoid re-definitions.

    How-To Tips
    • Find the first canonical 'const' for each fragment and switch it to 'export const' (replaceIfMissing).
    • Guard with requireAbsent so you don’t double-export.
  6. Export pageBuilderFields and reuse in getPageQuery
    frontend/sanity/lib/queries.ts

    Centralize the Page Builder selections in a shared export and point getPageQuery to it to prevent drift.

    How-To Tips
    • Introduce an exported fragment near related exports and update the query to reference it (replaceBetween).
    • Use requireAbsent to avoid redefining if it’s already exported.
  7. Add PageType to linkReference mapping (rich-text links)
    frontend/sanity/lib/queries.ts

    Let rich-text links resolve your new type by exposing its slug in the link mapping used by queries.

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

    Teach the client resolver how to build URLs for the new type so internal links are consistent.

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

    Add the new type to the sitemap filter so search engines can discover these pages.

    How-To Tips
    • Identify the list of allowed _type checks in the sitemap query.
    • Insert the new _type immediately before the slug guard (insertBeforeInline) and set fallbackOnly to keep it idempotent.
  10. Add archive link to Header nav (idempotent)
    frontend/app/components/Header.tsx

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

    How-To Tips
    • Find the first stable nav list item (e.g., the first <li>) and place the archive link alongside similar links.
    • Use addMarkerBelowTarget with occurrence: 'first' and fallbackOnly: true.
  11. Expose PageType as a link type in blockContent
    studio/src/schemaTypes/objects/blockContent.tsx

    Let editors choose the new type directly in portable text link options.

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

    Show a reference picker only when the new link type is selected, keeping forms tidy and validation accurate.

    How-To Tips
    • Locate the cluster of link-specific reference fields.
    • Insert a conditional reference field at the end of that cluster (addMarkerAboveTarget).
  13. Expose PageType as a link type in link object
    studio/src/schemaTypes/objects/link.ts

    Add the new type to the generic link object so any schema using it can target this type too.

    How-To Tips
    • Find the radio/option list for link types.
    • Add a new entry in the same style and order (addMarkerAboveTarget).
  14. Add conditional PageType reference field in link object
    studio/src/schemaTypes/objects/link.ts

    Add a reference field that appears only when the new option is chosen and is required in that state.

    How-To Tips
    • Identify where other type-specific reference fields live.
    • 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 {notFound} from 'next/navigation'
          import {type PortableTextBlock} from 'next-sanity'
          import {Suspense} from 'react'
          
          import Avatar from '@/app/components/Avatar'
          import CoverImage from '@/app/components/CoverImage'
          import {MorePosts} from '@/app/components/Posts'
          import PortableText from '@/app/components/PortableText'
          import {sanityFetch} from '@/sanity/lib/live'
          import { {{.LowerCasePageTypeSingular}}Slugs, {{.LowerCasePageTypeSingular}}BySlugQuery } from '@/sanity/lib/pagetype-queries/{{.KebabCasePageTypeSingular}}.queries'
          import {resolveOpenGraphImage} from '@/sanity/lib/utils'
          
          export type Props = { params: Promise<{slug: string}> }
          
          export async function generateStaticParams() {
            const {data} = await sanityFetch({
              query: {{.LowerCasePageTypeSingular}}Slugs,
              perspective: 'published',
              stega: false,
            })
            return data
          }
          
          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,
            })
          
            const previousImages = (await parent).openGraph?.images || []
            const ogImage = resolveOpenGraphImage(doc?.coverImage)
          
            return {
              authors:
                doc?.author?.firstName && doc?.author?.lastName
                  ? [{name: `${doc.author.firstName} ${doc.author.lastName}`}] 
                  : [],
              title: doc?.title,
              description: doc?.excerpt,
              openGraph: {
                images: ogImage ? [ogImage, ...previousImages] : previousImages,
              },
            } 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 notFound()
            }
          
            return (
              <>
                <div className="">
                  <div className="container my-12 lg:my-24 grid gap-12">
                    <div>
                      <div className="pb-6 grid gap-6 mb-6 border-b border-gray-100">
                        <div className="max-w-3xl flex flex-col gap-6">
                          <h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl lg:text-7xl">
                            {doc.title}
                          </h2>
                        </div>
                        <div className="max-w-3xl flex gap-4 items-center">
                          {doc.author && doc.author.firstName && doc.author.lastName && (
                            <Avatar person={doc.author} date={doc.date} />
                          )}
                        </div>
                      </div>
                      <article className="gap-6 grid max-w-4xl">
                        <div className="">
                          {doc?.coverImage && <CoverImage image={doc.coverImage} priority />}
                        </div>
                        {doc?.content?.length ? (
                          <PortableText className="max-w-2xl" value={doc.content as PortableTextBlock[]} />
                        ) : null}
                      </article>
                    </div>
                  </div>
                </div>
                <div className="border-t border-gray-100 bg-gray-50">
                  <div className="container py-12 lg:py-24 grid gap-12">
                    <aside>
                      <Suspense>{await MorePosts({skip: doc._id, limit: 2})}</Suspense>
                    </aside>
                  </div>
                </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 { linkReference, postFields } from "../queries";
          
          
          export const all{{.PascalCasePageTypePlural}}Query = defineQuery(`
            *[_type == "{{.LowerCasePageTypeSingular}}" && defined(slug.current)] | order(date desc, _updatedAt desc) {
              ${postFields}
            }
          `);
          
          export const {{.LowerCasePageTypeSingular}}BySlugQuery = defineQuery(`
            *[_type == "{{.LowerCasePageTypeSingular}}" && slug.current == $slug] [0] {
              content[]{
              ...,
              titleDefs[]{
                ...,
                ${linkReference}
              }
            },
              ${postFields}
            }
          `);
          
          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 {DocumentTextIcon} from '@sanity/icons'
      import {format, parseISO} from 'date-fns'
      import {defineField, defineType} from 'sanity'
      
      export const {{.LowerCasePageTypeSingular}} = defineType({
        name: '{{.LowerCasePageTypeSingular}}',
        title: '{{.PascalCasePageTypeSingular}}',
        icon: DocumentTextIcon,
        type: 'document',
        fields: [
          defineField({
            name: 'title',
            title: 'Title',
            type: 'string',
            validation: (rule) => rule.required(),
          }),
          defineField({
            name: 'slug',
            title: 'Slug',
            type: 'slug',
            description: 'A slug is required for the page to show up in the preview',
            options: {
              source: 'title',
              maxLength: 96,
              isUnique: (value, context) => context.defaultIsUnique(value, context),
            },
            validation: (rule) => rule.required(),
          }),
          defineField({
            name: 'content',
            title: 'Content',
            type: 'blockContent',
          }),
          defineField({
            name: 'excerpt',
            title: 'Excerpt',
            type: 'text',
          }),
          defineField({
            name: 'coverImage',
            title: 'Cover Image',
            type: 'image',
            options: {
              hotspot: true,
              aiAssist: {
                imageDescriptionField: 'alt',
              },
            },
            fields: [
              {
                name: 'alt',
                type: 'string',
                title: 'Alternative text',
                description: 'Important for SEO and accessibility.',
                validation: (rule) => {
                  // Custom validation to ensure alt text is provided if the image is present. https://www.sanity.io/docs/validation
                  return rule.custom((alt, context) => {
                    if ((context.document?.coverImage as any)?.asset?._ref && !alt) {
                      return 'Required'
                    }
                    return true
                  })
                },
              },
            ],
            validation: (rule) => rule.required(),
          }),
          defineField({
            name: 'date',
            title: 'Date',
            type: 'datetime',
            initialValue: () => new Date().toISOString(),
          }),
          defineField({
            name: 'author',
            title: 'Author',
            type: 'reference',
            to: [{type: 'person'}],
          }),
        ],
        // List preview configuration. https://www.sanity.io/docs/previews-list-views
        preview: {
          select: {
            title: 'title',
            authorFirstName: 'author.firstName',
            authorLastName: 'author.lastName',
            date: 'date',
            media: 'coverImage',
          },
          prepare({title, media, authorFirstName, authorLastName, date}) {
            const subtitles = [
              authorFirstName && authorLastName && `by ${authorFirstName} ${authorLastName}`,
              date && `on ${format(parseISO(date), 'LLL d, yyyy')}`,
            ].filter(Boolean)
      
            return {title, media, subtitle: subtitles.join(' ')}
          },
        },
      })
      
  • 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,
          }),
        ],
      })
      
      
      
      
  • 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}},
    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 an array of all the schema types.  This is used in the Sanity Studio configuration. https://www.sanity.io/docs/schema-types
    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-15T10:37:21Z",
  "_id": "685d6a2d-5a4d-4bdd-b385-3a611f2fc6b6",
  "_rev": "2lI7L5yvGPDIkpqLKgAkoO",
  "_system": {
    "base": {
      "id": "685d6a2d-5a4d-4bdd-b385-3a611f2fc6b6",
      "rev": "G2Y6JXdwXQRnotb29fdabp"
    }
  },
  "_type": "command-slug",
  "_updatedAt": "2025-09-16T12:13:21Z",
  "description": "Instantly add a PageType with Block Editor 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-1757021003924-sa8u1fy",
      "nodes": [
        {
          "_key": "1757939837317-92snjhy6m",
          "_type": "treeNode",
          "actionFile": false,
          "actions": [],
          "children": [
            {
              "_key": "20250903-front-index-folder",
              "_type": "treeNode",
              "actionFile": false,
              "actions": [],
              "children": [
                {
                  "_key": "1757178527060-uga95spuh",
                  "_type": "treeNode",
                  "actionFile": false,
                  "actions": [],
                  "children": [
                    {
                      "_key": "20250903-front-index-file",
                      "_type": "treeNode",
                      "actionFile": false,
                      "actions": [
                        {
                          "_key": "kWkIvI7T6XFbABGiWe5M2f",
                          "logic": {},
                          "mark": ""
                        }
                      ],
                      "children": [],
                      "code": "import Link from \"next/link\";\nimport type { Metadata } from \"next\";\nimport { client } from \"@/sanity/lib/client\";\nimport { all{{.PascalCasePageTypePlural}}Query } from \"@/sanity/lib/pagetype-queries/{{.KebabCasePageTypeSingular}}.queries\";\nimport { All{{.PascalCasePageTypePlural}} } from \"@/app/components/{{.PascalCasePageTypePlural}}\";\n\nexport const metadata: Metadata = {\n  title: \"{{.PascalCasePageTypePlural}}\",\n  description: \"All {{.LowerCasePageTypePlural}}\"\n};\n\nexport default async function {{.PascalCasePageTypeSingular}}IndexPage() {\n  const items = await client.fetch(all{{.PascalCasePageTypePlural}}Query);\n\n  if (!items?.length) {\n    return (\n      <main className=\"container mx-auto p-6\">\n        <h1 className=\"text-2xl font-semibold\">{{.PascalCasePageTypePlural}}</h1>\n        <p className=\"opacity-70 mt-2\">No {{.LowerCasePageTypePlural}} yet.</p>\n      </main>\n    );\n  }\n\n  return (\n    <main className=\"container mx-auto p-6\">\n    <All{{.PascalCasePageTypePlural}} />\n    </main>\n  );\n}\n",
                      "id": "file-frontend-index",
                      "name": "page.tsx",
                      "nodeType": "file"
                    }
                  ],
                  "code": "",
                  "id": "folder-1757178527060",
                  "name": "(index)",
                  "nodeType": "folder"
                },
                {
                  "_key": "1757178518306-5reh6n8zd",
                  "_type": "treeNode",
                  "actionFile": false,
                  "actions": [],
                  "children": [
                    {
                      "_key": "20250903-front-slug-page",
                      "_type": "treeNode",
                      "actionFile": false,
                      "actions": [
                        {
                          "_key": "kWkIvI7T6XFbABGiWe5M7C",
                          "logic": {},
                          "mark": ""
                        }
                      ],
                      "children": [],
                      "code": "import type {Metadata, ResolvingMetadata} from 'next'\nimport {notFound} from 'next/navigation'\nimport {type PortableTextBlock} from 'next-sanity'\nimport {Suspense} from 'react'\n\nimport Avatar from '@/app/components/Avatar'\nimport CoverImage from '@/app/components/CoverImage'\nimport {MorePosts} from '@/app/components/Posts'\nimport PortableText from '@/app/components/PortableText'\nimport {sanityFetch} from '@/sanity/lib/live'\nimport { {{.LowerCasePageTypeSingular}}Slugs, {{.LowerCasePageTypeSingular}}BySlugQuery } from '@/sanity/lib/pagetype-queries/{{.KebabCasePageTypeSingular}}.queries'\nimport {resolveOpenGraphImage} from '@/sanity/lib/utils'\n\nexport type Props = { params: Promise<{slug: string}> }\n\nexport async function generateStaticParams() {\n  const {data} = await sanityFetch({\n    query: {{.LowerCasePageTypeSingular}}Slugs,\n    perspective: 'published',\n    stega: false,\n  })\n  return data\n}\n\nexport async function generateMetadata(props: Props, parent: ResolvingMetadata): Promise<Metadata> {\n  const params = await props.params\n  const {data: doc} = await sanityFetch({\n    query: {{.LowerCasePageTypeSingular}}BySlugQuery,\n    params,\n    stega: false,\n  })\n\n  const previousImages = (await parent).openGraph?.images || []\n  const ogImage = resolveOpenGraphImage(doc?.coverImage)\n\n  return {\n    authors:\n      doc?.author?.firstName && doc?.author?.lastName\n        ? [{name: `${doc.author.firstName} ${doc.author.lastName}`}] \n        : [],\n    title: doc?.title,\n    description: doc?.excerpt,\n    openGraph: {\n      images: ogImage ? [ogImage, ...previousImages] : previousImages,\n    },\n  } satisfies Metadata\n}\n\nexport default async function {{.PascalCasePageTypeSingular}}Page(props: Props) {\n  const params = await props.params\n  const [{data: doc}] = await Promise.all([\n    sanityFetch({ query: {{.LowerCasePageTypeSingular}}BySlugQuery, params })\n  ])\n\n  if (!doc?._id) {\n    return notFound()\n  }\n\n  return (\n    <>\n      <div className=\"\">\n        <div className=\"container my-12 lg:my-24 grid gap-12\">\n          <div>\n            <div className=\"pb-6 grid gap-6 mb-6 border-b border-gray-100\">\n              <div className=\"max-w-3xl flex flex-col gap-6\">\n                <h2 className=\"text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl lg:text-7xl\">\n                  {doc.title}\n                </h2>\n              </div>\n              <div className=\"max-w-3xl flex gap-4 items-center\">\n                {doc.author && doc.author.firstName && doc.author.lastName && (\n                  <Avatar person={doc.author} date={doc.date} />\n                )}\n              </div>\n            </div>\n            <article className=\"gap-6 grid max-w-4xl\">\n              <div className=\"\">\n                {doc?.coverImage && <CoverImage image={doc.coverImage} priority />}\n              </div>\n              {doc?.content?.length ? (\n                <PortableText className=\"max-w-2xl\" value={doc.content as PortableTextBlock[]} />\n              ) : null}\n            </article>\n          </div>\n        </div>\n      </div>\n      <div className=\"border-t border-gray-100 bg-gray-50\">\n        <div className=\"container py-12 lg:py-24 grid gap-12\">\n          <aside>\n            <Suspense>{await MorePosts({skip: doc._id, limit: 2})}</Suspense>\n          </aside>\n        </div>\n      </div>\n    </>\n  )\n}\n",
                      "id": "file-frontend-slug-page",
                      "name": "page.tsx",
                      "nodeType": "file"
                    }
                  ],
                  "code": "",
                  "id": "folder-1757178518306",
                  "name": "[slug]",
                  "nodeType": "folder"
                }
              ],
              "code": "",
              "id": "folder-frontend-index",
              "name": "{{.KebabCasePageTypePlural}}",
              "nodeType": "folder"
            },
            {
              "_key": "1757588704461-4k7snx2n3",
              "_type": "treeNode",
              "actionFile": false,
              "actions": [],
              "children": [
                {
                  "_key": "1757176137674-q3yigc355",
                  "_type": "treeNode",
                  "actionFile": false,
                  "actions": [
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5MBj",
                      "logic": {},
                      "mark": ""
                    }
                  ],
                  "children": [],
                  "code": "import Link from 'next/link'\n\nimport { sanityFetch } from '@/sanity/lib/live'\nimport { all{{.PascalCasePageTypePlural}}Query } from '@/sanity/lib/pagetype-queries/{{.KebabCasePageTypeSingular}}.queries'\nimport DateComponent from '@/app/components/Date'\nimport OnBoarding from '@/app/components/Onboarding'\nimport Avatar from '@/app/components/Avatar'\nimport { createDataAttribute } from 'next-sanity'\n\ntype {{.PascalCasePageTypeSingular}}ListItem = {\n  _id: string\n  title?: string\n  name?: string\n  slug: string\n  excerpt?: string | null\n  subheading?: string | null\n  coverImage?: unknown\n  date?: string\n  author?:\n    | {\n        firstName?: string\n        lastName?: string\n        picture?: unknown\n      }\n    | null\n}\n\nconst {{.PascalCasePageTypeSingular}}Card = ({ item }: { item: {{.PascalCasePageTypeSingular}}ListItem }) => {\n  const { _id, slug, date, author } = item\n  const title = item.title ?? item.name ?? 'Untitled'\n  const excerpt = (item.excerpt ?? item.subheading) ?? null\n\n  const attr = createDataAttribute({\n    id: _id,\n    type: '{{.LowerCasePageTypeSingular}}',\n    path: (item.title ? 'title' : 'name') as 'title' | 'name',\n  })\n\n  return (\n    <article\n      data-sanity={attr()}\n      key={_id}\n      className=\"border border-gray-200 rounded-sm p-6 bg-gray-50 flex flex-col justify-between transition-colors hover:bg-white relative\"\n    >\n      <Link className=\"hover:text-brand underline transition-colors\" href={`/{{.LowerCasePageTypePlural}}/${slug}`}>\n        <span className=\"absolute inset-0 z-10\" />\n      </Link>\n\n      <div>\n        <h3 className=\"text-2xl font-bold mb-4 leading-tight\">{title}</h3>\n\n        {excerpt && (\n          <p className=\"line-clamp-3 text-sm leading-6 text-gray-600 max-w-[70ch]\">{excerpt}</p>\n        )}\n      </div>\n\n      <div className=\"flex items-center justify-between mt-6 pt-4 border-t border-gray-100\">\n        {author?.firstName && author?.lastName && (\n          <div className=\"flex items-center\">\n            <Avatar person={author as any} small={true} />\n          </div>\n        )}\n        {date && (\n          <time className=\"text-gray-500 text-xs font-mono\" dateTime={date}>\n            <DateComponent dateString={date} />\n          </time>\n        )}\n      </div>\n    </article>\n  )\n}\n\nconst {{.PascalCasePageTypePlural}} = ({\n  children,\n  heading,\n  subHeading,\n}: {\n  children: React.ReactNode\n  heading?: string\n  subHeading?: string\n}) => (\n  <div>\n    {heading && (\n      <h2 className=\"text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl lg:text-5xl\">\n        {heading}\n      </h2>\n    )}\n    {subHeading && <p className=\"mt-2 text-lg leading-8 text-gray-600\">{subHeading}</p>}\n\n    <div className=\"pt-6 space-y-6\">{children}</div>\n  </div>\n)\n\nexport const All{{.PascalCasePageTypePlural}} = async () => {\n  const { data } = await sanityFetch({ query: all{{.PascalCasePageTypePlural}}Query })\n\n  if (!data || data.length === 0) {\n    return <OnBoarding />\n  }\n\n  const list = data as unknown as {{.PascalCasePageTypeSingular}}ListItem[]\n\n  return (\n    <{{.PascalCasePageTypePlural}}\n      heading=\"{{.PascalCasePageTypePlural}}\"\n      subHeading=\"{{.PascalCasePageTypePlural}} populated from your Sanity Studio.\"\n    >\n      {list.map((item) => (\n        <{{.PascalCasePageTypeSingular}}Card key={item._id} item={item} />\n      ))}\n    </{{.PascalCasePageTypePlural}}>\n  )\n}\n",
                  "id": "file-1757176137674",
                  "name": "{{.PascalCasePageTypePlural}}.tsx",
                  "nodeType": "file"
                },
                {
                  "_key": "1756989436755-t8zcsbyvk",
                  "_type": "treeNode",
                  "actionFile": true,
                  "actions": [
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5MGG",
                      "logic": {
                        "behaviour": "addMarkerBelowTarget",
                        "content": "<Link href=\"/{{.LowerCasePageTypePlural}}\" className=\"mr-6 hover:underline\">{{.PascalCasePageTypePlural}}</Link>",
                        "fallbackOnly": true,
                        "occurrence": "first",
                        "target": "<li>"
                      },
                      "title": "PAGETYPE ARCHIVE LINK"
                    }
                  ],
                  "children": [],
                  "code": "import Link from 'next/link'\nimport {settingsQuery} from '@/sanity/lib/queries'\nimport {sanityFetch} from '@/sanity/lib/live'\n\nexport default async function Header() {\n  const {data: settings} = await sanityFetch({\n    query: settingsQuery,\n  })\n\n  return (\n    <header className=\"fixed z-50 h-24 inset-0 bg-white/80 flex items-center backdrop-blur-lg\">\n      <div className=\"container py-6 px-2 sm:px-6\">\n        <div className=\"flex items-center justify-between gap-5\">\n          <Link className=\"flex items-center gap-2\" href=\"/\">\n            <span className=\"text-lg sm:text-2xl pl-2 font-semibold\">\n              {settings?.title || 'Sanity + Next.js'}\n            </span>\n          </Link>\n\n          <nav>\n            <ul\n              role=\"list\"\n              className=\"flex items-center gap-4 md:gap-6 leading-5 text-xs sm:text-base tracking-tight font-mono\"\n            >\n              <li>\n                <Link href=\"/about\" className=\"hover:underline\">\n                  About\n                </Link>\n              </li>\n\n              <li className=\"sm:before:w-[1px] sm:before:bg-gray-200 before:block flex sm:gap-4 md:gap-6\">\n                <Link\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\"\n                  href=\"https://github.com/sanity-io/sanity-template-nextjs-clean\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                >\n                  <span className=\"whitespace-nowrap\">View on GitHub</span>\n                  <svg\n                    xmlns=\"http://www.w3.org/2000/svg\"\n                    viewBox=\"0 0 24 24\"\n                    fill=\"currentColor\"\n                    className=\"hidden sm:block h-4 sm:h-6\"\n                  >\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>\n                  </svg>\n                </Link>\n              </li>\n            </ul>\n          </nav>\n        </div>\n      </div>\n    </header>\n  )\n}\n",
                  "id": "file-1756989436755",
                  "name": "Header.tsx",
                  "nodeType": "file"
                }
              ],
              "code": "",
              "id": "folder-1757588704461",
              "name": "components",
              "nodeType": "folder"
            }
          ],
          "code": "",
          "id": "folder-1757939837317",
          "name": "app",
          "nodeType": "folder"
        },
        {
          "_key": "1757588738551-ngu8jne6q",
          "_type": "treeNode",
          "actionFile": false,
          "actions": [],
          "children": [
            {
              "_key": "1757588761958-9lzogtrzx",
              "_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": "kWkIvI7T6XFbABGiWe5MKn",
                          "logic": {},
                          "mark": ""
                        }
                      ],
                      "children": [],
                      "code": "import { defineQuery } from \"next-sanity\";\nimport { linkReference, postFields } from \"../queries\";\n\n\nexport const all{{.PascalCasePageTypePlural}}Query = defineQuery(`\n  *[_type == \"{{.LowerCasePageTypeSingular}}\" && defined(slug.current)] | order(date desc, _updatedAt desc) {\n    ${postFields}\n  }\n`);\n\nexport const {{.LowerCasePageTypeSingular}}BySlugQuery = defineQuery(`\n  *[_type == \"{{.LowerCasePageTypeSingular}}\" && slug.current == $slug] [0] {\n    content[]{\n    ...,\n    titleDefs[]{\n      ...,\n      ${linkReference}\n    }\n  },\n    ${postFields}\n  }\n`);\n\nexport const {{.LowerCasePageTypeSingular}}Slugs = defineQuery(`\n  *[_type == \"{{.LowerCasePageTypeSingular}}\" && defined(slug.current)]\n  {\"slug\": slug.current}\n`);\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": "kWkIvI7T6XFbABGiWe5MPK",
                      "logic": {
                        "behaviour": "replaceIfMissing",
                        "occurrence": "first",
                        "replacement": "export const linkFields = /* groq */ `",
                        "requireAbsent": "export const linkFields = /* groq */ `",
                        "target": "const linkFields = /* groq */ `"
                      },
                      "title": "set linkFields to Export"
                    },
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5MTr",
                      "logic": {
                        "behaviour": "replaceIfMissing",
                        "occurrence": "first",
                        "replacement": "export const postFields = /* groq */ `",
                        "requireAbsent": "export const postFields = /* groq */ `",
                        "target": "const postFields = /* groq */ `"
                      },
                      "title": "Set postFields to Export"
                    },
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5MYO",
                      "logic": {
                        "behaviour": "replaceIfMissing",
                        "occurrence": "first",
                        "replacement": "export const linkReference = /* groq */ `",
                        "requireAbsent": "export const linkReference = /* groq */ `",
                        "target": "const linkReference = /* groq */ `"
                      },
                      "title": "Set linkReference to Export"
                    },
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5Mcv",
                      "logic": {
                        "behaviour": "addMarkerBelowTarget",
                        "content": "\"{{.LowerCasePageTypeSingular}}\": {{.LowerCasePageTypeSingular}}->slug.current,",
                        "occurrence": "first",
                        "target": "_type == \"link\" => {"
                      },
                      "title": "connect up PageType as linkReference"
                    },
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5MhS",
                      "logic": {
                        "behaviour": "insertBeforeInline",
                        "content": " || _type == \"{{.LowerCasePageTypeSingular}}\"",
                        "fallbackOnly": true,
                        "occurrence": "first",
                        "target": "&& defined(slug.current)] | order(_type asc) {"
                      },
                      "title": "adding PageType to Sitemap"
                    },
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5Mlz",
                      "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(`"
                      },
                      "title": "Exportable PageBuilder Fields"
                    }
                  ],
                  "children": [],
                  "code": "import {defineQuery} from 'next-sanity'\n\nexport const settingsQuery = defineQuery(`*[_type == \"settings\"][0]`)\n\nexport const postFields = /* groq */ `\n  _id,\n  \"status\": select(_originalId in path(\"drafts.**\") => \"draft\", \"published\"),\n  \"title\": coalesce(title, \"Untitled\"),\n  \"slug\": slug.current,\n  excerpt,\n  coverImage,\n  \"date\": coalesce(date, _updatedAt),\n  \"author\": author->{firstName, lastName, picture},\n`\n\nexport const linkReference = /* groq */ `\n  _type == \"link\" => {\n    \"page\": page->slug.current,\n    \"post\": post->slug.current,\n  }\n`\n\nexport const linkFields = /* groq */ `\n  link {\n      ...,\n      ${linkReference}\n      }\n`\n\nexport 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`)\n\n\n\nexport const sitemapData = defineQuery(`\n  *[_type == \"page\" || _type == \"post\" && defined(slug.current)] | order(_type asc) {\n    \"slug\": slug.current,\n    _type,\n    _updatedAt,\n  }\n`)\n\nexport const allPostsQuery = defineQuery(`\n  *[_type == \"post\" && defined(slug.current)] | order(date desc, _updatedAt desc) {\n    ${postFields}\n  }\n`)\n\nexport const morePostsQuery = defineQuery(`\n  *[_type == \"post\" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] {\n    ${postFields}\n  }\n`)\n\nexport const postQuery = defineQuery(`\n  *[_type == \"post\" && slug.current == $slug] [0] {\n    content[]{\n    ...,\n    titleDefs[]{\n      ...,\n      ${linkReference}\n    }\n  },\n    ${postFields}\n  }\n`)\n\nexport const postPagesSlugs = defineQuery(`\n  *[_type == \"post\" && defined(slug.current)]\n  {\"slug\": slug.current}\n`)\n\nexport const pagesSlugs = defineQuery(`\n  *[_type == \"page\" && defined(slug.current)]\n  {\"slug\": slug.current}\n`)\n",
                  "id": "file-1756919951450",
                  "name": "queries.ts",
                  "nodeType": "file"
                },
                {
                  "_key": "node-utils-linktype-case",
                  "_type": "treeNode",
                  "actionFile": true,
                  "actions": [
                    {
                      "_key": "kWkIvI7T6XFbABGiWe5MqW",
                      "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:"
                      },
                      "title": "Setting up routing for PageType"
                    }
                  ],
                  "children": [],
                  "code": "import createImageUrlBuilder from '@sanity/image-url'\nimport {Link} from '@/sanity.types'\nimport {dataset, projectId, studioUrl} from '@/sanity/lib/api'\nimport {createDataAttribute, CreateDataAttributeProps} from 'next-sanity'\nimport {getImageDimensions} from '@sanity/asset-utils'\n\nconst imageBuilder = createImageUrlBuilder({\n  projectId: projectId || '',\n  dataset: dataset || '',\n})\n\nexport const urlForImage = (source: any) => {\n  // Ensure that source image contains a valid reference\n  if (!source?.asset?._ref) {\n    return undefined\n  }\n\n  const imageRef = source?.asset?._ref\n  const crop = source.crop\n\n  // get the image's og dimensions\n  const {width, height} = getImageDimensions(imageRef)\n\n  if (Boolean(crop)) {\n    // compute the cropped image's area\n    const croppedWidth = Math.floor(width * (1 - (crop.right + crop.left)))\n\n    const croppedHeight = Math.floor(height * (1 - (crop.top + crop.bottom)))\n\n    // compute the cropped image's position\n    const left = Math.floor(width * crop.left)\n    const top = Math.floor(height * crop.top)\n\n    // gather into a url\n    return imageBuilder?.image(source).rect(left, top, croppedWidth, croppedHeight).auto('format')\n  }\n\n  return imageBuilder?.image(source).auto('format')\n}\n\nexport function resolveOpenGraphImage(image: any, width = 1200, height = 627) {\n  if (!image) return\n  const url = urlForImage(image)?.width(1200).height(627).fit('crop').url()\n  if (!url) return\n  return {url, alt: image?.alt as string, width, height}\n}\n\n// Depending on the type of link, we need to fetch the corresponding page, post, or URL.  Otherwise return null.\nexport function linkResolver(link: Link | undefined) {\n  if (!link) return null\n\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.\n  if (!link.linkType && link.href) {\n    link.linkType = 'href'\n  }\n\n  switch (link.linkType) {\n    case 'href':\n      return link.href || null\n    case 'page':\n      if (link?.page && typeof link.page === 'string') {\n        return `/${link.page}`\n      }\n    case 'post':\n      if (link?.post && typeof link.post === 'string') {\n        return `/posts/${link.post}`\n      }\n\n    default:\n      return null\n  }\n}\n\ntype DataAttributeConfig = CreateDataAttributeProps &\n  Required<Pick<CreateDataAttributeProps, 'id' | 'type' | 'path'>>\n\nexport function dataAttr(config: DataAttributeConfig) {\n  return createDataAttribute({\n    projectId,\n    dataset,\n    baseUrl: studioUrl,\n  }).combine(config)\n}\n",
                  "id": "node-utils-linktype-case",
                  "name": "utils.ts",
                  "nodeType": "file"
                }
              ],
              "code": "",
              "id": "folder-1757588761958",
              "name": "lib",
              "nodeType": "folder"
            }
          ],
          "code": "",
          "id": "folder-1757588738551",
          "name": "sanity",
          "nodeType": "folder"
        }
      ],
      "path": "frontend"
    },
    {
      "id": "path-1756910010585-qntg5zw",
      "nodes": [
        {
          "_key": "1757588655885-mozwooqnm",
          "_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 {DocumentTextIcon} from '@sanity/icons'\nimport {format, parseISO} from 'date-fns'\nimport {defineField, defineType} from 'sanity'\n\nexport const {{.LowerCasePageTypeSingular}} = defineType({\n  name: '{{.LowerCasePageTypeSingular}}',\n  title: '{{.PascalCasePageTypeSingular}}',\n  icon: DocumentTextIcon,\n  type: 'document',\n  fields: [\n    defineField({\n      name: 'title',\n      title: 'Title',\n      type: 'string',\n      validation: (rule) => rule.required(),\n    }),\n    defineField({\n      name: 'slug',\n      title: 'Slug',\n      type: 'slug',\n      description: 'A slug is required for the page to show up in the preview',\n      options: {\n        source: 'title',\n        maxLength: 96,\n        isUnique: (value, context) => context.defaultIsUnique(value, context),\n      },\n      validation: (rule) => rule.required(),\n    }),\n    defineField({\n      name: 'content',\n      title: 'Content',\n      type: 'blockContent',\n    }),\n    defineField({\n      name: 'excerpt',\n      title: 'Excerpt',\n      type: 'text',\n    }),\n    defineField({\n      name: 'coverImage',\n      title: 'Cover Image',\n      type: 'image',\n      options: {\n        hotspot: true,\n        aiAssist: {\n          imageDescriptionField: 'alt',\n        },\n      },\n      fields: [\n        {\n          name: 'alt',\n          type: 'string',\n          title: 'Alternative text',\n          description: 'Important for SEO and accessibility.',\n          validation: (rule) => {\n            // Custom validation to ensure alt text is provided if the image is present. https://www.sanity.io/docs/validation\n            return rule.custom((alt, context) => {\n              if ((context.document?.coverImage as any)?.asset?._ref && !alt) {\n                return 'Required'\n              }\n              return true\n            })\n          },\n        },\n      ],\n      validation: (rule) => rule.required(),\n    }),\n    defineField({\n      name: 'date',\n      title: 'Date',\n      type: 'datetime',\n      initialValue: () => new Date().toISOString(),\n    }),\n    defineField({\n      name: 'author',\n      title: 'Author',\n      type: 'reference',\n      to: [{type: 'person'}],\n    }),\n  ],\n  // List preview configuration. https://www.sanity.io/docs/previews-list-views\n  preview: {\n    select: {\n      title: 'title',\n      authorFirstName: 'author.firstName',\n      authorLastName: 'author.lastName',\n      date: 'date',\n      media: 'coverImage',\n    },\n    prepare({title, media, authorFirstName, authorLastName, date}) {\n      const subtitles = [\n        authorFirstName && authorLastName && `by ${authorFirstName} ${authorLastName}`,\n        date && `on ${format(parseISO(date), 'LLL d, yyyy')}`,\n      ].filter(Boolean)\n\n      return {title, media, subtitle: subtitles.join(' ')}\n    },\n  },\n})\n",
              "id": "file-studio-schema",
              "name": "{{.KebabCasePageTypeSingular}}.ts",
              "nodeType": "file"
            }
          ],
          "code": "",
          "id": "folder-1757588655885",
          "name": "documents",
          "nodeType": "folder"
        },
        {
          "_key": "1757588626543-lvk3lb2yi",
          "_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": null,
                  "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": null,
                  "title": "EXTRA INTERNAL LINK FIELD"
                }
              ],
              "children": [],
              "code": "import {defineArrayMember, defineType, defineField} from 'sanity'\n\n/**\n * This is the schema definition for the rich text fields used for\n * for this blog studio. When you import it in schemas.js it can be\n * reused in other parts of the studio with:\n *  {\n *    name: 'someName',\n *    title: 'Some title',\n *    type: 'blockContent'\n *  }\n *\n * Learn more: https://www.sanity.io/docs/block-content\n */\nexport const blockContent = defineType({\n  title: 'Block Content',\n  name: 'blockContent',\n  type: 'array',\n  of: [\n    defineArrayMember({\n      type: 'block',\n      titles: {\n        annotations: [\n          {\n            name: 'link',\n            type: 'object',\n            title: 'Link',\n            fields: [\n              defineField({\n                name: 'linkType',\n                title: 'Link Type',\n                type: 'string',\n                initialValue: 'href',\n                options: {\n                  list: [\n                    {title: 'URL', value: 'href'},\n                    {title: 'Page', value: 'page'},\n                    {title: 'Post', value: 'post'},\n                  ],\n                  layout: 'radio',\n                },\n              }),\n\n              defineField({\n                name: 'href',\n                title: 'URL',\n                type: 'url',\n                hidden: ({parent}) => parent?.linkType !== 'href' && parent?.linkType != null,\n                validation: (Rule) =>\n                  Rule.custom((value, context: any) => {\n                    if (context.parent?.linkType === 'href' && !value) {\n                      return 'URL is required when Link Type is URL'\n                    }\n                    return true\n                  }),\n              }),\n\n              defineField({\n                name: 'page',\n                title: 'Page',\n                type: 'reference',\n                to: [{type: 'page'}],\n                hidden: ({parent}) => parent?.linkType !== 'page',\n                validation: (Rule) =>\n                  Rule.custom((value, context: any) => {\n                    if (context.parent?.linkType === 'page' && !value) {\n                      return 'Page reference is required when Link Type is Page'\n                    }\n                    return true\n                  }),\n              }),\n\n              defineField({\n                name: 'post',\n                title: 'Post',\n                type: 'reference',\n                to: [{type: 'post'}],\n                hidden: ({parent}) => parent?.linkType !== 'post',\n                validation: (Rule) =>\n                  Rule.custom((value, context: any) => {\n                    if (context.parent?.linkType === 'post' && !value) {\n                      return 'Post reference is required when Link Type is Post'\n                    }\n                    return true\n                  }),\n              }),\n\n              defineField({\n                name: 'openInNewTab',\n                title: 'Open in new tab',\n                type: 'boolean',\n                initialValue: false,\n              }),\n            ],\n          },\n        ],\n      },\n    }),\n  ],\n})\n\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": null,
                  "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": null,
                  "title": "Adding PageType as a Link Field"
                }
              ],
              "children": [],
              "code": "import {defineField, defineType} from 'sanity'\nimport {LinkIcon} from '@sanity/icons'\n\n/**\n * Link schema object. This link object lets the user first select the type of link and then\n * then enter the URL, page reference, or post reference - depending on the type selected.\n * Learn more: https://www.sanity.io/docs/object-type\n */\nexport const link = defineType({\n  name: 'link',\n  title: 'Link',\n  type: 'object',\n  icon: LinkIcon,\n  fields: [\n    defineField({\n      name: 'linkType',\n      title: 'Link Type',\n      type: 'string',\n      initialValue: 'url',\n      options: {\n        list: [\n          {title: 'URL', value: 'href'},\n          {title: 'Page', value: 'page'},\n          {title: 'Post', value: 'post'},\n        ],\n        layout: 'radio',\n      },\n    }),\n\n    // URL\n    defineField({\n      name: 'href',\n      title: 'URL',\n      type: 'url',\n      hidden: ({parent}) => parent?.linkType !== 'href',\n      validation: (Rule) =>\n        Rule.custom((value, context: any) => {\n          if (context.parent?.linkType === 'href' && !value) {\n            return 'URL is required when Link Type is URL'\n          }\n          return true\n        }),\n    }),\n\n    // Page\n    defineField({\n      name: 'page',\n      title: 'Page',\n      type: 'reference',\n      to: [{type: 'page'}],\n      hidden: ({parent}) => parent?.linkType !== 'page',\n      validation: (Rule) =>\n        Rule.custom((value, context: any) => {\n          if (context.parent?.linkType === 'page' && !value) {\n            return 'Page reference is required when Link Type is Page'\n          }\n          return true\n        }),\n    }),\n\n    // Post\n    defineField({\n      name: 'post',\n      title: 'Post',\n      type: 'reference',\n      to: [{type: 'post'}],\n      hidden: ({parent}) => parent?.linkType !== 'post',\n      validation: (Rule) =>\n        Rule.custom((value, context: any) => {\n          if (context.parent?.linkType === 'post' && !value) {\n            return 'Post reference is required when Link Type is Post'\n          }\n          return true\n        }),\n    }),\n\n\n    defineField({\n      name: 'openInNewTab',\n      title: 'Open in new tab',\n      type: 'boolean',\n      initialValue: false,\n    }),\n  ],\n})\n\n\n\n",
              "id": "node-link-schema-fixes",
              "name": "link.ts",
              "nodeType": "file"
            }
          ],
          "code": "",
          "id": "folder-1757588626543",
          "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": null,
              "title": "Importing PageType to SchemaTypos Index"
            },
            {
              "logic": {
                "behaviour": "addMarkerAboveTarget",
                "content": "{{.LowerCasePageTypeSingular}},",
                "mark": "NEXTGEN PAGETYPES",
                "occurrence": "last",
                "target": "// Objects"
              },
              "mark": null,
              "title": "Adding DocumentType to SchemaType array"
            }
          ],
          "children": [],
          "code": "import {person} from './documents/person'\nimport {page} from './documents/page'\nimport {post} from './documents/post'\nimport {callToAction} from './objects/callToAction'\nimport {infoSection} from './objects/infoSection'\nimport {settings} from './singletons/settings'\nimport {link} from './objects/link'\nimport {blockContent} from './objects/blockContent'\n\n// Export an array of all the schema types.  This is used in the Sanity Studio configuration. https://www.sanity.io/docs/schema-types\nexport const schemaTypes = [\n  // Singletons\n  settings,\n  // Documents\n  page,\n  post,\n  person,\n  // Objects\n  blockContent,\n  infoSection,\n  callToAction,\n  link,\n]\n",
          "id": "file-studio-indexer",
          "name": "index.ts",
          "nodeType": "file"
        }
      ],
      "path": "studio/src/schemaTypes"
    }
  ],
  "goals": [
    {
      "_key": "7f757c26918f",
      "_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": "c3588fbb2060",
      "_type": "goal",
      "fileHints": null,
      "howToTips": null,
      "title": "Create List Component which output posts from new PageType"
    },
    {
      "_key": "b7c5c9b1d2f3",
      "_type": "goal",
      "fileHints": null,
      "howToTips": null,
      "title": "Create dedicated query file for the new PageType"
    },
    {
      "_key": "e8441ba9da93587f5c1e72be114ac312",
      "description": "Make the new document type available in Studio so editors can create and edit it right away.",
      "fileHints": [
        "studio/src/schemaTypes/index.ts"
      ],
      "howToTips": [
        "Add the import near other document imports (addMarkerBelowTarget fits well).",
        "Add the type to the documents section of schemaTypes before the objects divider (addMarkerAboveTarget)."
      ],
      "title": "Register PageType in Studio schema index"
    },
    {
      "_key": "87229e86eeeb29753e57f4578c00b8ef",
      "description": "Expose common query fragments once so other queries can reuse them consistently and avoid re-definitions.",
      "fileHints": [
        "frontend/sanity/lib/queries.ts"
      ],
      "howToTips": [
        "Find the first canonical 'const' for each fragment and switch it to 'export const' (replaceIfMissing).",
        "Guard with requireAbsent so you don’t double-export."
      ],
      "title": "Export shared GROQ fragments (postFields, linkFields, linkReference)"
    },
    {
      "_key": "1e5c6237f79e4842377f272f0ac607aa",
      "description": "Centralize the Page Builder selections in a shared export and point getPageQuery to it to prevent drift.",
      "fileHints": [
        "frontend/sanity/lib/queries.ts"
      ],
      "howToTips": [
        "Introduce an exported fragment near related exports and update the query to reference it (replaceBetween).",
        "Use requireAbsent to avoid redefining if it’s already exported."
      ],
      "title": "Export pageBuilderFields and reuse in getPageQuery"
    },
    {
      "_key": "d25745808429e2fc8d040a16afda66b8",
      "description": "Let rich-text links resolve your new type by exposing its slug in the link mapping used by queries.",
      "fileHints": [
        "frontend/sanity/lib/queries.ts"
      ],
      "howToTips": [
        "Locate the object that maps linkable types to slugs.",
        "Insert a new key for the PageType near the start of the mapping (addMarkerBelowTarget)."
      ],
      "title": "Add PageType to linkReference mapping (rich-text links)"
    },
    {
      "_key": "b0f4d9e19dc80ae9f29f623ac4e7cea7",
      "description": "Teach the client resolver how to build URLs for the new type so internal links are consistent.",
      "fileHints": [
        "frontend/sanity/lib/utils.ts"
      ],
      "howToTips": [
        "Find the switch/branch that handles link types.",
        "Add a case for the PageType just before the default (addMarkerAboveTarget)."
      ],
      "title": "Add PageType route case to linkResolver"
    },
    {
      "_key": "ff2e4fd8e27cfe0f007631f703d55901",
      "description": "Add the new type to the sitemap filter so search engines can discover these pages.",
      "fileHints": [
        "frontend/sanity/lib/queries.ts"
      ],
      "howToTips": [
        "Identify the list of allowed _type checks in the sitemap query.",
        "Insert the new _type immediately before the slug guard (insertBeforeInline) and set fallbackOnly to keep it idempotent."
      ],
      "title": "Include PageType in sitemapData query"
    },
    {
      "_key": "a67516fe85b27a9b2486a37d6c5432aa",
      "description": "Surface the new type in the main navigation so users can find its listing page.",
      "fileHints": [
        "frontend/app/components/Header.tsx"
      ],
      "howToTips": [
        "Find the first stable nav list item (e.g., the first <li>) and place the archive link alongside similar links.",
        "Use addMarkerBelowTarget with occurrence: 'first' and fallbackOnly: true."
      ],
      "title": "Add archive link to Header nav (idempotent)"
    },
    {
      "_key": "af45eb9dfa4c67d6ffcbc7ba324d8c6c",
      "description": "Let editors choose the new type directly in portable text link options.",
      "fileHints": [
        "studio/src/schemaTypes/objects/blockContent.tsx"
      ],
      "howToTips": [
        "Find the editor-facing list of link type options.",
        "Add an option entry near the end of the list (addMarkerAboveTarget)."
      ],
      "title": "Expose PageType as a link type in blockContent"
    },
    {
      "_key": "c4c189c62cb3bde4db3c700a8efbb103",
      "description": "Show a reference picker only when the new link type is selected, keeping forms tidy and validation accurate.",
      "fileHints": [
        "studio/src/schemaTypes/objects/blockContent.tsx"
      ],
      "howToTips": [
        "Locate the cluster of link-specific reference fields.",
        "Insert a conditional reference field at the end of that cluster (addMarkerAboveTarget)."
      ],
      "title": "Add conditional PageType reference field in blockContent"
    },
    {
      "_key": "edd92cf048bdf4e3b23a76e78cafb021",
      "description": "Add the new type to the generic link object so any schema using it can target this type too.",
      "fileHints": [
        "studio/src/schemaTypes/objects/link.ts"
      ],
      "howToTips": [
        "Find the radio/option list for link types.",
        "Add a new entry in the same style and order (addMarkerAboveTarget)."
      ],
      "title": "Expose PageType as a link type in link object"
    },
    {
      "_key": "8e434d979d415c1ed23348fa5d106034",
      "description": "Add a reference field that appears only when the new option is chosen and is required in that state.",
      "fileHints": [
        "studio/src/schemaTypes/objects/link.ts"
      ],
      "howToTips": [
        "Identify where other type-specific reference fields live.",
        "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-block-editor"
  },
  "slugCurrent": "add-page-type-with-block-editor",
  "title": "Add Page Type with Block Editor",
  "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"
    }
  ]
}