Discussion about setting a readOnly or hidden field using document actions in Sanity.io

12 replies
Last updated: Mar 7, 2021
Is there a way to automatically set a readOnly or hidden field? I was hoping that I could create a slug field that would be generated from a number field using the
source
and
slufigy
options but it doesn't get created automatically set to a value upon publish.
AI Update

The slug field's source and slugify options don't automatically generate the slug value on publish - they only power the "Generate" button in the Studio UI. However, you can automatically set a slug (or any readOnly/hidden field) on publish using a custom document action.

Here's the correct way to implement this in Studio v3:

Create a Custom Publish Action

First, create your custom action component (e.g., actions/SetSlugAndPublish.tsx):

import { useDocumentOperation } from 'sanity'
import { useState, useEffect } from 'react'
import type { DocumentActionComponent } from 'sanity'

function generateSlug(text: string | number): string {
  return String(text)
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/[^\w-]+/g, '')
}

export const SetSlugAndPublishAction: DocumentActionComponent = (props) => {
  const { patch, publish } = useDocumentOperation(props.id, props.type)
  const [isPublishing, setIsPublishing] = useState(false)

  useEffect(() => {
    // Reset publishing state if publish completes
    if (isPublishing && !publish.disabled) {
      setIsPublishing(false)
    }
  }, [isPublishing, publish.disabled])

  return {
    label: isPublishing ? 'Publishing...' : 'Publish',
    disabled: publish.disabled || isPublishing,
    onHandle: () => {
      setIsPublishing(true)
      
      // Generate slug from your source field(s)
      // For a number field:
      const slug = generateSlug(props.draft?.yourNumberField || '')
      
      // Patch the document with the slug
      patch.execute([{ set: { slug: { _type: 'slug', current: slug } } }])
      
      // Then publish
      publish.execute()
      
      props.onComplete()
    }
  }
}

Register the Action in Your Config

In your sanity.config.ts (or .js):

import { defineConfig } from 'sanity'
import { SetSlugAndPublishAction } from './actions/SetSlugAndPublish'

export default defineConfig({
  // ... other config
  document: {
    actions: (prev, context) => {
      // Replace the default publish action for specific document types
      if (context.schemaType === 'yourDocumentType') {
        return prev.map((originalAction) => 
          originalAction.action === 'publish' 
            ? SetSlugAndPublishAction 
            : originalAction
        )
      }
      return prev
    }
  }
})

Key Points for Studio v3

Important differences from older patterns:

  1. Import from sanity package: Use import { useDocumentOperation } from 'sanity' - NOT from @sanity/react-hooks (deprecated)

  2. Return an action description object: Your component must return an object with label, onHandle, and optionally disabled, icon, shortcut, etc. - NOT a React component

  3. Use the callback pattern: In defineConfig, use document.actions: (prev, context) => [...] to access context and modify the actions array

  4. For multiple source fields: You can combine fields like this:

    const slug = generateSlug(`${props.draft?.field1} ${props.draft?.field2}`)
  5. For number fields specifically: Just convert to string:

    const slug = generateSlug(String(props.draft?.yourNumberField))

Alternative: Using Initial Value Templates

If you want the slug set when the document is created (not on publish), you can use Initial Value Templates instead:

import { defineConfig } from 'sanity'

export default defineConfig({
  // ... other config
  schema: {
    templates: (prev) => [
      ...prev,
      {
        id: 'document-with-slug',
        title: 'Document with Slug',
        schemaType: 'yourDocumentType',
        value: (params) => ({
          slug: {
            _type: 'slug',
            current: generateSlug(params.yourNumberField || 'default')
          }
        })
      }
    ]
  }
})

This approach works great for hidden or readOnly fields that should be set automatically based on other field values when publishing or creating documents.

Could you build a custom input field which renders a hidden field and use a document action to set the field on publish? https://www.sanity.io/docs/document-actions#362c883e4421
Thank you for the link,
user L
. It seems that the document action strategy will work for what I need. I don't know about the custom input field rendering a hidden field, but what I am hoping to accomplish with the document action is to create and set a slug field upon publish using required fields in the document as input variables.
Also, I seem to be having difficulty locating the part of the documentation that directs you where to place your actions. I'm not sure what folder I would put
setAndPublishAction.js
into or load it from (
schema.js
?).

I think I'm figuring it out.
I did not figure out how to incorporate the example action in that link, unfortunately. Anyone who can help point me in the right direction?
Possibly you didn’t make the sanity.json changes? Here’s a gist that given a schema with a sourceField and slug field, both set a as string will populate the slug field on publish https://gist.github.com/raffij/4a42175eb8c5f146b1d8a806787d9511
Thank you,
user L
, I did add the sanity.json code that was in the example but it crashed the editor in studio. I will try out your gist and see if I can get something working from there. I really appreciate your help!
user L
I added the code but It's still crashing the studio when I try to edit/create a new document.
TypeError: hook is not a function
    at HookStateContainer (/static/js/app.bundle.js:282315:19)
    at renderWithHooks (/static/js/vendor.bundle.js:17524:18)
    at mountIndeterminateComponent (/static/js/vendor.bundle.js:20203:13)
    at beginWork (/static/js/vendor.bundle.js:21317:16)
    at HTMLUnknownElement.callCallback (/static/js/vendor.bundle.js:2909:14)
    at Object.invokeGuardedCallbackDev (/static/js/vendor.bundle.js:2958:16)
    at invokeGuardedCallback (/static/js/vendor.bundle.js:3013:31)
    at beginWork$1 (/static/js/vendor.bundle.js:25924:7)
    at performUnitOfWork (/static/js/vendor.bundle.js:24875:12)
    at workLoopSync (/static/js/vendor.bundle.js:24851:22)
Sorry can’t offer any assitance.
Oy!

user L
I found the issue in the code you provided and was able to see it working in action. There was a missing
module.exports =
in the SetSlugAndPublishAction.js file.
I honestly don't see a difference between what I've done and the code you provided, outside of the file structure. I was putting my files into the path
./schema/
instead of
./lib/
like your code. Does file structure make a difference in sanity?
It's because I copied it from 1 file into many and didn't think about that.
No worries! I'm grateful for your help!
Your first response is what is leading me to my solution, the code example you provided helped me resolve my issues and get me moving. It turns out the only difference between the code I got from that documentation link and the code you provided was the folder structure? Weird!

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?