Discussion about setting a readOnly or hidden field using document actions in Sanity.io
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:
Import from
sanitypackage: Useimport { useDocumentOperation } from 'sanity'- NOT from@sanity/react-hooks(deprecated)Return an action description object: Your component must return an object with
label,onHandle, and optionallydisabled,icon,shortcut, etc. - NOT a React componentUse the callback pattern: In
defineConfig, usedocument.actions: (prev, context) => [...]to access context and modify the actions arrayFor multiple source fields: You can combine fields like this:
const slug = generateSlug(`${props.draft?.field1} ${props.draft?.field2}`)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.
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.