Add Portable Text Mark

/add-portable-text-markCreated: 19 Sept 2025, 11:47Updated: 19 Sept 2025, 13:54
Variables
2
Goals
1
Path Groups
2
Nodes
13
Compact File Tree
Quick overview of planned files
Base Path
frontend
โ””โ”€ ๐Ÿ“ app
   โ””โ”€ ๐Ÿ“ components
      โ”œโ”€ ๐Ÿ“ portable-text-components
      โ”‚  โ””โ”€ ๐Ÿ“ portable-marks
      โ”‚     โ”œโ”€ ๐Ÿ“„ index.ts
      โ”‚     โ””โ”€ ๐Ÿ“„ PortableMark{{.PascalCasePortableMark}}.tsx
      โ””โ”€ ๐Ÿ“„ PortableText.tsx
Base Path
/studio/src
โ””โ”€ ๐Ÿ“ schemaTypes
   โ””โ”€ ๐Ÿ“ objects
      โ”œโ”€ ๐Ÿ“ portable-text-components
      โ”‚  โ””โ”€ ๐Ÿ“ portable-text-marks
      โ”‚     โ””โ”€ ๐Ÿ“„ PortableMark{{.PascalCasePortableMark}}Decorator.tsx
      โ””โ”€ ๐Ÿ“„ blockContent.tsx
Variables
Argument-driven inputs used by your generator
  • PortableMark
    PortableMark
    P1

    The mark/decorator name to add (inline style applied to selected text).

    highlightkbdsubsup
  • MarkIconText
    MarkIconText
    P2

    A short string shown as the toolbar icon in Studio.

    HKSubSup
Ignored Patterns
Globs/paths skipped by the executor
No ignored patterns.
Goals
What this command is trying to accomplish
  1. Wire a custom Portable Text mark across Studio & Frontend

    Adds a reusable mark (decorator) to the Studio editor with icon + inline preview, and renders it on the frontend by extending the PortableText marks map.

