Add Portable Text Mark
/add-portable-text-markCreated: 19 Sept 2025, 11:47Updated: 19 Sept 2025, 13:54Variables
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.tsxBase Path
/studio/srcโโ ๐ schemaTypes
โโ ๐ objects
โโ ๐ portable-text-components
โ โโ ๐ portable-text-marks
โ โโ ๐ PortableMark{{.PascalCasePortableMark}}Decorator.tsx
โโ ๐ blockContent.tsxVariables
Argument-driven inputs used by your generator
- P1PortableMarkPortableMark
The mark/decorator name to add (inline style applied to selected text).
highlightkbdsubsup - P2MarkIconTextMarkIconText
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
- 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
frontendDetailed view
- appFolder
- componentsFolder
- portable-text-componentsFolder
- portable-marksFolder
- index.tsFile โข Action FileActions
- Import Mark component into portable-marks indexBehaviour: addMarkerAboveTargetOccurrence: firstTarget:
const portableMarks = {Content
import PortableMark{{.PascalCasePortableMark}} from "./PortableMark{{.PascalCasePortableMark}}" - Register mark in portable-marks mapBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
const portableMarks = {Content
{{.camelCasePortableMark}}: PortableMark{{.PascalCasePortableMark}},
View Code
const portableMarks = { } export default portableMarks; - PortableMark{{.PascalCasePortableMark}}.tsxFile
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.tsxFile โข Action FileActions
- Import portable-marks into PortableTextBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
import {PortableText, type PortableTextComponents, type PortableTextBlock} from 'next-sanity'Content
import portableMarks from './portable-text-components/portable-marks'
- Spread custom marks into PortableText marks mapBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
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/srcDetailed view
- schemaTypesFolder
- objectsFolder
- portable-text-componentsFolder
- portable-text-marksFolder
- PortableMark{{.PascalCasePortableMark}}Decorator.tsxFile
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.tsxFile โข Action FileActions
- Import Studio mark decorator preview componentBehaviour: addMarkerBelowTargetOccurrence: firstTarget:
import {defineArrayMember, defineType, defineField} from 'sanity'Content
import PortableMark{{.PascalCasePortableMark}}Decorator from './portable-text-components/portable-text-marks/PortableMark{{.PascalCasePortableMark}}Decorator' - Create decorators list with built-ins and custom markBehaviour: replaceIfMissingOccurrence: firstTarget:
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, }, ], - Append custom mark to existing decorators (if present)Behaviour: addMarkerBelowTargetOccurrence: firstTarget:
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"
}
]
}