Type something to search...
Creating Singleton Documents in Sanity

Creating Singleton Documents in Sanity

A singleton document in Sanity is a document type that should only ever have one instance, such as site settings, a homepage or a navigation menu. You create one by defining a normal document type, giving it a fixed document ID, and making the Studio open that one document instead of a list. In current Studio versions (6.17.0 and later) you register it with document.singletons in sanity.config.ts and add it to your structure with S.listItem().singleton('siteSettings'). Studio then hides the type from the default lists, removes the duplicate action and keeps it out of create menus. In older versions, you wire the same behaviour up by hand with the Structure Builder, template filtering and document actions.

Without singletons, editors can create three "Site Settings" documents and your front end has to guess which one is real. With them, there's exactly one, always at the same ID, which you can fetch with a simple query. This article covers when to use singletons, the modern document.singletons setup, the manual approach for older Studios, querying singletons from a Next.js front end, and common singleton designs.

When to Use a Singleton

Singletons suit content that is global or one of a kind:

  • Site settings: site title, description, default social image, analytics IDs.
  • Navigation: header and footer menus.
  • Homepage: when the homepage has a unique structure no other page shares.
  • Global banners: a site-wide announcement bar.
  • Page-specific settings: the intro text for a blog index page.

They're not a good fit for anything you might need more than one of later. If you could plausibly have a second "landing page", model it as a normal type with a slug. The wider trade-offs are covered in content modeling best practices in Sanity.

Step 1: Define the Schema Type

A singleton starts as an ordinary document type:

// schemaTypes/siteSettings.ts
import { defineArrayMember, defineField, defineType } from 'sanity'
import { CogIcon } from '@sanity/icons'

export const siteSettings = defineType({
  name: 'siteSettings',
  title: 'Site settings',
  type: 'document',
  icon: CogIcon,
  fields: [
    defineField({
      name: 'title',
      type: 'string',
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'description',
      type: 'text',
      rows: 3,
      validation: (rule) => rule.max(160),
    }),
    defineField({
      name: 'ogImage',
      title: 'Default social image',
      type: 'image',
      fields: [defineField({ name: 'alt', type: 'string', title: 'Alt text' })],
    }),
    defineField({
      name: 'mainNav',
      title: 'Main navigation',
      type: 'array',
      of: [
        defineArrayMember({
          type: 'object',
          name: 'navItem',
          fields: [
            defineField({ name: 'label', type: 'string' }),
            defineField({ name: 'page', type: 'reference', to: [{ type: 'page' }] }),
          ],
        }),
      ],
    }),
  ],
  preview: {
    prepare: () => ({ title: 'Site settings' }),
  },
})

The preview returns a fixed title, which looks tidier in search results and reference pickers than whatever the title field contains. Register the type in schemaTypes/index.ts as usual; if you're new to that, see how to create your first schema in Sanity.

Step 2: Register It With document.singletons

From Studio 6.17.0, Sanity has built-in singleton support. Register the type in the document section of your config:

// sanity.config.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './schemaTypes'
import { structure } from './structure'

export default defineConfig({
  name: 'default',
  title: 'My Studio',
  projectId: 'your-project-id',
  dataset: 'production',
  plugins: [structureTool({ structure }), visionTool()],
  schema: { types: schemaTypes },
  document: {
    singletons: ['siteSettings'],
  },
})

The shorthand string works when the singleton's ID, document ID and schema type all share a name. With that one line, Studio:

  • Hides siteSettings from the default list of document types.
  • Removes the Duplicate action for the document.
  • Keeps the type out of the global create menus.
  • Shows a "Singleton" badge in the document editor.

Custom IDs With defineSingleton

When the document ID differs from the type name, or two singletons share a schema type, use defineSingleton:

// sanity.config.ts
import { defineConfig, defineSingleton } from 'sanity'

export default defineConfig({
  // ...
  document: {
    singletons: [
      'siteSettings',
      defineSingleton({
        documentId: 'homepage',
        schemaType: 'page',
        title: 'Homepage',
      }),
      defineSingleton({
        documentId: 'blogIndex',
        schemaType: 'page',
        title: 'Blog index',
      }),
    ],
  },
})

Here homepage and blogIndex are both page documents, each with a fixed ID. defineSingleton also accepts icon and initialValue. Sharing a type with regular documents has an extra requirement: regular page documents still need an initial value template in schema.templates and an explicit S.documentTypeListItem('page') in your structure, because Studio otherwise treats the type as singleton-only. How to set initial values in Sanity documents explains templates.

This API is marked beta in Sanity's type definitions, so check the changelog when upgrading.

Step 3: Add It to Your Structure

Registration controls behaviour; the Structure Builder controls where the singleton appears in the sidebar:

// structure.ts
import type { StructureResolver } from 'sanity/structure'

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Content')
    .items([
      S.listItem().singleton('siteSettings'),
      S.listItem().singleton('homepage'),
      S.divider(),
      ...S.documentTypeListItems(),
    ])

S.listItem().singleton() uses the title and icon from the singleton definition (or the schema type) and opens the document directly, with no list in between. S.documentTypeListItems() excludes singleton-only types automatically, so you don't need to filter them out yourself.

To append every registered singleton at the end of the list, call .singletons() after .items():

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Content')
    .items(S.documentTypeListItems())
    .singletons()

Don't combine a bare .singletons() with a hand-written S.listItem().singleton() for the same singleton, or you'll get a duplicate ID error. If you want to customise the list item and its pane separately, build them yourself:

S.listItem()
  .title('Settings')
  .id('settings')
  .child(S.document().singleton('siteSettings'))

The post on understanding Sanity Studio shows how lists, panes and documents fit together.

The Manual Approach for Older Studios

If you're on a Studio version before 6.17.0, you can achieve the same result with three pieces of configuration. This was the standard pattern for years and still works.

