Add Portable Text Type
/add-portable-text-typeCreated: 19 Sept 2025, 11:47Updated: 19 Sept 2025, 11:47Variables
1
Goals
2
Path Groups
2
Nodes
14
Compact File Tree
Quick overview of planned files
Base Path
/studio/srcββ π schemaTypes
ββ π objects
β ββ π portable-text-components
β β ββ π portable-text-types
β β ββ π portable{{.PascalPortableBlock}}.ts
β ββ π blockContent.tsx
ββ π index.tsBase Path
frontendββ π app
ββ π components
ββ π portable-text-components
β ββ π portable-types
β ββ π index.ts
β ββ π Portable{{.PascalCasePortableBlock}}.tsx
ββ π PortableText.tsxVariables
Argument-driven inputs used by your generator
- P1PortableBlockPortableBlock
The name of the portable text component to add.
imagepollquotequestionnaire
Ignored Patterns
Globs/paths skipped by the executor
No ignored patterns.
Goals
What this command is trying to accomplish
- Create React component for the new Portable Text componentfrontend/app/components/{{.PascalCasePortableBlock}}.tsx
Scaffold a presentational component that renders heading, text, and an optional button using ResolvedLink, strongly typed from sanity.types.
- Register Portable Text component in Studio schema indexstudio/src/schemaTypes/index.ts
Makes the Portable Text UI available in Studio so the new Portable Text component can actually use its fields.
How-To Tips
- Add the import near other object imports (use an addMarkerAboveTarget-style insertion; guard with requireAbsent).
- Include 'portableText' in the schemaTypes array close to other objects (addMarkerAboveTarget; guard with requireAbsent).
File Tree
Detailed view with actions
Base Path
/studio/srcDetailed view
- schemaTypesFolder
- objectsFolder
- portable-text-componentsFolder
- portable-text-typesFolder
- portable{{.PascalPortableBlock}}.tsFile
View Code
import {defineField, defineType} from 'sanity' import {TextIcon} from '@sanity/icons' export const portable{{.PascalCasePortableBlock}} = defineType({ name: 'portable{{.PascalCasePortableBlock}}', title: '{{.PortableBlock}}', type: 'object', icon: TextIcon, fields: [ defineField({ name: 'image', title: '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: 'heading', title: 'Heading', type: 'string', }), defineField({ name: 'content', title: 'Content', type: 'blockContent', }), ], preview: { select: { image: 'image', title: 'heading', }, prepare({title, image}) { return { media: image, title: title || 'Untitled portableQuote', } }, }, })
- blockContent.tsxFile β’ Action FileActions
- Adding PortableBlock to Block Content arrayBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
of: [Content
defineArrayMember({ type: 'portable{{.PascalCasePortableBlock}}' }),
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', marks: { 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, }), ], }, ], }, }), ], })
- index.tsFile β’ Action FileActions
- Import PortableBlock to schemaTypes IndexBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
import {blockContent} from './objects/blockContent'Content
import { portable{{.PascalCasePortableBlock}} } from './objects/portable-text-components/portable-text-types/portable{{.PascalCasePortableBlock}}' - Add PortableBlock to schemaTypesBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
export const schemaTypes = [Content
portable{{.PascalCasePortableBlock}},
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, ]
Base Path
frontendDetailed view
- appFolder
- componentsFolder
- portable-text-componentsFolder
- portable-typesFolder
- index.tsFile β’ Action FileActions
- Import PortableType to PortableTypes IndexBehaviour: addMarkerAboveTargetOccurrence: firstTarget:
const portableTypes = {Content
import Portable{{.PascalCasePortableBlock}} from "./Portable{{.PascalCasePortableBlock}}" - Add PortableType to PortableTypes IndexBehaviour: addMarkerBelowTargetTarget:
const portableTypes = {Content
portable{{.PascalCasePortableBlock}}: Portable{{.PascalCasePortableBlock}},
View Code
const portableTypes = { } export default portableTypes; - Portable{{.PascalCasePortableBlock}}.tsxFile
View Code
import { Image } from "next-sanity/image" import { stegaClean } from '@sanity/client/stega' import { getImageDimensions } from '@sanity/asset-utils' import { urlForImage } from '@/sanity/lib/utils' import PortableText from "@/app/components/PortableText" interface Portable{{.PascalCasePortableBlock}}Props { value: { heading: string content: any image: any } } export default function Portable{{.PascalCasePortableBlock}}({ value }: Portable{{.PascalCasePortableBlock}}Props) { return ( <div> {value.image && ( <Image width={getImageDimensions(value.image).width} height={getImageDimensions(value.image).height} alt={stegaClean(value.image?.alt) || ''} src={urlForImage(value.image)?.url() as string} /> )} {value.heading && ( <h1 className="text-2xl font-bold">{value.heading}</h1> )} {value.content && ( <PortableText value={value.content} /> )} </div> ) }
- PortableText.tsxFile β’ Action FileActions
- Import PortableTypes Index to Portable TextBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
import {PortableText, type PortableTextComponents, type PortableTextBlock} from 'next-sanity'Content
import portableTypes from './portable-text-components/portable-types'
- Add PortableType to PortableTextComponentsBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
const components: PortableTextComponents = {Content
types: { ...portableTypes, },
View Code
/** * This component uses Portable Text to render a post body. * * You can learn more about Portable Text on: * https://www.sanity.io/docs/block-content * https://github.com/portabletext/react-portabletext * https://portabletext.org/ * */ import {PortableText, type PortableTextComponents, type PortableTextBlock} from 'next-sanity' import portableTypes from './portable-text-components/portable-types' import ResolvedLink from '@/app/components/ResolvedLink' export default function CustomPortableText({ className, value, }: { className?: string value: PortableTextBlock[] }) { const components: PortableTextComponents = { types: { ...portableTypes, }, block: { h1: ({children, value}) => ( // Add an anchor to the h1 <h1 className="group relative"> {children} <a href={`#${value?._key}`} className="absolute left-0 top-0 bottom-0 -ml-6 flex items-center opacity-0 group-hover:opacity-100 transition-opacity" > <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /> </svg> </a> </h1> ), h2: ({children, value}) => { // Add an anchor to the h2 return ( <h2 className="group relative"> {children} <a href={`#${value?._key}`} className="absolute left-0 top-0 bottom-0 -ml-6 flex items-center opacity-0 group-hover:opacity-100 transition-opacity" > <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /> </svg> </a> </h2> ) }, }, marks: { link: ({children, value: link}) => { return <ResolvedLink link={link}>{children}</ResolvedLink> }, }, } return ( <div className={['prose prose-a:text-brand', className].filter(Boolean).join(' ')}> <PortableText components={components} value={value} /> </div> ) }
Raw JSON
Debug view of the fetched document
{
"_createdAt": "2025-09-19T11:47:39Z",
"_id": "98aa2e32-e147-47a6-a2d2-ed8de2a425e7",
"_rev": "3JcS3A5GjOK7pB9VbVxvon",
"_system": {
"base": {
"id": "98aa2e32-e147-47a6-a2d2-ed8de2a425e7",
"rev": "K4tdAJtZqIlru9YbYKVrcM"
}
},
"_type": "command-slug",
"_updatedAt": "2025-09-19T11:47:50Z",
"description": "Instantly scaffold a new Portable Text component across your Sanity Studio and Next.js frontendβgenerating a typed React component, wiring it into PortableText, extending the shared GROQ portableTextFields and getPortableTextQuery, creating and registering the Studio object schema, exposing it in the reusable portableText array/menu, and updating the Studio index.",
"filePaths": [
{
"id": "path-1758109429918",
"nodes": [
{
"_key": "1758110151454-1v3b9778v",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "1758109431965-lkxvfd11k",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "1758109548620-mhlaenwvf",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "1758109732800-ovgdbke3h",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "1758110289262-qkvv7l69f",
"_type": "treeNode",
"actionFile": false,
"actions": [
{
"_key": "vl8dfkjxSJsatcSY4BySaK",
"logic": {},
"mark": ""
}
],
"children": [],
"code": "import {defineField, defineType} from 'sanity'\r\nimport {TextIcon} from '@sanity/icons'\r\n\r\nexport const portable{{.PascalCasePortableBlock}} = defineType({\r\n name: 'portable{{.PascalCasePortableBlock}}',\r\n title: '{{.PortableBlock}}',\r\n type: 'object',\r\n icon: TextIcon,\r\n fields: [\r\n defineField({\r\n name: 'image',\r\n title: 'Image',\r\n type: 'image',\r\n options: {\r\n hotspot: true,\r\n aiAssist: {\r\n imageDescriptionField: 'alt',\r\n },\r\n },\r\n fields: [\r\n {\r\n name: 'alt',\r\n type: 'string',\r\n title: 'Alternative text',\r\n description: 'Important for SEO and accessibility.',\r\n validation: (rule) => {\r\n // Custom validation to ensure alt text is provided if the image is present. https://www.sanity.io/docs/validation\r\n return rule.custom((alt, context) => {\r\n if ((context.document?.coverImage as any)?.asset?._ref && !alt) {\r\n return 'Required'\r\n }\r\n return true\r\n })\r\n },\r\n },\r\n ],\r\n validation: (rule) => rule.required(),\r\n }),\r\n defineField({\r\n name: 'heading',\r\n title: 'Heading',\r\n type: 'string',\r\n }),\r\n defineField({\r\n name: 'content',\r\n title: 'Content',\r\n type: 'blockContent',\r\n }),\r\n ],\r\n preview: {\r\n select: {\r\n image: 'image',\r\n title: 'heading',\r\n \r\n },\r\n prepare({title, image}) {\r\n return {\r\n media: image,\r\n title: title || 'Untitled portableQuote',\r\n }\r\n },\r\n },\r\n})\r\n",
"id": "file-1758110289262",
"name": "portable{{.PascalPortableBlock}}.ts",
"nodeType": "file"
}
],
"code": "",
"id": "folder-1758109732800",
"name": "portable-text-types",
"nodeType": "folder"
}
],
"code": "",
"id": "folder-1758109548620",
"name": "portable-text-components",
"nodeType": "folder"
},
{
"_key": "1758109462849-a1dhia33f",
"_type": "treeNode",
"actionFile": true,
"actions": [
{
"_key": "vl8dfkjxSJsatcSY4BySdQ",
"logic": {
"behaviour": "addMarkerBelowTarget",
"content": " defineArrayMember({\r\n type: 'portable{{.PascalCasePortableBlock}}'\r\n }),",
"mark": "PORTABLE BLOCKS",
"occurrence": "first",
"target": "of: ["
},
"mark": "",
"title": "Adding PortableBlock to Block Content array"
}
],
"children": [],
"code": "import {defineArrayMember, defineType, defineField} from 'sanity'\r\n\r\n\r\n/**\r\n * This is the schema definition for the rich text fields used for\r\n * for this blog studio. When you import it in schemas.js it can be\r\n * reused in other parts of the studio with:\r\n * {\r\n * name: 'someName',\r\n * title: 'Some title',\r\n * type: 'blockContent'\r\n * }\r\n *\r\n * Learn more: https://www.sanity.io/docs/block-content\r\n */\r\nexport const blockContent = defineType({\r\n title: 'Block Content',\r\n name: 'blockContent',\r\n type: 'array',\r\n of: [\r\n defineArrayMember({\r\n type: 'block',\r\n marks: {\r\n annotations: [\r\n {\r\n name: 'link',\r\n type: 'object',\r\n title: 'Link',\r\n fields: [\r\n defineField({\r\n name: 'linkType',\r\n title: 'Link Type',\r\n type: 'string',\r\n initialValue: 'href',\r\n options: {\r\n list: [\r\n {title: 'URL', value: 'href'},\r\n {title: 'Page', value: 'page'},\r\n {title: 'Post', value: 'post'},\r\n ],\r\n layout: 'radio',\r\n },\r\n }),\r\n defineField({\r\n name: 'href',\r\n title: 'URL',\r\n type: 'url',\r\n hidden: ({parent}) => parent?.linkType !== 'href' && parent?.linkType != null,\r\n validation: (Rule) =>\r\n Rule.custom((value, context: any) => {\r\n if (context.parent?.linkType === 'href' && !value) {\r\n return 'URL is required when Link Type is URL'\r\n }\r\n return true\r\n }),\r\n }),\r\n defineField({\r\n name: 'page',\r\n title: 'Page',\r\n type: 'reference',\r\n to: [{type: 'page'}],\r\n hidden: ({parent}) => parent?.linkType !== 'page',\r\n validation: (Rule) =>\r\n Rule.custom((value, context: any) => {\r\n if (context.parent?.linkType === 'page' && !value) {\r\n return 'Page reference is required when Link Type is Page'\r\n }\r\n return true\r\n }),\r\n }),\r\n defineField({\r\n name: 'post',\r\n title: 'Post',\r\n type: 'reference',\r\n to: [{type: 'post'}],\r\n hidden: ({parent}) => parent?.linkType !== 'post',\r\n validation: (Rule) =>\r\n Rule.custom((value, context: any) => {\r\n if (context.parent?.linkType === 'post' && !value) {\r\n return 'Post reference is required when Link Type is Post'\r\n }\r\n return true\r\n }),\r\n }),\r\n defineField({\r\n name: 'openInNewTab',\r\n title: 'Open in new tab',\r\n type: 'boolean',\r\n initialValue: false,\r\n }),\r\n ],\r\n },\r\n ],\r\n },\r\n }),\r\n ],\r\n})",
"id": "file-1758109462849",
"name": "blockContent.tsx",
"nodeType": "file"
}
],
"code": "",
"id": "folder-1758109431965",
"name": "objects",
"nodeType": "folder"
},
{
"_key": "1758110177468-2svfo9uqn",
"_type": "treeNode",
"actionFile": true,
"actions": [
{
"logic": {
"behaviour": "addMarkerBelowTarget",
"content": "import { portable{{.PascalCasePortableBlock}} } from './objects/portable-text-components/portable-text-types/portable{{.PascalCasePortableBlock}}'",
"mark": "IMPORT PORTABLEBLOCKS",
"occurrence": "first",
"target": "import {blockContent} from './objects/blockContent'"
},
"mark": "",
"title": "Import PortableBlock to schemaTypes Index"
},
{
"logic": {
"behaviour": "addMarkerBelowTarget",
"content": "portable{{.PascalCasePortableBlock}},",
"mark": "PORTABLEBLOCKS",
"occurrence": "first",
"target": "export const schemaTypes = ["
},
"mark": "",
"title": "Add PortableBlock to schemaTypes"
}
],
"children": [],
"code": "import {person} from './documents/person'\r\nimport {page} from './documents/page'\r\nimport {post} from './documents/post'\r\nimport {callToAction} from './objects/callToAction'\r\nimport {infoSection} from './objects/infoSection'\r\nimport {settings} from './singletons/settings'\r\nimport {link} from './objects/link'\r\nimport {blockContent} from './objects/blockContent'\r\n\r\n// Export an array of all the schema types. This is used in the Sanity Studio configuration. https://www.sanity.io/docs/schema-types\r\n\r\nexport const schemaTypes = [\r\n // Singletons\r\n settings,\r\n // Documents\r\n page,\r\n post,\r\n person,\r\n // Objects\r\n blockContent,\r\n infoSection,\r\n callToAction,\r\n link,\r\n]\r\n",
"id": "file-1758110177468",
"name": "index.ts",
"nodeType": "file"
}
],
"code": "",
"id": "folder-1758110151454",
"name": "schemaTypes",
"nodeType": "folder"
}
],
"path": "/studio/src"
},
{
"id": "path-1758111573363-2uw6i9l5w",
"nodes": [
{
"_key": "1758114029952-6skyomi42",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "1758114044158-g3pao7lkk",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "1758115726249-139rkyblr",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "1758115748903-gybtimxk6",
"_type": "treeNode",
"actionFile": false,
"actions": [],
"children": [
{
"_key": "1758115756801-jxu1o7xxr",
"_type": "treeNode",
"actionFile": true,
"actions": [
{
"_key": "vl8dfkjxSJsatcSY4BySmi",
"logic": {
"behaviour": "addMarkerAboveTarget",
"content": "import Portable{{.PascalCasePortableBlock}} from \"./Portable{{.PascalCasePortableBlock}}\"",
"occurrence": "first",
"target": "const portableTypes = {"
},
"mark": "",
"title": "Import PortableType to PortableTypes Index"
},
{
"_key": "vl8dfkjxSJsatcSY4BySpo",
"logic": {
"behaviour": "addMarkerBelowTarget",
"content": "portable{{.PascalCasePortableBlock}}: Portable{{.PascalCasePortableBlock}},",
"target": "const portableTypes = {"
},
"mark": "",
"title": "Add PortableType to PortableTypes Index"
}
],
"children": [],
"code": "\r\n\r\nconst portableTypes = {\r\n}\r\n\r\nexport default portableTypes;",
"id": "file-1758115756801",
"name": "index.ts",
"nodeType": "file"
},
{
"_key": "1758116035646-s7soez0mj",
"_type": "treeNode",
"actionFile": false,
"actions": [
{
"_key": "vl8dfkjxSJsatcSY4BySsu",
"logic": {},
"mark": ""
}
],
"children": [],
"code": "import { Image } from \"next-sanity/image\"\r\nimport { stegaClean } from '@sanity/client/stega'\r\nimport { getImageDimensions } from '@sanity/asset-utils'\r\nimport { urlForImage } from '@/sanity/lib/utils'\r\nimport PortableText from \"@/app/components/PortableText\"\r\n\r\ninterface Portable{{.PascalCasePortableBlock}}Props {\r\n value: {\r\n heading: string\r\n content: any\r\n image: any\r\n }\r\n}\r\n\r\n\r\n\r\n\r\nexport default function Portable{{.PascalCasePortableBlock}}({ value }: Portable{{.PascalCasePortableBlock}}Props) {\r\n return (\r\n <div>\r\n {value.image && (\r\n <Image\r\n width={getImageDimensions(value.image).width}\r\n height={getImageDimensions(value.image).height}\r\n alt={stegaClean(value.image?.alt) || ''}\r\n src={urlForImage(value.image)?.url() as string}\r\n />\r\n )}\r\n {value.heading && (\r\n <h1 className=\"text-2xl font-bold\">{value.heading}</h1>\r\n )}\r\n {value.content && (\r\n <PortableText value={value.content} />\r\n )}\r\n </div>\r\n )\r\n}",
"id": "file-1758116035646",
"name": "Portable{{.PascalCasePortableBlock}}.tsx",
"nodeType": "file"
}
],
"code": "",
"id": "folder-1758115748903",
"name": "portable-types",
"nodeType": "folder"
}
],
"code": "",
"id": "folder-1758115726249",
"name": "portable-text-components",
"nodeType": "folder"
},
{
"_key": "1758114053022-0lp5lu1ki",
"_type": "treeNode",
"actionFile": true,
"actions": [
{
"_key": "vl8dfkjxSJsatcSY4BySw0",
"logic": {
"behaviour": "addMarkerBelowTarget",
"content": "import portableTypes from './portable-text-components/portable-types'",
"occurrence": "first",
"requireAbsent": "import portableTypes from './portable-text-components/portable-types'",
"target": "import {PortableText, type PortableTextComponents, type PortableTextBlock} from 'next-sanity'"
},
"mark": "",
"title": "Import PortableTypes Index to Portable Text"
},
{
"_key": "vl8dfkjxSJsatcSY4BySz6",
"logic": {
"behaviour": "addMarkerBelowTarget",
"content": " types: {\r\n ...portableTypes,\r\n },",
"occurrence": "first",
"requireAbsent": "types: {",
"target": "const components: PortableTextComponents = {"
},
"mark": "",
"title": "Add PortableType to PortableTextComponents"
}
],
"children": [],
"code": "/**\r\n * This component uses Portable Text to render a post body.\r\n *\r\n * You can learn more about Portable Text on:\r\n * https://www.sanity.io/docs/block-content\r\n * https://github.com/portabletext/react-portabletext\r\n * https://portabletext.org/\r\n *\r\n */\r\n\r\nimport {PortableText, type PortableTextComponents, type PortableTextBlock} from 'next-sanity'\r\nimport portableTypes from './portable-text-components/portable-types'\r\n\r\nimport ResolvedLink from '@/app/components/ResolvedLink'\r\n\r\nexport default function CustomPortableText({\r\n className,\r\n value,\r\n}: {\r\n className?: string\r\n value: PortableTextBlock[]\r\n}) {\r\n const components: PortableTextComponents = {\r\n types: {\r\n ...portableTypes,\r\n },\r\n block: {\r\n h1: ({children, value}) => (\r\n // Add an anchor to the h1\r\n <h1 className=\"group relative\">\r\n {children}\r\n <a\r\n href={`#${value?._key}`}\r\n className=\"absolute left-0 top-0 bottom-0 -ml-6 flex items-center opacity-0 group-hover:opacity-100 transition-opacity\"\r\n >\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n className=\"h-4 w-4\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n >\r\n <path\r\n strokeLinecap=\"round\"\r\n strokeLinejoin=\"round\"\r\n strokeWidth={2}\r\n d=\"M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1\"\r\n />\r\n </svg>\r\n </a>\r\n </h1>\r\n ),\r\n h2: ({children, value}) => {\r\n // Add an anchor to the h2\r\n return (\r\n <h2 className=\"group relative\">\r\n {children}\r\n <a\r\n href={`#${value?._key}`}\r\n className=\"absolute left-0 top-0 bottom-0 -ml-6 flex items-center opacity-0 group-hover:opacity-100 transition-opacity\"\r\n >\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n className=\"h-4 w-4\"\r\n fill=\"none\"\r\n viewBox=\"0 0 24 24\"\r\n stroke=\"currentColor\"\r\n >\r\n <path\r\n strokeLinecap=\"round\"\r\n strokeLinejoin=\"round\"\r\n strokeWidth={2}\r\n d=\"M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1\"\r\n />\r\n </svg>\r\n </a>\r\n </h2>\r\n )\r\n },\r\n },\r\n marks: {\r\n link: ({children, value: link}) => {\r\n return <ResolvedLink link={link}>{children}</ResolvedLink>\r\n },\r\n },\r\n }\r\n\r\n return (\r\n <div className={['prose prose-a:text-brand', className].filter(Boolean).join(' ')}>\r\n <PortableText components={components} value={value} />\r\n </div>\r\n )\r\n}\r\n",
"id": "file-1758114053022",
"name": "PortableText.tsx",
"nodeType": "file"
}
],
"code": "",
"id": "folder-1758114044158",
"name": "components",
"nodeType": "folder"
}
],
"code": "",
"id": "folder-1758114029952",
"name": "app",
"nodeType": "folder"
}
],
"path": "frontend"
}
],
"goals": [
{
"_key": "K4tdAJtZqIlru9YbYKVrjK",
"description": "Scaffold a presentational component that renders heading, text, and an optional button using ResolvedLink, strongly typed from sanity.types.",
"fileHints": [
"frontend/app/components/{{.PascalCasePortableBlock}}.tsx"
],
"howToTips": [],
"title": "Create React component for the new Portable Text component"
},
{
"_key": "K4tdAJtZqIlru9YbYKVrkU",
"description": "Makes the Portable Text UI available in Studio so the new Portable Text component can actually use its fields.",
"fileHints": [
"studio/src/schemaTypes/index.ts"
],
"howToTips": [
"Add the import near other object imports (use an addMarkerAboveTarget-style insertion; guard with requireAbsent).",
"Include 'portableText' in the schemaTypes array close to other objects (addMarkerAboveTarget; guard with requireAbsent)."
],
"title": "Register Portable Text component in Studio schema index"
}
],
"ignoredPatterns": [],
"slug": {
"_type": "slug",
"current": "add-portable-text-type"
},
"slugCurrent": "add-portable-text-type",
"title": "Add Portable Text Type",
"variables": [
{
"_type": "variableDefinition",
"description": "The name of the portable text component to add.",
"examples": [
"image",
"poll",
"quote",
"questionnaire"
],
"name": "PortableBlock",
"priority": 1,
"title": "PortableBlock"
}
]
}