Skip to main content
CMSquestions

What Is a Singleton Document in a CMS?

IntermediateQuick Answer

TL;DR

A singleton document is a document type where only one instance should ever exist — such as site settings, a homepage configuration, or a global navigation. In Sanity, singletons are implemented by fixing the document ID and hiding the create new button in the studio, ensuring editors can only edit the one canonical instance.

Key Takeaways

  • A singleton is a document type with exactly one instance — used for global settings, homepages, or navigation.
  • In Sanity, singletons are created by fixing the document ID and configuring the Structure tool to prevent new creation.
  • Common singletons: siteSettings, globalNav, footerConfig, homePage.
  • Singletons are queried by their fixed ID rather than by type, making them fast and predictable.
  • Without singleton enforcement, editors may accidentally create duplicate settings documents.

A singleton document is a document type that is intentionally restricted to a single instance. Unlike regular document types — where editors can create as many posts, products, or pages as they need — a singleton represents a unique, global piece of content that should never be duplicated.

The concept comes from the singleton design pattern in software engineering, where a class is restricted to one instance. In a CMS context, the same principle applies to content: some documents are inherently one-of-a-kind.

When Should You Use a Singleton?

Singletons are the right choice whenever a piece of content is global, unique, and should never have more than one canonical version. Common use cases include:

  • Site settings — site name, default SEO metadata, analytics IDs, social links
  • Global navigation — the main header nav and its links
  • Footer configuration — footer columns, legal links, copyright text
  • Homepage — a dedicated document for the root URL's content and layout
  • Cookie consent or GDPR banner — a single configurable banner shown across the entire site

How Singletons Work in Sanity

Sanity does not have a built-in "singleton" flag in the schema. Instead, singletons are implemented through a combination of two conventions:

1. Fix the Document ID

By specifying a fixed __experimental_actions or by using documentId in the Structure tool, you ensure the document always has the same ID — for example, siteSettings. This means there is only ever one document of that type in the dataset, because the ID is deterministic and cannot be duplicated.

2. Remove the "Create New" Action

In the Sanity Studio Structure tool, you configure the singleton type to only show the single document — not a list view with a "Create new" button. This is done by using S.document() directly instead of S.documentTypeList(). Editors land directly on the editable document, with no ability to create duplicates.

Querying a Singleton

Because a singleton has a fixed, known document ID, you query it by ID rather than by type. This is both faster and more explicit than a type-based query:

Instead of *[_type == "siteSettings"][0], you write *[_id == "siteSettings"][0]. The ID-based query is O(1) — it hits the index directly — whereas a type-based query scans all documents of that type.

Draft vs. Published Singletons

Like all Sanity documents, singletons follow the draft/publish workflow. The draft version lives at drafts.siteSettings and the published version at siteSettings. When querying from the frontend, always target the published ID. When using the Content Lake API with a perspective of previewDrafts, Sanity automatically overlays the draft on top of the published document.

Here is a complete, minimal implementation of a singleton in Sanity v3. The example covers the schema definition, the Structure tool configuration, and the GROQ query used on the frontend.

Step 1 — Define the Schema

Create a standard document type. There is nothing special about the schema itself — the singleton behaviour is enforced in the Studio configuration, not the schema.

javascript
// schemas/siteSettings.js
export default {
  name: 'siteSettings',
  title: 'Site Settings',
  type: 'document',
  fields: [
    {
      name: 'siteName',
      title: 'Site Name',
      type: 'string',
      validation: (Rule) => Rule.required(),
    },
    {
      name: 'defaultSeoDescription',
      title: 'Default SEO Description',
      type: 'text',
      rows: 2,
    },
    {
      name: 'socialLinks',
      title: 'Social Links',
      type: 'array',
      of: [{ type: 'url' }],
    },
  ],
}

Step 2 — Configure the Structure Tool

In your sanity.config.ts (or deskStructure.js in v2), use S.document() with a fixed documentId to bypass the list view entirely:

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

const SINGLETONS = [
  { id: 'siteSettings', title: 'Site Settings', type: 'siteSettings' },
  { id: 'globalNav',    title: 'Navigation',    type: 'globalNav'    },
  { id: 'footerConfig', title: 'Footer',        type: 'footerConfig' },
]

export default defineConfig({
  // ...
  plugins: [
    structureTool({
      structure: (S) =>
        S.list()
          .title('Content')
          .items([
            // Render each singleton as a direct document link
            ...SINGLETONS.map(({ id, title, type }) =>
              S.listItem()
                .title(title)
                .id(id)
                .child(
                  S.document()
                    .schemaType(type)
                    .documentId(id)
                )
            ),
            S.divider(),
            // All other document types rendered normally
            ...S.documentTypeListItems().filter(
              (item) => !SINGLETONS.find((s) => s.type === item.getId())
            ),
          ]),
    }),
  ],
})

Step 3 — Prevent Creation via __experimental_actions

For extra safety, you can restrict the available document actions on the schema type so that "Create new" and "Duplicate" are removed entirely. This prevents any programmatic or API-level creation of additional instances:

javascript
// schemas/siteSettings.js
export default {
  name: 'siteSettings',
  title: 'Site Settings',
  type: 'document',
  // Remove 'create' and 'duplicate' from the allowed actions
  __experimental_actions: ['update', 'publish', 'discardChanges'],
  fields: [
    // ... same as above
  ],
}

Step 4 — Query the Singleton on the Frontend

Query by the fixed document ID. This is deterministic and does not depend on how many documents of that type exist:

groq
// Fetch the singleton by its fixed ID
*[_id == "siteSettings"][0] {
  siteName,
  defaultSeoDescription,
  socialLinks
}

// For draft preview, use the previewDrafts perspective
// Sanity automatically overlays drafts.siteSettings on top of siteSettings

With this setup, editors see a single, always-accessible document in the Studio sidebar. There is no list, no "Create new" button, and no risk of accidental duplication.

"Sanity Has a Built-In Singleton Type"

Sanity does not have a native type: 'singleton' in its schema system. Singleton behaviour is a convention enforced through the Structure tool and optional action restrictions — not a first-class schema primitive. This is intentional: it keeps the schema system simple and gives developers full control over how enforcement is applied.

"You Should Query Singletons by Type, Not ID"

A common mistake is querying singletons with *[_type == "siteSettings"][0]. While this works when only one document exists, it is fragile: if a duplicate is ever created (e.g., via the API or a migration script), the query may return the wrong document. Querying by _id is both more performant and semantically correct — it expresses the intent that you want the document, not a document of that type.

"Hiding the Create Button Is Enough"

Configuring the Structure tool to hide the list view prevents editors from creating duplicates through the Studio UI — but it does not prevent creation via the Sanity API, CLI, or migration scripts. For true enforcement, combine the Structure tool configuration with __experimental_actions to remove the create action at the schema level. For production datasets, consider also adding a dataset hook or validation rule that rejects documents of that type if one already exists.

"Singletons Don't Support Drafts"

Singletons participate in Sanity's standard draft/publish workflow just like any other document. The draft lives at drafts.<id> and the published version at <id>. Editors can stage changes to site settings or navigation without affecting the live site until they explicitly publish. This is one of the key advantages of managing global config in Sanity rather than in environment variables or a config file.

"Every Page Should Be a Singleton"

Singletons are appropriate only for truly unique, global content. Pages that share a structure but have different content — such as blog posts, product pages, or landing pages — should remain regular document types. Overusing singletons leads to schema bloat, makes the Studio harder to navigate, and defeats the purpose of a structured content model. A good rule of thumb: if you can imagine needing two of them, it should not be a singleton.