Implementing Singleton Document Internationalization in Sanity Structure

8 replies
Last updated: Jan 29, 2026
Help me implement this: https://github.com/sanity-io/document-internationalization/blob/main/docs/known-caveats.md#usage-with-singletons
If I add that code to

export default () => S.list()
  .id('__root__')
  .title('Content')
  ..items([
    [add here?], <---- Throws error! Expected List item, found Object
    S.divider(),
    ...restOfTypes
  ])
AI Update

The URL you're trying to follow (known-caveats.md) doesn't actually exist in the document-internationalization repository—it returns a 404 error. However, the actual documentation for implementing singletons is in 01-singleton-documents.md.

The error "Expected List item, found Object" happens because you're trying to spread a single object directly into the items array. The fix is to use the spread operator (...) on the array returned by .map(), not on individual objects.

Here's the correct implementation from the actual singleton documentation:

import {StructureResolver} from 'sanity/structure'

// Define your singleton documents
const SINGLETONS = [
  {id: 'home', _type: 'page', title: 'Home'},
  {id: 'login', _type: 'page', title: 'Login'},
]

// Define your languages
const LANGUAGES = [
  {id: 'en', title: 'English'},
  {id: 'fr', title: 'French'},
]

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Content')
    .items([
      // Spread the array returned by .map() - this is the key!
      ...SINGLETONS.map((singleton) =>
        S.listItem()
          .title(singleton.title)
          .id(singleton.id)
          .child(
            S.list()
              .title(singleton.title)
              .id(singleton.id)
              .items(
                LANGUAGES.map((language) =>
                  S.documentListItem()
                    .schemaType('page')
                    .id(`${singleton.id}-${language.id}`)
                    .title(`${singleton.title} (${language.id.toLocaleUpperCase()})`)
                )
              )
              .canHandleIntent(
                (intentName, params) => 
                  intentName === 'edit' && params.id.startsWith(singleton.id)
              )
          )
      ),
      S.divider(),
      // ...other list items
    ])

Why this works:

  1. SINGLETONS.map() returns an array of S.listItem() objects
  2. The spread operator ... unpacks that array into individual items
  3. Each singleton becomes a nested list showing all language versions

Before this will work, you need to:

  1. Create the actual singleton documents using the createSingletons.ts script. This creates both the language-specific documents (like home-en, home-fr) and the translation.metadata document that binds them together.

  2. Add a language field to your schema (usually hidden):

defineField({
  name: 'language',
  type: 'string',
  readOnly: true,
  hidden: true,
})

The canHandleIntent ensures that when you click on a singleton document from elsewhere in the Studio, it opens in the correct place in your structure.

Show original thread
8 replies

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?