Add Page Type with Block Editor
/add-page-type-with-block-editorCreated: 15 Sept 2025, 10:37Updated: 16 Sept 2025, 12:13frontendββ π app
β ββ π {{.KebabCasePageTypePlural}}
β β ββ π (index)
β β β ββ π page.tsx
β β ββ π [slug]
β β ββ π page.tsx
β ββ π components
β ββ π {{.PascalCasePageTypePlural}}.tsx
β ββ π Header.tsx
ββ π sanity
ββ π lib
ββ π pagetype-queries
β ββ π {{.KebabCasePageTypeSingular}}.queries.ts
ββ π queries.ts
ββ π utils.tsstudio/src/schemaTypesββ π documents
β ββ π {{.KebabCasePageTypeSingular}}.ts
ββ π objects
β ββ π blockContent.tsx
β ββ π link.ts
ββ π index.ts- P1PageTypeSingularName your page type
This is the name of the "_type" we use in Sanity. This dictates a lot of the naming conventions elsewhere.
authoreventproductservice - P2PageTypePluralPluralize your page type
This helps cases where we need to pluralize. No worries if it's the same as the singular.
authorseventsproductsservices
- Create Files and Folders for the Page Type routing
Creates folder for the route, with dedicated page.tsx for both (index) and [slug].
- Create List Component which output posts from new PageType
- Create dedicated query file for the new PageType
- Register PageType in Studio schema indexstudio/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).
- 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.
- Export pageBuilderFields and reuse in getPageQueryfrontend/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.
- 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).
- Add PageType route case to linkResolverfrontend/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).
- Include PageType in sitemapData queryfrontend/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.
- 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.
- Expose PageType as a link type in blockContentstudio/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).
- Add conditional PageType reference field in blockContentstudio/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).
- Expose PageType as a link type in link objectstudio/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).
- Add conditional PageType reference field in link objectstudio/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).
frontend- appFolder
- {{.KebabCasePageTypePlural}}Folder
- (index)Folder
- page.tsxFile
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.tsxFile
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> </> ) }
- componentsFolder
- {{.PascalCasePageTypePlural}}.tsxFile
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.tsxFile β’ Action FileActions
- PAGETYPE ARCHIVE LINKBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
<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> ) }
- sanityFolder
- libFolder
- pagetype-queriesFolder
- {{.KebabCasePageTypeSingular}}.queries.tsFile
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.tsFile β’ Action FileActions
- set linkFields to ExportBehaviour: replaceIfMissingOccurrence: firstTarget:
const linkFields = /* groq */ ` - Set postFields to ExportBehaviour: replaceIfMissingOccurrence: firstTarget:
const postFields = /* groq */ ` - Set linkReference to ExportBehaviour: replaceIfMissingOccurrence: firstTarget:
const linkReference = /* groq */ ` - connect up PageType as linkReferenceBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
_type == "link" => {Content
"{{.LowerCasePageTypeSingular}}": {{.LowerCasePageTypeSingular}}->slug.current, - adding PageType to SitemapBehaviour: insertBeforeInlineOccurrence: firstTarget:
&& defined(slug.current)] | order(_type asc) {Content
|| _type == "{{.LowerCasePageTypeSingular}}" - Exportable PageBuilder FieldsBehaviour: replaceBetweenOccurrence: 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.tsFile β’ Action FileActions
- Setting up routing for PageTypeBehaviour: addMarkerAboveTargetOccurrence: firstTarget:
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) }
studio/src/schemaTypes- documentsFolder
- {{.KebabCasePageTypeSingular}}.tsFile
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(' ')} }, }, })
- objectsFolder
- blockContent.tsxFile β’ Action FileActions
- Adding PageType as a LinkType in BlockContentBehaviour: addMarkerAboveTargetOccurrence: firstTarget:
],Content
{title: '{{.PascalCasePageTypeSingular}}', value: '{{.LowerCasePageTypeSingular}}'}, - EXTRA INTERNAL LINK FIELDBehaviour: addMarkerAboveTargetOccurrence: lastTarget:
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.tsFile β’ Action FileActions
- Adding PageType as a LinkType optionBehaviour: addMarkerAboveTargetOccurrence: firstTarget:
],Content
{title: '{{.PascalCasePageTypeSingular}}', value: '{{.LowerCasePageTypeSingular}}'}, - Adding PageType as a Link FieldBehaviour: addMarkerAboveTargetOccurrence: lastTarget:
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.tsFile β’ Action FileActions
- Importing PageType to SchemaTypos IndexBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
import {post} from './documents/post'Content
import { {{.LowerCasePageTypeSingular}} } from './documents/{{.KebabCasePageTypeSingular}}' - Adding DocumentType to SchemaType arrayBehaviour: addMarkerAboveTargetOccurrence: lastTarget:
// ObjectsContent
{{.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, ]
{
"_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"
}
]
}