Add Portable Text Type

/add-portable-text-typeCreated: 19 Sept 2025, 11:47Updated: 19 Sept 2025, 11:47
Variables
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.ts
Base Path
frontend
└─ πŸ“ app
   └─ πŸ“ components
      β”œβ”€ πŸ“ portable-text-components
      β”‚  └─ πŸ“ portable-types
      β”‚     β”œβ”€ πŸ“„ index.ts
      β”‚     └─ πŸ“„ Portable{{.PascalCasePortableBlock}}.tsx
      └─ πŸ“„ PortableText.tsx
Variables
Argument-driven inputs used by your generator
  • PortableBlock
    PortableBlock
    P1

    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
  1. Create React component for the new Portable Text component
    frontend/app/components/{{.PascalCasePortableBlock}}.tsx

    Scaffold a presentational component that renders heading, text, and an optional button using ResolvedLink, strongly typed from sanity.types.

  2. Register Portable Text component in Studio schema index
    studio/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/src
Detailed view
  • schemaTypes
    Folder
    • objects
      Folder
      • portable-text-components
        Folder
        • portable-text-types
          Folder
          • portable{{.PascalPortableBlock}}.ts
            File
            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.tsx
        File β€’ Action File
        Actions
        1. Adding PortableBlock to Block Content array
          Behaviour: addMarkerBelowTarget
          Occurrence: first
          Target: 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.ts
      File β€’ Action File
      Actions
      1. Import PortableBlock to schemaTypes Index
        Behaviour: addMarkerBelowTarget
        Occurrence: first
        Target: import {blockContent} from './objects/blockContent'
        Content
        import { portable{{.PascalCasePortableBlock}} } from './objects/portable-text-components/portable-text-types/portable{{.PascalCasePortableBlock}}'
      2. Add PortableBlock to schemaTypes
        Behaviour: addMarkerBelowTarget
        Occurrence: first
        Target: 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
frontend
Detailed view
  • app
    Folder
    • components
      Folder
      • portable-text-components
        Folder
        • portable-types
          Folder
          • index.ts
            File β€’ Action File
            Actions
            1. Import PortableType to PortableTypes Index
              Behaviour: addMarkerAboveTarget
              Occurrence: first
              Target: const portableTypes = {
              Content
              import Portable{{.PascalCasePortableBlock}} from "./Portable{{.PascalCasePortableBlock}}"
            2. Add PortableType to PortableTypes Index
              Behaviour: addMarkerBelowTarget
              Target: const portableTypes = {
              Content
              portable{{.PascalCasePortableBlock}}: Portable{{.PascalCasePortableBlock}},
            View Code
            
            const portableTypes = {
            }
            
            export default portableTypes;
          • Portable{{.PascalCasePortableBlock}}.tsx
            File
            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.tsx
        File β€’ Action File
        Actions
        1. Import PortableTypes Index to Portable Text
          Behaviour: addMarkerBelowTarget
          Occurrence: first
          Target: import {PortableText, type PortableTextComponents, type PortableTextBlock} from 'next-sanity'
          Content
          import portableTypes from './portable-text-components/portable-types'
        2. Add PortableType to PortableTextComponents
          Behaviour: addMarkerBelowTarget
          Occurrence: first
          Target: 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"
    }
  ]
}