// sanity.config.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { schemaTypes } from './schemaTypes'

const singletonTypes = new Set(['siteSettings'])
const singletonActions = new Set(['publish', 'discardChanges', 'restore'])

export default defineConfig({
  name: 'default',
  title: 'My Studio',
  projectId: 'your-project-id',
  dataset: 'production',
  plugins: [
    structureTool({
      structure: (S) =>
        S.list()
          .title('Content')
          .items([
            S.listItem()
              .title('Site settings')
              .id('siteSettings')
              .child(S.document().schemaType('siteSettings').documentId('siteSettings')),
            S.divider(),
            ...S.documentTypeListItems().filter(
              (item) => !singletonTypes.has(item.getId() ?? ''),
            ),
          ]),
    }),
  ],
  schema: {
    types: schemaTypes,
    // Remove singletons from the "Create new document" menu
    templates: (templates) => templates.filter(({ schemaType }) => !singletonTypes.has(schemaType)),
  },
  document: {
    // Only allow publish, discard and restore on singletons
    actions: (input, context) =>
      singletonTypes.has(context.schemaType)
        ? input.filter(({ action }) => action && singletonActions.has(action))
        : input,
  },
})

Each part has a job:

  1. The structure item opens a document with a fixed ID, siteSettings, instead of a list.
  2. The filter removes the type from the generic list so editors can't create a second one there.
  3. The template filter removes the type from the global create menu.
  4. The actions filter removes Duplicate and Delete, leaving only Publish, Discard changes and Restore.

When you upgrade, you can replace all of this with document.singletons and S.listItem().singleton(). Keep the same document ID and no content migration is needed.

Querying a Singleton

Because the ID is fixed, querying is straightforward. You can filter by ID or by type:

*[_id == "siteSettings"][0]{
  title,
  description,
  "ogImageUrl": ogImage.asset->url,
  "ogImageAlt": ogImage.alt,
  mainNav[]{ _key, label, "slug": page->slug.current }
}

Filtering on _id is the most precise, since it can never match a stray duplicate. Filtering on _type == "siteSettings" also works, but add [0] in case an old duplicate exists.

Using It in Next.js

In a Next.js App Router project, fetch the settings in your root layout and in generateMetadata:

// src/sanity/client.ts
import { createClient } from 'next-sanity'

export const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
  apiVersion: '2026-11-01',
  useCdn: true,
})
// src/app/layout.tsx
import type { Metadata } from 'next'
import Link from 'next/link'
import { defineQuery } from 'next-sanity'
import { client } from '@/sanity/client'

const SETTINGS_QUERY = defineQuery(`*[_id == "siteSettings"][0]{
  title,
  description,
  mainNav[]{ _key, label, "slug": page->slug.current }
}`)

export async function generateMetadata(): Promise<Metadata> {
  const settings = await client.fetch(SETTINGS_QUERY)
  return {
    title: { default: settings?.title ?? 'My site', template: `%s | ${settings?.title ?? 'My site'}` },
    description: settings?.description ?? undefined,
  }
}

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const settings = await client.fetch(SETTINGS_QUERY)

  return (
    <html lang="en">
      <body>
        <nav>
          {settings?.mainNav?.map((item) => (
            <Link key={item._key} href={`/${item.slug ?? ''}`}>
              {item.label}
            </Link>
          ))}
        </nav>
        {children}
      </body>
    </html>
  )
}

Always handle null. Until an editor publishes the singleton for the first time, the query returns nothing, and a site that crashes on a missing settings document is a poor first deploy. If you've set up TypeGen, defineQuery gives you typed results here; using defineType and defineField explains how that fits together.

Common Singleton Designs

SingletonTypical fieldsNotes
Site settingsTitle, description, social image, contact emailFetch once in the root layout
NavigationArrays of links with labels and referencesUse references so renamed pages update automatically
HomepageHero, sections array, SEO fieldsOften shares the page type via defineSingleton
FooterColumns of links, legal text, social profilesCan live inside site settings if small
Announcement barMessage, link, start and end datesCombine with a date filter in GROQ

A few habits help:

  • Keep singletons focused: one giant "Settings" document with 60 fields is hard to edit. Split it into site settings, navigation and footer, or use field groups and fieldsets.
  • Use references for links: linking to page documents rather than typing URLs means navigation updates when slugs change.
  • Seed the document: after deploying, open each singleton and publish it so your front end has data from day one.

FAQ: Singleton Documents in Sanity

A singleton is a document type that should only have one instance, such as site settings or a homepage. It uses a fixed document ID, and the Studio opens that document directly rather than showing a list.

Built-in singleton support with document.singletons and S.listItem().singleton() is available from Studio 6.17.0. Older versions can use the manual Structure Builder, template and actions approach.

With document.singletons, Studio removes the duplicate action. In the manual approach you filter document actions yourself, typically keeping only publish, discard changes and restore.

Querying by _id is the most precise, because the ID is fixed and can't match a stray duplicate. Querying by _type works too, but add [0] to return a single document.

Yes. Use defineSingleton with different documentId values and the same schemaType, for example a homepage and a blog index that both use the page type.

No, as long as you keep the same document ID. The new API changes Studio behaviour and structure, not the stored document.


Conclusion

Singleton documents give global and one-off content a single, predictable home. Define a normal document type, give it a fixed ID, and make the Studio open it directly. In current Studios, document.singletons plus S.listItem().singleton() handles the hiding, the create menus and the duplicate action for you, and defineSingleton covers custom IDs and shared types.

On older Studios, the manual combination of a structure item, a template filter and an actions filter achieves the same thing. Either way, query the singleton by its ID, handle the case where it hasn't been published yet, and keep each singleton focused so editors can find what they need quickly.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper