
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeThe 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:
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()
}
}
}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
}
}
})Important differences from older patterns:
Import from sanity package: Use import { useDocumentOperation } from 'sanity' - NOT from @sanity/react-hooks (deprecated)
Return an action description object: Your component must return an object with label, onHandle, and optionally disabled, icon, shortcut, etc. - NOT a React component
Use the callback pattern: In defineConfig, use document.actions: (prev, context) => [...] to access context and modify the actions array
For 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))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 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.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store