Convert comma-separated string to multiple tag references in Sanity array

12 replies
Last updated: Dec 29, 2022
Hello, I’m new to Sanity, I’ve created a Tag type that includes a slug and a title, then in the Post type I’ve added this field:

{
      name: 'tags',
      title: 'Tags',
      type: 'array',
      of: [{type: 'reference', to: {type: 'tag'}}]
    }
It works but I have to add / select tags one by one, is there any way to convert a comma-separated string to multiple tags (they are objects and it’s not a string array).

Thanks
AI Update

Great question! Yes, you can definitely create a better UX for adding multiple tags at once. You'll need to build a custom input component that accepts comma-separated input and converts it to tag references.

Here's a working approach:

import {set, unset, insert, setIfMissing} from 'sanity'
import {Stack, TextInput, Button, Card, Text, Flex} from '@sanity/ui'
import {useCallback, useState} from 'react'
import {useClient} from 'sanity'

function TagsInput(props) {
  const {value = [], onChange, schemaType} = props
  const [inputValue, setInputValue] = useState('')
  const client = useClient({apiVersion: '2023-01-01'})
  
  const handleAddTags = useCallback(async () => {
    // Split by comma and clean up
    const tagNames = inputValue
      .split(',')
      .map(t => t.trim())
      .filter(Boolean)
    
    if (tagNames.length === 0) return
    
    // Create or fetch tag documents
    const tagRefs = await Promise.all(
      tagNames.map(async (tagName) => {
        const slug = tagName.toLowerCase().replace(/\s+/g, '-')
        
        // Check if tag already exists
        const existingTag = await client.fetch(
          `*[_type == "tag" && slug.current == $slug][0]`,
          {slug}
        )
        
        if (existingTag) {
          return {
            _type: 'reference',
            _ref: existingTag._id,
            _key: `tag-${Date.now()}-${Math.random()}`
          }
        }
        
        // Create new tag document
        const newTag = await client.create({
          _type: 'tag',
          title: tagName,
          slug: {_type: 'slug', current: slug}
        })
        
        return {
          _type: 'reference',
          _ref: newTag._id,
          _key: `tag-${Date.now()}-${Math.random()}`
        }
      })
    )
    
    // Merge with existing tags
    const newValue = [...(value || []), ...tagRefs]
    onChange(set(newValue))
    setInputValue('')
  }, [inputValue, value, onChange, client])
  
  return (
    <Stack space={3}>
      <Flex gap={2}>
        <Card flex={1}>
          <TextInput
            value={inputValue}
            onChange={(e) => setInputValue(e.currentTarget.value)}
            placeholder="Enter tags separated by commas"
            onKeyDown={(e) => {
              if (e.key === 'Enter') {
                e.preventDefault()
                handleAddTags()
              }
            }}
          />
        </Card>
        <Button onClick={handleAddTags} text="Add Tags" tone="primary" />
      </Flex>
      
      {value && value.length > 0 && (
        <Text size={1} muted>
          {value.length} tag{value.length !== 1 ? 's' : ''} selected
        </Text>
      )}
      
      {/* Render the default array input below */}
      {props.renderDefault(props)}
    </Stack>
  )
}

// In your schema:
export default {
  name: 'post',
  type: 'document',
  fields: [
    {
      name: 'tags',
      title: 'Tags',
      type: 'array',
      of: [{type: 'reference', to: {type: 'tag'}}],
      components: {
        input: TagsInput
      }
    }
  ]
}

How this works:

  1. The custom input provides a text field where you can enter comma-separated tag names
  2. When you click "Add Tags" (or press Enter), it splits the string and processes each tag
  3. For each tag name, it checks if a tag document with that slug already exists
  4. If it exists, it creates a reference to it; if not, it creates a new tag document first
  5. All the new references are added to your existing array using the set() patch operation
  6. The renderDefault(props) call at the end shows the standard array input below your custom input, so you can still manage tags individually if needed

Key points:

  • Uses useClient() to access Sanity's client for creating/fetching tag documents
  • Generates unique _key values for each reference (required for array items)
  • Automatically creates slugs from tag titles
  • Maintains your existing reference-based structure
  • You can remove the renderDefault() line if you want only the comma-separated input

This gives you the best of both worlds - quick bulk tag entry while keeping your clean reference-based data structure intact!

Show original thread
12 replies
Hey
user E
! If I'm understanding correctly, what you'll need to do is run a mutation. How does the comma separated string relate to your tags? Are they titles or slugs for the tags?
Hello
user M
, they will be title generally, if one doesn’t exist it would be good if it’s created automatically
Got it! You'd have to write a migration script to populate them then. Here's the basics of what you'd need to do:
const query = `{
  "tags: *[_type == "tag"]{_id, title},
  "posts": *[_type == "post"]
}`

//fetch all tag and post documents
const {tags, posts} = await client.fetch(query)

//loop through all posts
posts.forEach(post => {
  //split the string of tag titles you have and return an array of refs instead
  const tagRefs = tagString.split(',').map(title => {
    const tag = tags.find(tag => tag.title === title)
    return {_type: 'reference', _ref: tag._id}
  })

 //patch the post with the new array of tag refs
  client.patch(post._id).set({tags: tagRefs}).commit({autoGenerateArrayKeys: true,})
})
Depending on what version of the Studio you're using, you'd need to
import and configure a client. If you have a large number of documents, you'd also need to work in batches to make sure you don't hit the API rate limit . Finally, you'd execute the script
sanity exec ./path/to/script --with-user-token
thanks, that cannot then be done in real time and I would also have to “flag” them so I can only select the new (or edited) ones and avoid that ones that have been already tagged
is there another way you suggest?
also, is there a simpler way just to use an easier UI component that make it easy to type them one after the other and select the correct one? At the moment the interface is good for small selections but slow for things like that
I’m using Studio 3
also, what do you suggest for full text search of content?
If you need to run it in real time, then you'd use a client configured to use a token. This functionality doesn't exist natively in the Studio, you'd need to set it up as a custom component or plugin (which is much easier than it initially seems, I promise!).
If you search our docs for 'full text search' you'll find the docs you're looking for.
cool, thanks for the help
🙂
You're welcome!

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?