File Tree
Detailed view with actions
Base Path
frontend
Detailed view
  • app
    Folder
    • components
      Folder
      • portable-text-components
        Folder
        • portable-marks
          Folder
          • index.ts
            File โ€ข Action File
            Actions
            1. Import Mark component into portable-marks index
              Behaviour: addMarkerAboveTarget
              Occurrence: first
              Target: const portableMarks = {
              Content
              import PortableMark{{.PascalCasePortableMark}} from "./PortableMark{{.PascalCasePortableMark}}"
            2. Register mark in portable-marks map
              Behaviour: addMarkerBelowTarget
              Occurrence: first
              Target: const portableMarks = {
              Content
                {{.camelCasePortableMark}}: PortableMark{{.PascalCasePortableMark}},
            View Code
            
            
            const portableMarks = {
            }
            
            export default portableMarks;
          • PortableMark{{.PascalCasePortableMark}}.tsx
            File
            View Code
            import * as React from "react"
            
            export default function PortableMark{{.PascalCasePortableMark}}({ children }: { children: React.ReactNode }) {
              return (
                <span className={"pt-mark-{{.kebabCasePortableMark}}"} data-mark="{{.PortableMark}}">
                  {children}
                </span>
              )
            }
            
      • PortableText.tsx
        File โ€ข Action File
        Actions
        1. Import portable-marks into PortableText
          Behaviour: addMarkerBelowTarget
          Occurrence: first
          Target: import {PortableText, type PortableTextComponents, type PortableTextBlock} from 'next-sanity'
          Content
          import portableMarks from './portable-text-components/portable-marks'
        2. Spread custom marks into PortableText marks map
          Behaviour: addMarkerBelowTarget
          Occurrence: first
          Target: marks: {
          Content
                ...portableMarks,
        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-annotations'
        
        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>
          )
        }
        
Base Path
/studio/src
Detailed view
  • schemaTypes
    Folder
    • objects
      Folder
      • portable-text-components
        Folder
        • portable-text-marks
          Folder
          • PortableMark{{.PascalCasePortableMark}}Decorator.tsx
            File
            View Code
            import React from 'react'
            
            export default function PortableMark{{.PascalCasePortableMark}}Decorator(props: any) {
              return (
                <span style={{
                  padding: '0 0.15em',
                  borderRadius: '0.2em',
                  boxDecorationBreak: 'clone'
                }}>
                  {props.children}
                </span>
              )
            }
            
      • blockContent.tsx
        File โ€ข Action File
        Actions
        1. Import Studio mark decorator preview component
          Behaviour: addMarkerBelowTarget
          Occurrence: first
          Target: import {defineArrayMember, defineType, defineField} from 'sanity'
          Content
          import PortableMark{{.PascalCasePortableMark}}Decorator from './portable-text-components/portable-text-marks/PortableMark{{.PascalCasePortableMark}}Decorator'
        2. Create decorators list with built-ins and custom mark
          Behaviour: replaceIfMissing
          Occurrence: first
          Target: marks: {
          Content
                  decorators: [
                    {title: 'Strong', value: 'strong'},
                    {title: 'Emphasis', value: 'em'},
                    {title: 'Code', value: 'code'},
                    {title: 'Underline', value: 'underline'},
                    {title: 'Strike', value: 'strike-through'},
                    {
                      title: '{{.PortableMark}}',
                      value: '{{.camelCasePortableMark}}',
                      icon: () => '{{.MarkIconText}}',
                      component: PortableMark{{.PascalCasePortableMark}}Decorator,
                    },
                  ],
        3. Append custom mark to existing decorators (if present)
          Behaviour: addMarkerBelowTarget
          Occurrence: first
          Target: decorators: [
          Content
                    {
                      title: '{{.PortableMark}}',
                      value: '{{.camelCasePortableMark}}',
                      icon: () => '{{.MarkIconText}}',
                      component: PortableMark{{.PascalCasePortableMark}}Decorator,
                    },
        View Code
        import {defineArrayMember, defineType, defineField} from 'sanity'
        
        export const blockContent = defineType({
          title: 'Block Content',
          name: 'blockContent',
          type: 'array',
          of: [
            defineArrayMember({
              type: 'block',
              marks: {
              }
            })
          ]
        })
Raw JSON
Debug view of the fetched document
{
  "_createdAt": "2025-09-19T11:47:12Z",
  "_id": "9d52d861-1d75-466f-ac9f-6d79f1b861c3",
  "_rev": "oX2XKfmSw1CZillJfDFbg3",
  "_system": {
    "base": {
      "id": "9d52d861-1d75-466f-ac9f-6d79f1b861c3",
      "rev": "oX2XKfmSw1CZillJfDFagb"
    }
  },
  "_type": "command-slug",
  "_updatedAt": "2025-09-19T13:54:22Z",
  "description": "A custom inline style you apply to text (e.g., highlight, bold, italic).",
  "filePaths": [
    {
      "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": "K4tdAJtZqIlru9YbYKflaI",
                              "logic": {
                                "behaviour": "addMarkerAboveTarget",
                                "content": "import PortableMark{{.PascalCasePortableMark}} from \"./PortableMark{{.PascalCasePortableMark}}\"",
                                "occurrence": "first",
                                "requireAbsent": "Mark{{.PascalCasePortableMark}}",
                                "target": "const portableMarks = {"
                              },
                              "mark": "",
                              "title": "Import Mark component into portable-marks index"
                            },
                            {
                              "_key": "K4tdAJtZqIlru9YbYKflbS",
                              "logic": {
                                "behaviour": "addMarkerBelowTarget",
                                "content": "  {{.camelCasePortableMark}}: PortableMark{{.PascalCasePortableMark}},",
                                "occurrence": "first",
                                "requireAbsent": "{{.camelCasePortableMark}}:",
                                "target": "const portableMarks = {"
                              },
                              "mark": "",
                              "title": "Register mark in portable-marks map"
                            }
                          ],
                          "children": [],
                          "code": "\n\nconst portableMarks = {\n}\n\nexport default portableMarks;",
                          "id": "file-1758115756801",
                          "name": "index.ts",
                          "nodeType": "file"
                        },
                        {
                          "_key": "1758116035646-s7soez0mj",
                          "_type": "treeNode",
                          "actionFile": false,
                          "actions": [
                            {
                              "_key": "K4tdAJtZqIlru9YbYKflcc",
                              "logic": {},
                              "mark": ""
                            }
                          ],
                          "children": [],
                          "code": "import * as React from \"react\"\n\nexport default function PortableMark{{.PascalCasePortableMark}}({ children }: { children: React.ReactNode }) {\n  return (\n    <span className={\"pt-mark-{{.kebabCasePortableMark}}\"} data-mark=\"{{.PortableMark}}\">\n      {children}\n    </span>\n  )\n}\n",
                          "id": "file-1758116035646",
                          "name": "PortableMark{{.PascalCasePortableMark}}.tsx",
                          "nodeType": "file"
                        }
                      ],
                      "code": "",
                      "id": "folder-1758115748903",
                      "name": "portable-marks",
                      "nodeType": "folder"
                    }
                  ],
                  "code": "",
                  "id": "folder-1758115726249",
                  "name": "portable-text-components",
                  "nodeType": "folder"
                },
                {
                  "_key": "1758114053022-0lp5lu1ki",
                  "_type": "treeNode",
                  "actionFile": true,
                  "actions": [
                    {
                      "_key": "K4tdAJtZqIlru9YbYKfldm",
                      "logic": {
                        "behaviour": "addMarkerBelowTarget",
                        "content": "import portableMarks from './portable-text-components/portable-marks'",
                        "occurrence": "first",
                        "requireAbsent": "import portableMarks from './portable-text-components/portable-marks'",
                        "target": "import {PortableText, type PortableTextComponents, type PortableTextBlock} from 'next-sanity'"
                      },
                      "mark": "",
                      "title": "Import portable-marks into PortableText"
                    },
                    {
                      "_key": "K4tdAJtZqIlru9YbYKflew",
                      "logic": {
                        "behaviour": "addMarkerBelowTarget",
                        "content": "      ...portableMarks,",
                        "occurrence": "first",
                        "requireAbsent": "...portableMarks",
                        "target": "marks: {"
                      },
                      "mark": "",
                      "title": "Spread custom marks into PortableText marks map"
                    }
                  ],
                  "children": [],
                  "code": "/**\n * This component uses Portable Text to render a post body.\n *\n * You can learn more about Portable Text on:\n * https://www.sanity.io/docs/block-content\n * https://github.com/portabletext/react-portabletext\n * https://portabletext.org/\n *\n */\n\nimport {PortableText, type PortableTextComponents, type PortableTextBlock} from 'next-sanity'\nimport portableTypes from './portable-text-components/portable-annotations'\n\nimport ResolvedLink from '@/app/components/ResolvedLink'\n\nexport default function CustomPortableText({\n  className,\n  value,\n}: {\n  className?: string\n  value: PortableTextBlock[]\n}) {\n  const components: PortableTextComponents = {\n    types: {\n      ...portableTypes,\n    },\n    block: {\n      h1: ({children, value}) => (\n        // Add an anchor to the h1\n        <h1 className=\"group relative\">\n          {children}\n          <a\n            href={`#${value?._key}`}\n            className=\"absolute left-0 top-0 bottom-0 -ml-6 flex items-center opacity-0 group-hover:opacity-100 transition-opacity\"\n          >\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              className=\"h-4 w-4\"\n              fill=\"none\"\n              viewBox=\"0 0 24 24\"\n              stroke=\"currentColor\"\n            >\n              <path\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth={2}\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\"\n              />\n            </svg>\n          </a>\n        </h1>\n      ),\n      h2: ({children, value}) => {\n        // Add an anchor to the h2\n        return (\n          <h2 className=\"group relative\">\n            {children}\n            <a\n              href={`#${value?._key}`}\n              className=\"absolute left-0 top-0 bottom-0 -ml-6 flex items-center opacity-0 group-hover:opacity-100 transition-opacity\"\n            >\n              <svg\n                xmlns=\"http://www.w3.org/2000/svg\"\n                className=\"h-4 w-4\"\n                fill=\"none\"\n                viewBox=\"0 0 24 24\"\n                stroke=\"currentColor\"\n              >\n                <path\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  strokeWidth={2}\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\"\n                />\n              </svg>\n            </a>\n          </h2>\n        )\n      },\n    },\n    marks: {\n      link: ({children, value: link}) => {\n        return <ResolvedLink link={link}>{children}</ResolvedLink>\n      },\n    },\n  }\n\n  return (\n    <div className={['prose prose-a:text-brand', className].filter(Boolean).join(' ')}>\n      <PortableText components={components} value={value} />\n    </div>\n  )\n}\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"
    },
    {
      "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": "K4tdAJtZqIlru9YbYKflg6",
                              "logic": {},
                              "mark": ""
                            }
                          ],
                          "children": [],
                          "code": "import React from 'react'\n\nexport default function PortableMark{{.PascalCasePortableMark}}Decorator(props: any) {\n  return (\n    <span style={{\n      padding: '0 0.15em',\n      borderRadius: '0.2em',\n      boxDecorationBreak: 'clone'\n    }}>\n      {props.children}\n    </span>\n  )\n}\n",
                          "id": "file-1758110289262",
                          "name": "PortableMark{{.PascalCasePortableMark}}Decorator.tsx",
                          "nodeType": "file"
                        }
                      ],
                      "code": "",
                      "id": "folder-1758109732800",
                      "name": "portable-text-marks",
                      "nodeType": "folder"
                    }
                  ],
                  "code": "",
                  "id": "folder-1758109548620",
                  "name": "portable-text-components",
                  "nodeType": "folder"
                },
                {
                  "_key": "1758109462849-a1dhia33f",
                  "_type": "treeNode",
                  "actionFile": true,
                  "actions": [
                    {
                      "_key": "K4tdAJtZqIlru9YbYKflhG",
                      "logic": {
                        "behaviour": "addMarkerBelowTarget",
                        "content": "import PortableMark{{.PascalCasePortableMark}}Decorator from './portable-text-components/portable-text-marks/PortableMark{{.PascalCasePortableMark}}Decorator'",
                        "occurrence": "first",
                        "requireAbsent": "",
                        "target": "import {defineArrayMember, defineType, defineField} from 'sanity'"
                      },
                      "mark": "",
                      "title": "Import Studio mark decorator preview component"
                    },
                    {
                      "_key": "K4tdAJtZqIlru9YbYKfliQ",
                      "logic": {
                        "behaviour": "replaceIfMissing",
                        "content": "        decorators: [\n          {title: 'Strong', value: 'strong'},\n          {title: 'Emphasis', value: 'em'},\n          {title: 'Code', value: 'code'},\n          {title: 'Underline', value: 'underline'},\n          {title: 'Strike', value: 'strike-through'},\n          {\n            title: '{{.PortableMark}}',\n            value: '{{.camelCasePortableMark}}',\n            icon: () => '{{.MarkIconText}}',\n            component: PortableMark{{.PascalCasePortableMark}}Decorator,\n          },\n        ],",
                        "mark": "",
                        "occurrence": "first",
                        "replacement": "marks: {\n    decorators: [\n          {\n            title: '{{.PortableMark}}',\n            value: '{{.camelCasePortableMark}}',\n            icon: () => '{{.MarkIconText}}',\n            component: PortableMark{{.PascalCasePortableMark}}Decorator,\n          },\n          {title: 'Strong', value: 'strong'},\n          {title: 'Emphasis', value: 'em'},\n          {title: 'Code', value: 'code'},\n          {title: 'Underline', value: 'underline'},\n          {title: 'Strike', value: 'strike-through'},\n        ],",
                        "requireAbsent": "decorators: [",
                        "target": "marks: {",
                        "targetStart": "marks: {"
                      },
                      "mark": "",
                      "title": "Create decorators list with built-ins and custom mark"
                    },
                    {
                      "_key": "K4tdAJtZqIlru9YbYKflja",
                      "logic": {
                        "behaviour": "addMarkerBelowTarget",
                        "content": "          {\n            title: '{{.PortableMark}}',\n            value: '{{.camelCasePortableMark}}',\n            icon: () => '{{.MarkIconText}}',\n            component: PortableMark{{.PascalCasePortableMark}}Decorator,\n          },",
                        "occurrence": "first",
                        "requireAbsent": "value: '{{.camelCasePortableMark}}'",
                        "target": "decorators: ["
                      },
                      "mark": "",
                      "title": "Append custom mark to existing decorators (if present)"
                    }
                  ],
                  "children": [],
                  "code": "import {defineArrayMember, defineType, defineField} from 'sanity'\n\nexport const blockContent = defineType({\n  title: 'Block Content',\n  name: 'blockContent',\n  type: 'array',\n  of: [\n    defineArrayMember({\n      type: 'block',\n      marks: {\n      }\n    })\n  ]\n})",
                  "id": "file-1758109462849",
                  "name": "blockContent.tsx",
                  "nodeType": "file"
                }
              ],
              "code": "",
              "id": "folder-1758109431965",
              "name": "objects",
              "nodeType": "folder"
            }
          ],
          "code": "",
          "id": "folder-1758110151454",
          "name": "schemaTypes",
          "nodeType": "folder"
        }
      ],
      "path": "/studio/src"
    }
  ],
  "goals": [
    {
      "_key": "772f44cf9752f92e26d3b6684223fc0b",
      "description": "Adds a reusable mark (decorator) to the Studio editor with icon + inline preview, and renders it on the frontend by extending the PortableText marks map.",
      "fileHints": [],
      "howToTips": [],
      "title": "Wire a custom Portable Text mark across Studio & Frontend"
    }
  ],
  "ignoredPatterns": [],
  "slug": {
    "_type": "slug",
    "current": "add-portable-text-mark"
  },
  "slugCurrent": "add-portable-text-mark",
  "title": "Add Portable Text Mark",
  "variables": [
    {
      "_type": "variableDefinition",
      "description": "The mark/decorator name to add (inline style applied to selected text).",
      "examples": [
        "highlight",
        "kbd",
        "sub",
        "sup"
      ],
      "name": "PortableMark",
      "priority": 1,
      "title": "PortableMark"
    },
    {
      "_type": "variableDefinition",
      "description": "A short string shown as the toolbar icon in Studio.",
      "examples": [
        "H",
        "K",
        "Sub",
        "Sup"
      ],
      "name": "MarkIconText",
      "priority": 2,
      "title": "MarkIconText"
    }
  ]
}