Add Pagebuilder Block
/add-pagebuilder-blockCreated: 16 Sept 2025, 15:52Updated: 16 Sept 2025, 15:52frontendββ π app
β ββ π components
β ββ π {{.PascalCaseBlockTypeSingular}}.tsx
β ββ π BlockRenderer.tsx
ββ π sanity
ββ π lib
ββ π queries.tsstudio/src/schemaTypesββ π documents
β ββ π page.ts
ββ π objects
β ββ π {{.KebabCaseBlockTypeSingular}}.ts
β ββ π pageBuilder.tsx
ββ π index.ts- P1BlockTypeSingularName your block type
Sanity object `_type` used inside pageBuilder arrays.
herotestimonialgalleryfeature
- Create React component for the new BlockTypefrontend/app/components/{{.PascalCaseBlockTypeSingular}}.tsx
Scaffold a presentational component that renders heading, text, and an optional button using ResolvedLink, strongly typed from sanity.types.
- Register BlockType in BlockRenderer (import + mapping)frontend/app/components/BlockRenderer.tsx
Wire the new component into the runtime renderer so Page Builder can display it.
How-To Tips
- Add an import for the new component below the React import (addMarkerBelowTarget).
- Append a keyβcomponent pair to the Blocks map (addMarkerBelowTarget) using the blockβs lowercased type as the key.
- Add BlockType selection to pageBuilderFields (GROQ)frontend/sanity/lib/queries.ts
Include the blockβs fields in the shared pageBuilder GROQ selection so itβs fetched for pages.
How-To Tips
- Within pageBuilderFields, add a new case `_type == "{{.KebabCaseBlockTypeSingular}}" => { ... }` near similar entries (addMarkerBelowTarget).
- Export pageBuilderFields and reuse in getPageQueryfrontend/sanity/lib/queries.ts
Ensure pageBuilderFields is a shared export and that getPageQuery references it to avoid drift.
How-To Tips
- Hoist pageBuilderFields into an exported fragment and update getPageQuery to interpolate it (replaceBetween) with requireAbsent guards.
- Export shared GROQ fragments if missing (linkFields, linkReference, postFields)frontend/sanity/lib/queries.ts
Stabilize commonly reused fragments as exports so other queries can import them consistently.
How-To Tips
- Use replaceIfMissing to switch each first canonical `const` to `export const`, guarded with requireAbsent to avoid double-exports.
- Create Sanity object schema for BlockTypestudio/src/schemaTypes/objects/{{.KebabCaseBlockTypeSingular}}.ts
Define the Studio object schema for the block with fields for heading, text, buttonText, and link, including a clean preview.
- Add BlockType to pageBuilder object (insert menu)studio/src/schemaTypes/objects/pageBuilder.tsx
Expose the new block in the reusable pageBuilder array so editors can add it via the insert menu.
How-To Tips
- Insert `{type: '{{.CamelCaseBlockTypeSingular}}'},` inside the `of: [` list (addMarkerBelowTarget) keeping consistent ordering.
- Switch Page document to the reusable pageBuilder objectstudio/src/schemaTypes/documents/page.ts
Standardize the Page schema to use the shared pageBuilder object instead of an inline array definition.
How-To Tips
- Replace the inline pageBuilder field with `{ name: 'pageBuilder', type: 'pageBuilder' }` (replaceBetween) to keep it idempotent.
- Register BlockType object in Studio schema indexstudio/src/schemaTypes/index.ts
Import the new object schema and add it to the exported schemaTypes array.
How-To Tips
- Add the import for the object near other object imports (addMarkerBelowTarget).
- Append the object to the Objects section of schemaTypes before the closing bracket (addMarkerAboveTarget).
- Ensure pageBuilder object is imported and listed in schemaTypesstudio/src/schemaTypes/index.ts
Guarantee the shared pageBuilder object is available in the Studio by importing and registering it once.
How-To Tips
- Import pageBuilder if absent and include it in schemaTypes (addMarkerAboveTarget with requireAbsent on both import and array entry).
frontend- appFolder
- componentsFolder
- {{.PascalCaseBlockTypeSingular}}.tsxFile
View Code
import {Suspense} from 'react' import ResolvedLink from '@/app/components/ResolvedLink' import { {{.PascalCaseBlockTypeSingular}} as {{.PascalCaseBlockTypeSingular}}Type } from '@/sanity.types' type {{.PascalCaseBlockTypeSingular}}Props = { block: {{.PascalCaseBlockTypeSingular}}Type } export default function {{.PascalCaseBlockTypeSingular}}({block}: {{.PascalCaseBlockTypeSingular}}Props) { return ( <section className="container my-12"> <div className="bg-gray-50 border border-gray-100 rounded-2xl p-10 grid gap-6"> {block?.heading && ( <h2 className="text-3xl font-bold tracking-tight text-black sm:text-4xl">{block.heading}</h2> )} {block?.text && <p className="text-lg leading-8 text-gray-600">{block.text}</p>} {block?.buttonText && block?.link && ( <Suspense fallback={null}> <div className="flex items-center gap-x-6"> <ResolvedLink link={block.link} className="rounded-full flex gap-2 items-center bg-black hover:bg-blue focus:bg-blue py-3 px-6 text-white transition-colors duration-200" > {block.buttonText} </ResolvedLink> </div> </Suspense> )} </div> </section> ) } - BlockRenderer.tsxFile β’ Action FileActions
- Import Pagebuilder Block Component to BlockRendererBehaviour: addMarkerBelowTargetOccurrence: lastTarget:
import React from 'react'Content
import {{.PascalCaseBlockTypeSingular}} from '@/app/components/{{.PascalCaseBlockTypeSingular}}' - Adding Pagebuilder Block Components to "Blocks"Behaviour: addMarkerBelowTargetOccurrence: lastTarget:
const Blocks: BlocksType = {Content
{{.LowerCaseBlockTypeSingular}}: {{.PascalCaseBlockTypeSingular}},
View Code
import React from 'react' import Cta from '@/app/components/Cta' import Info from '@/app/components/InfoSection' import {dataAttr} from '@/sanity/lib/utils' type BlocksType = { [key: string]: React.FC<any> } type BlockType = { _type: string _key: string } type BlockProps = { index: number block: BlockType pageId: string pageType: string } const Blocks: BlocksType = { callToAction: Cta, infoSection: Info, } export default function BlockRenderer({block, index, pageId, pageType}: BlockProps) { if (typeof Blocks[block._type] !== 'undefined') { return ( <div key={block._key} data-sanity={dataAttr({ id: pageId, type: pageType, path: `pageBuilder[_key==\"${block._key}\"]`, }).toString()} > {React.createElement(Blocks[block._type], { key: block._key, block: block, index: index, })} </div> )} return ( <div className="w-full bg-gray-100 text-center text-gray-500 p-20 rounded"> A β{block._type}β block hasn\'t been created </div> ) }
- sanityFolder
- libFolder
- queries.tsFile β’ Action FileActions
- Setting "linkFields" to ExportBehaviour: replaceIfMissingOccurrence: firstTarget:
const linkFields = /* groq */ ` - Setting "postFields" to ExportBehaviour: replaceIfMissingOccurrence: firstTarget:
const postFields = /* groq */ ` - Setting "linkReference" to ExportBehaviour: replaceIfMissingOccurrence: firstTarget:
const linkReference = /* groq */ ` - Making "PageBuilder Fields" exportableBehaviour: replaceBetweenOccurrence: first
- Adding "BlockType" to "pageBuilderFields"Behaviour: addMarkerBelowTargetOccurrence: firstTarget:
export const pageBuilderFields = /* groq */ `Content
_type == "{{.KebabCaseBlockTypeSingular}}" => { ..., },
View Code
//THIS IS AN INDEXER FILE 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} `)
studio/src/schemaTypes- documentsFolder
- page.tsFile β’ Action FileActions
- Use Standard Pagebuilder ObjectBehaviour: replaceBetweenOccurrence: last
View Code
import {defineField, defineType} from 'sanity' import {DocumentIcon} from '@sanity/icons' /** * Page schema. Define and edit the fields for the 'page' content type. * Learn more: https://www.sanity.io/docs/schema-types */ export const page = defineType({ name: 'page', title: 'Page', type: 'document', icon: DocumentIcon, fields: [ defineField({ name: 'name', title: 'Name', type: 'string', validation: (Rule) => Rule.required(), }), defineField({ name: 'slug', title: 'Slug', type: 'slug', validation: (Rule) => Rule.required(), options: { source: 'name', maxLength: 96, }, }), defineField({ name: 'heading', title: 'Heading', type: 'string', validation: (Rule) => Rule.required(), }), defineField({ name: 'subheading', title: 'Subheading', type: 'string', }), defineField({ name: 'pageBuilder', title: 'Page builder', type: 'array', of: [{type: 'callToAction'}, {type: 'infoSection'}], options: { insertMenu: { // Configure the "Add Item" menu to display a thumbnail preview of the content type. https://www.sanity.io/docs/array-type#efb1fe03459d views: [ { name: 'grid', previewImageUrl: (schemaTypeName) => `/static/page-builder-thumbnails/${schemaTypeName}.webp`, }, ], }, }, }), ], })
- objectsFolder
- {{.KebabCaseBlockTypeSingular}}.tsFile
View Code
import {defineField, defineType} from 'sanity' import {DocumentIcon} from '@sanity/icons' export const {{.LowerCaseBlockTypeSingular}} = defineType({ name: '{{.LowerCaseBlockTypeSingular}}', title: '{{.PascalCaseBlockTypeSingular}}', type: 'object', icon: DocumentIcon, fields: [ defineField({ name: 'heading', title: 'Heading', type: 'string' }), defineField({ name: 'text', title: 'Text', type: 'text' }), defineField({ name: 'buttonText', title: 'Button text', type: 'string' }), defineField({ name: 'link', title: 'Button link', type: 'link' }) ], preview: { select: { title: 'heading' }, prepare({ title }) { return { title: title || '{{.PascalCaseBlockTypeSingular}}', subtitle: '{{.PascalCaseBlockTypeSingular}} block' } } } }) - pageBuilder.tsxFile β’ Action FileActions
- Add BlockType to pageBuilder SchemaBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
of: [Content
{type: '{{.CamelCaseBlockTypeSingular}}'},
View Code
import { defineType } from "sanity"; export const pageBuilder = defineType({ name: 'pageBuilder', title: 'Page builder', type: 'array', of: [ {type: 'callToAction'}, {type: 'infoSection'} ], options: { insertMenu: { // Configure the "Add Item" menu to display a thumbnail preview of the content type. https://www.sanity.io/docs/array-type#efb1fe03459d views: [ { name: 'grid', previewImageUrl: (schemaTypeName) => `/static/page-builder-thumbnails/${schemaTypeName}.webp`, }, ], }, } })
- index.tsFile β’ Action FileActions
- Imporing Pagebuilder BlockTypeBehaviour: addMarkerBelowTargetOccurrence: lastTarget:
import {blockContent} from './objects/blockContent'Content
import { {{.LowerCaseBlockTypeSingular}} } from './objects/{{.KebabCaseBlockTypeSingular}}' - OBJECT ARRAY ITEMBehaviour: addMarkerAboveTargetOccurrence: lastTarget:
]Content
{{.LowerCaseBlockTypeSingular}}, - Add Pagebuilder Object to SchemaTypesBehaviour: addMarkerAboveTargetOccurrence: lastTarget:
]Content
pageBuilder,
- Import PageBuilder Object to schemaTypes indexBehaviour: addMarkerAboveTargetOccurrence: firstTarget:
export const schemaTypes = [Content
import {pageBuilder} from './objects/pageBuilder'
View Code
import {person} from './documents/person' import {page} from './documents/page' import {post} from './documents/post' import {callToAction} from './objects/callToAction' import {infoSection} from './objects/infoSection' import {settings} from './singletons/settings' import {link} from './objects/link' import {blockContent} from './objects/blockContent' // Export 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-16T15:52:29Z",
"_id": "b0223c4c-63dc-4b14-b8e8-b85487273fff",
"_rev": "2lI7L5yvGPDIkpqLKgygb2",
"_system": {
"base": {
"id": "b0223c4c-63dc-4b14-b8e8-b85487273fff",
"rev": "2lI7L5yvGPDIkpqLKgyfoG"
}
},
"_type": "command-slug",
"_updatedAt": "2025-09-16T15:52:46Z",
"description": "Instantly scaffold a new Page Builder block across your Sanity Studio and Next.js frontendβgenerating a typed React component, wiring it into BlockRenderer, extending the shared GROQ pageBuilderFields and getPageQuery, creating and registering the Studio object schema, exposing it in the reusable pageBuilder array/menu, and updating the Studio index.",
"filePaths": [
{
"id": "path-1758028961857-11q1izg5e",
"nodes": [
{
"_key": "1758028961857-f4n8ezmbp",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "1758028961857-0o2kx4ll9",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "addblock-component-file",
"_type": "treeNode",
"actionFile": false,
"actions": [
{
"_key": "DZ9eudr4jMd24KPubMpyWj",
"logic": {},
"mark": ""
}
],
"children": [],
"code": "import {Suspense} from 'react'\nimport ResolvedLink from '@/app/components/ResolvedLink'\nimport { {{.PascalCaseBlockTypeSingular}} as {{.PascalCaseBlockTypeSingular}}Type } from '@/sanity.types'\n\ntype {{.PascalCaseBlockTypeSingular}}Props = {\n block: {{.PascalCaseBlockTypeSingular}}Type\n}\n\nexport default function {{.PascalCaseBlockTypeSingular}}({block}: {{.PascalCaseBlockTypeSingular}}Props) {\n return (\n <section className=\"container my-12\">\n <div className=\"bg-gray-50 border border-gray-100 rounded-2xl p-10 grid gap-6\">\n {block?.heading && (\n <h2 className=\"text-3xl font-bold tracking-tight text-black sm:text-4xl\">{block.heading}</h2>\n )}\n {block?.text && <p className=\"text-lg leading-8 text-gray-600\">{block.text}</p>}\n {block?.buttonText && block?.link && (\n <Suspense fallback={null}>\n <div className=\"flex items-center gap-x-6\">\n <ResolvedLink\n link={block.link}\n className=\"rounded-full flex gap-2 items-center bg-black hover:bg-blue focus:bg-blue py-3 px-6 text-white transition-colors duration-200\"\n >\n {block.buttonText}\n </ResolvedLink>\n </div>\n </Suspense>\n )}\n </div>\n </section>\n )\n}\n",
"id": "file-frontend-block-component",
"name": "{{.PascalCaseBlockTypeSingular}}.tsx",
"nodeType": "file"
},
{
"_key": "addblock-renderer-patch",
"_type": "treeNode",
"actionFile": true,
"actions": [
{
"_key": "DZ9eudr4jMd24KPubMpybJ",
"logic": {
"behaviour": "addMarkerBelowTarget",
"content": "\nimport {{.PascalCaseBlockTypeSingular}} from '@/app/components/{{.PascalCaseBlockTypeSingular}}'",
"mark": "IMPORT PAGEBUILDER BLOCK COMPONENTS",
"occurrence": "last",
"target": "import React from 'react'"
},
"title": "Import Pagebuilder Block Component to BlockRenderer"
},
{
"_key": "DZ9eudr4jMd24KPubMpyft",
"logic": {
"behaviour": "addMarkerBelowTarget",
"content": " {{.LowerCaseBlockTypeSingular}}: {{.PascalCaseBlockTypeSingular}},\n",
"mark": "PAGEBUILDER COMPONENTS",
"occurrence": "last",
"target": "const Blocks: BlocksType = {"
},
"title": "Adding Pagebuilder Block Components to \"Blocks\""
}
],
"children": [],
"code": "import React from 'react'\n\nimport Cta from '@/app/components/Cta'\nimport Info from '@/app/components/InfoSection'\nimport {dataAttr} from '@/sanity/lib/utils'\n\n\ntype BlocksType = {\n [key: string]: React.FC<any>\n}\n\ntype BlockType = {\n _type: string\n _key: string\n}\n\ntype BlockProps = {\n index: number\n block: BlockType\n pageId: string\n pageType: string\n}\n\nconst Blocks: BlocksType = {\n callToAction: Cta,\n infoSection: Info,\n}\n\nexport default function BlockRenderer({block, index, pageId, pageType}: BlockProps) {\n if (typeof Blocks[block._type] !== 'undefined') {\n return (\n <div\n key={block._key}\n data-sanity={dataAttr({\n id: pageId,\n type: pageType,\n path: `pageBuilder[_key==\\\"${block._key}\\\"]`,\n }).toString()}\n >\n {React.createElement(Blocks[block._type], {\n key: block._key,\n block: block,\n index: index,\n })}\n </div>\n )}\n return (\n <div className=\"w-full bg-gray-100 text-center text-gray-500 p-20 rounded\">\n A β{block._type}β block hasn\\'t been created\n </div>\n )\n}\n",
"id": "file-frontend-block-renderer",
"name": "BlockRenderer.tsx",
"nodeType": "file"
}
],
"code": "",
"id": "folder-1758028961857-0o2kx4ll9",
"name": "components",
"nodeType": "folder"
}
],
"code": "",
"id": "folder-1758028961857-f4n8ezmbp",
"name": "app",
"nodeType": "folder"
},
{
"_key": "1758028961857-8mopymmtk",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "1758028961857-0iq8le4bl",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "1756919951450-hqircx415",
"_type": "treeNode",
"actionFile": true,
"actions": [
{
"_key": "DZ9eudr4jMd24KPubMpykT",
"logic": {
"behaviour": "replaceIfMissing",
"occurrence": "first",
"replacement": "export const linkFields = /* groq */ `",
"requireAbsent": "export const linkFields = /* groq */ `",
"target": "const linkFields = /* groq */ `"
},
"title": "Setting \"linkFields\" to Export"
},
{
"_key": "DZ9eudr4jMd24KPubMpyp3",
"logic": {
"behaviour": "replaceIfMissing",
"occurrence": "first",
"replacement": "export const postFields = /* groq */ `",
"requireAbsent": "export const postFields = /* groq */ `",
"target": "const postFields = /* groq */ `"
},
"title": "Setting \"postFields\" to Export"
},
{
"_key": "DZ9eudr4jMd24KPubMpytd",
"logic": {
"behaviour": "replaceIfMissing",
"occurrence": "first",
"replacement": "export const linkReference = /* groq */ `",
"requireAbsent": "export const linkReference = /* groq */ `",
"target": "const linkReference = /* groq */ `"
},
"title": "Setting \"linkReference\" to Export"
},
{
"_key": "DZ9eudr4jMd24KPubMpyyD",
"logic": {
"behaviour": "replaceBetween",
"occurrence": "first",
"replacement": "export const pageBuilderFields = /* groq */ `\n ...,\n _type == \"callToAction\" => {\n ${linkFields},\n },\n _type == \"infoSection\" => {\n content[]{\n ...,\n markDefs[]{\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": "Making \"PageBuilder Fields\" exportable"
},
{
"_key": "DZ9eudr4jMd24KPubMpz2n",
"logic": {
"behaviour": "addMarkerBelowTarget",
"content": "_type == \"{{.KebabCaseBlockTypeSingular}}\" => {\n...,\n },",
"fallbackOnly": false,
"mark": "BLOCKTYPES FOR PAGEBUILDER",
"occurrence": "first",
"target": "export const pageBuilderFields = /* groq */ `"
},
"title": "Adding \"BlockType\" to \"pageBuilderFields\""
}
],
"children": [],
"code": "//THIS IS AN INDEXER FILE \nimport {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"
}
],
"code": "",
"id": "folder-1758028961857-0iq8le4bl",
"name": "lib",
"nodeType": "folder"
}
],
"code": "",
"id": "folder-1758028961857-8mopymmtk",
"name": "sanity",
"nodeType": "folder"
}
],
"path": "frontend"
},
{
"id": "studio-object-schema",
"nodes": [
{
"_key": "1758030195496-luzwl8rkw",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "1758030210890-6twupqdki",
"_type": "treeNode",
"actionFile": true,
"actions": [
{
"logic": {
"behaviour": "replaceBetween",
"content": null,
"mark": null,
"occurrence": "last",
"target": null
},
"mark": "",
"title": "Use Standard Pagebuilder Object"
}
],
"children": [],
"code": "import {defineField, defineType} from 'sanity'\nimport {DocumentIcon} from '@sanity/icons'\n\n/**\n * Page schema. Define and edit the fields for the 'page' content type.\n * Learn more: https://www.sanity.io/docs/schema-types\n */\n\nexport const page = defineType({\n name: 'page',\n title: 'Page',\n type: 'document',\n icon: DocumentIcon,\n fields: [\n defineField({\n name: 'name',\n title: 'Name',\n type: 'string',\n validation: (Rule) => Rule.required(),\n }),\n\n defineField({\n name: 'slug',\n title: 'Slug',\n type: 'slug',\n validation: (Rule) => Rule.required(),\n options: {\n source: 'name',\n maxLength: 96,\n },\n }),\n defineField({\n name: 'heading',\n title: 'Heading',\n type: 'string',\n validation: (Rule) => Rule.required(),\n }),\n defineField({\n name: 'subheading',\n title: 'Subheading',\n type: 'string',\n }),\n defineField({\n name: 'pageBuilder',\n title: 'Page builder',\n type: 'array',\n of: [{type: 'callToAction'}, {type: 'infoSection'}],\n options: {\n insertMenu: {\n // Configure the \"Add Item\" menu to display a thumbnail preview of the content type. https://www.sanity.io/docs/array-type#efb1fe03459d\n views: [\n {\n name: 'grid',\n previewImageUrl: (schemaTypeName) =>\n `/static/page-builder-thumbnails/${schemaTypeName}.webp`,\n },\n ],\n },\n },\n }),\n ],\n})\n",
"id": "file-1758030210890",
"name": "page.ts",
"nodeType": "file"
}
],
"code": "",
"id": "folder-1758030195496",
"name": "documents",
"nodeType": "folder"
},
{
"_key": "1758029082445-f7khaqbw4",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "addblock-object-schema-file",
"_type": "treeNode",
"actionFile": false,
"actions": [
{
"logic": {
"behaviour": null,
"content": null,
"mark": null,
"occurrence": null,
"target": null
},
"mark": "",
"title": null
}
],
"children": [],
"code": "import {defineField, defineType} from 'sanity'\nimport {DocumentIcon} from '@sanity/icons'\n\nexport const {{.LowerCaseBlockTypeSingular}} = defineType({\n name: '{{.LowerCaseBlockTypeSingular}}',\n title: '{{.PascalCaseBlockTypeSingular}}',\n type: 'object',\n icon: DocumentIcon,\n fields: [\n defineField({ name: 'heading', title: 'Heading', type: 'string' }),\n defineField({ name: 'text', title: 'Text', type: 'text' }),\n defineField({ name: 'buttonText', title: 'Button text', type: 'string' }),\n defineField({ name: 'link', title: 'Button link', type: 'link' })\n ],\n preview: {\n select: { title: 'heading' },\n prepare({ title }) {\n return { title: title || '{{.PascalCaseBlockTypeSingular}}', subtitle: '{{.PascalCaseBlockTypeSingular}} block' }\n }\n }\n})\n",
"id": "file-studio-object-schema",
"name": "{{.KebabCaseBlockTypeSingular}}.ts",
"nodeType": "file"
},
{
"_key": "1758029181288-0wejuus4u",
"_type": "treeNode",
"actionFile": true,
"actions": [
{
"logic": {
"behaviour": "addMarkerBelowTarget",
"content": "{type: '{{.CamelCaseBlockTypeSingular}}'},",
"mark": "PAGEBUILDER BLOCKS",
"occurrence": "first",
"target": "of: ["
},
"mark": "",
"title": "Add BlockType to pageBuilder Schema"
}
],
"children": [],
"code": "import { defineType } from \"sanity\";\n\nexport const pageBuilder = defineType({\n name: 'pageBuilder',\n title: 'Page builder',\n type: 'array',\n of: [\n {type: 'callToAction'},\n {type: 'infoSection'}\n ],\n options: {\n insertMenu: {\n // Configure the \"Add Item\" menu to display a thumbnail preview of the content type. https://www.sanity.io/docs/array-type#efb1fe03459d\n views: [\n {\n name: 'grid',\n previewImageUrl: (schemaTypeName) =>\n `/static/page-builder-thumbnails/${schemaTypeName}.webp`,\n },\n ],\n },\n }\n})\n ",
"id": "file-1758029181288-0wejuus4u",
"name": "pageBuilder.tsx",
"nodeType": "file"
}
],
"code": "",
"id": "folder-1758029082445",
"name": "objects",
"nodeType": "folder"
},
{
"_key": "20250903-studio-indexer-file",
"_type": "treeNode",
"actionFile": true,
"actions": [
{
"logic": {
"behaviour": "addMarkerBelowTarget",
"content": "\nimport { {{.LowerCaseBlockTypeSingular}} } from './objects/{{.KebabCaseBlockTypeSingular}}'",
"mark": "IMPORT PAGEBUILDER BLOCKS",
"occurrence": "last",
"target": "import {blockContent} from './objects/blockContent'"
},
"mark": null,
"title": "Imporing Pagebuilder BlockType"
},
{
"logic": {
"behaviour": "addMarkerAboveTarget",
"content": "\n {{.LowerCaseBlockTypeSingular}},",
"mark": "PAGEBUILDER BLOCKS",
"occurrence": "last",
"target": "]"
},
"mark": null,
"title": "OBJECT ARRAY ITEM"
},
{
"logic": {
"behaviour": "addMarkerAboveTarget",
"content": "pageBuilder,",
"mark": null,
"occurrence": "last",
"target": "]"
},
"mark": "",
"title": "Add Pagebuilder Object to SchemaTypes"
},
{
"logic": {
"behaviour": "addMarkerAboveTarget",
"content": "import {pageBuilder} from './objects/pageBuilder'",
"mark": "",
"occurrence": "first",
"target": "export const schemaTypes = ["
},
"mark": "",
"title": "Import PageBuilder Object to schemaTypes index"
}
],
"children": [],
"code": "import {person} from './documents/person'\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\n\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": "DZ9eudr4jMd24KPubMpxmv",
"description": "Scaffold a presentational component that renders heading, text, and an optional button using ResolvedLink, strongly typed from sanity.types.",
"fileHints": [
"frontend/app/components/{{.PascalCaseBlockTypeSingular}}.tsx"
],
"howToTips": [],
"title": "Create React component for the new BlockType"
},
{
"_key": "DZ9eudr4jMd24KPubMpxrV",
"description": "Wire the new component into the runtime renderer so Page Builder can display it.",
"fileHints": [
"frontend/app/components/BlockRenderer.tsx"
],
"howToTips": [
"Add an import for the new component below the React import (addMarkerBelowTarget).",
"Append a keyβcomponent pair to the Blocks map (addMarkerBelowTarget) using the blockβs lowercased type as the key."
],
"title": "Register BlockType in BlockRenderer (import + mapping)"
},
{
"_key": "DZ9eudr4jMd24KPubMpxw5",
"description": "Include the blockβs fields in the shared pageBuilder GROQ selection so itβs fetched for pages.",
"fileHints": [
"frontend/sanity/lib/queries.ts"
],
"howToTips": [
"Within pageBuilderFields, add a new case `_type == \"{{.KebabCaseBlockTypeSingular}}\" => { ... }` near similar entries (addMarkerBelowTarget)."
],
"title": "Add BlockType selection to pageBuilderFields (GROQ)"
},
{
"_key": "DZ9eudr4jMd24KPubMpy0f",
"description": "Ensure pageBuilderFields is a shared export and that getPageQuery references it to avoid drift.",
"fileHints": [
"frontend/sanity/lib/queries.ts"
],
"howToTips": [
"Hoist pageBuilderFields into an exported fragment and update getPageQuery to interpolate it (replaceBetween) with requireAbsent guards."
],
"title": "Export pageBuilderFields and reuse in getPageQuery"
},
{
"_key": "DZ9eudr4jMd24KPubMpy5F",
"description": "Stabilize commonly reused fragments as exports so other queries can import them consistently.",
"fileHints": [
"frontend/sanity/lib/queries.ts"
],
"howToTips": [
"Use replaceIfMissing to switch each first canonical `const` to `export const`, guarded with requireAbsent to avoid double-exports."
],
"title": "Export shared GROQ fragments if missing (linkFields, linkReference, postFields)"
},
{
"_key": "DZ9eudr4jMd24KPubMpy9p",
"description": "Define the Studio object schema for the block with fields for heading, text, buttonText, and link, including a clean preview.",
"fileHints": [
"studio/src/schemaTypes/objects/{{.KebabCaseBlockTypeSingular}}.ts"
],
"howToTips": [],
"title": "Create Sanity object schema for BlockType"
},
{
"_key": "DZ9eudr4jMd24KPubMpyEP",
"description": "Expose the new block in the reusable pageBuilder array so editors can add it via the insert menu.",
"fileHints": [
"studio/src/schemaTypes/objects/pageBuilder.tsx"
],
"howToTips": [
"Insert `{type: '{{.CamelCaseBlockTypeSingular}}'},` inside the `of: [` list (addMarkerBelowTarget) keeping consistent ordering."
],
"title": "Add BlockType to pageBuilder object (insert menu)"
},
{
"_key": "DZ9eudr4jMd24KPubMpyIz",
"description": "Standardize the Page schema to use the shared pageBuilder object instead of an inline array definition.",
"fileHints": [
"studio/src/schemaTypes/documents/page.ts"
],
"howToTips": [
"Replace the inline pageBuilder field with `{ name: 'pageBuilder', type: 'pageBuilder' }` (replaceBetween) to keep it idempotent."
],
"title": "Switch Page document to the reusable pageBuilder object"
},
{
"_key": "DZ9eudr4jMd24KPubMpyNZ",
"description": "Import the new object schema and add it to the exported schemaTypes array.",
"fileHints": [
"studio/src/schemaTypes/index.ts"
],
"howToTips": [
"Add the import for the object near other object imports (addMarkerBelowTarget).",
"Append the object to the Objects section of schemaTypes before the closing bracket (addMarkerAboveTarget)."
],
"title": "Register BlockType object in Studio schema index"
},
{
"_key": "DZ9eudr4jMd24KPubMpyS9",
"description": "Guarantee the shared pageBuilder object is available in the Studio by importing and registering it once.",
"fileHints": [
"studio/src/schemaTypes/index.ts"
],
"howToTips": [
"Import pageBuilder if absent and include it in schemaTypes (addMarkerAboveTarget with requireAbsent on both import and array entry)."
],
"title": "Ensure pageBuilder object is imported and listed in schemaTypes"
}
],
"ignoredPatterns": [],
"slug": {
"_type": "slug",
"current": "add-pagebuilder-block"
},
"slugCurrent": "add-pagebuilder-block",
"title": "Add Pagebuilder Block",
"variables": [
{
"_type": "variableDefinition",
"description": "Sanity object `_type` used inside pageBuilder arrays.",
"examples": [
"hero",
"testimonial",
"gallery",
"feature"
],
"name": "BlockTypeSingular",
"priority": 1,
"title": "Name your block type"
}
]
}