All skills
sanity-io avatar

/sanity-best-practices

@fc8116b official
by Sanitysanity-io/agent-toolkit187 stars
30

Sanity development best practices for schema design, GROQ queries, TypeGen, Visual Editing, images, Portable Text, Studio structure, localization, migrations, Sanity Functions, webhooks, Blueprints, and framework integrations such as Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, Hydrogen, and the App SDK. Use this skill whenever working with Sanity schemas, defineType or defineField, GROQ or defineQuery, content modeling, Presentation or preview setups, Sanity-powered frontend integrations, event-driven content automation, documentEventHandler, defineDocumentFunction, defineMediaLibraryAssetFunction, @sanity/functions, @sanity/blueprints, sanity.blueprint.ts, event-driven content automation, or when reviewing and fixing a Sanity codebase.

Use this Skill: https://skilld.dev/gh/sanity-io/agent-toolkit/sanity-best-practices

This session only. Nothing lands on disk.

referenceslocalization.md

≈3.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Sanity Localization Rules

Use the contents list to jump directly to the localization pattern you need.

Table of Contents

  • Guiding principles
  • Terminology
  • Locale content type
  • Choosing document-level vs field-level localization
  • Document-level localization
  • Localized singletons
  • Field-level localization
  • AI-powered translation
  • UI enhancement
  • Frontend URL best practices

1. Guiding Principles

Priority: Easy Authoring Experience

The structured nature of Sanity schemas and GROQ make it easy to parse localized content for your frontend. Never let frontend architecture dictate your localization approach — prioritize the editor experience.

Avoid Content Duplication

Don't create nearly identical copies with slight differences (e.g., US vs British English). Use Portable Text marks and custom blocks to swap out words or sections as needed.

2. Terminology

Term Definition
Internationalization (i18n) Designing your frontend to support multiple languages
Localization Adapting content for a specific language/region
Language Tag Code like en, en-US, zh-Hant-TW (per IETF RFC 5646)
Locale A language tag with region info (e.g., en-US)

3. Create a Locale Content Type

Best Practice: Store locales in Sanity, not just in code. This allows sharing between Studio and frontend.

// schemaTypes/locale.ts
import { TranslateIcon } from '@sanity/icons/Translate'
import { defineField, defineType } from 'sanity'

export const localeType = defineType({
  name: 'locale',
  icon: TranslateIcon,
  type: 'document',
  fields: [
    defineField({ name: 'name', type: 'string', validation: (r) => r.required() }),
    defineField({ name: 'tag', type: 'string', description: 'IANA tag (en, en-US)', validation: (r) => r.required() }),
    defineField({ name: 'fallback', type: 'reference', to: [{ type: 'locale' }] }),
    defineField({ name: 'default', type: 'boolean' }),
  ],
  preview: { select: { title: 'name', subtitle: 'tag' } },
})

Tip: Restrict locale editing to admins via Structure by filtering locale from non-admin users.

4. Choose Your Localization Method

Content Type Examples Recommended Method
Structured (things) Products, People, Locations, Categories Field-level
Presentation (UI) Pages, Posts, Components Document-level

Decision Questions

  1. Are fields shared across languages? → Field-level
  2. Should changes be "global" for all locales? (e.g., reordering components) → Field-level
  3. Is content mostly the same except regional differences? → Field-level with PT marks
  4. Need to publish language versions independently? → Document-level

5. Document-Level Localization

Use the @sanity/document-internationalization plugin.

npm install @sanity/document-internationalization

Configuration

// sanity.config.ts
import { documentInternationalization } from '@sanity/document-internationalization'

export default defineConfig({
  plugins: [
    documentInternationalization({
      // Fetch from Content Lake
      supportedLanguages: (client) =>
        client.fetch(`*[_type == "locale"]{ "id": tag, "title": name }`),
      // Document types to localize
      schemaTypes: ['post', 'page'],
    }),
  ],
})

Add Language Field to Schema

// In each schema type listed in schemaTypes
defineField({
  name: 'language',
  type: 'string',
  readOnly: true,
  hidden: true,
})

Initial Value Templates

Pre-set language when creating documents outside the translation UI:

// sanity.config.ts
import { defineConfig } from 'sanity'
import type { Template } from 'sanity'

const LOCALIZED_TYPES = ['post', 'page']
const BASE_LANGUAGE = 'en'

export default defineConfig({
  // ...
  document: {
    newDocumentOptions: (prev) => [
      // Drop the auto-generated entries for localized types — they create
      // documents with no `language` set
      ...prev.filter((item) => !LOCALIZED_TYPES.includes(item.templateId)),
      // Offer the base-language templates instead
      // The plugin handles creating translations from the document itself
      ...LOCALIZED_TYPES.map((schemaType) => ({
        templateId: `${schemaType}-${BASE_LANGUAGE}`,
        parameters: {language: BASE_LANGUAGE},
      })),
    ],
  },
  schema: {
    // A base-language template per localized type
    templates: (prev): Template[] => [
      ...prev,
      ...LOCALIZED_TYPES.map((schemaType) => ({
        id: `${schemaType}-${BASE_LANGUAGE}`,
        title: `${schemaType} (${BASE_LANGUAGE})`,
        schemaType,
        parameters: [{name: 'language', type: 'string'}],
        value: ({language}: {language: string}) => ({language}),
      })),
    ],
  },
})

A template that declares parameters is left out of the auto-generated "New document" list, so it only appears if newDocumentOptions adds it explicitly, with the parameter values supplied on the item. The auto-generated items carry no parameters of their own, so a filter that tests item.parameters matches nothing and empties the menu.

Querying Translated Documents

// Get document in specific language
*[_type == "post" && language == $locale && slug.current == $slug][0]

// Get all translations via metadata document
*[_type == "translation.metadata" && references($docId)][0] {
  translations[] {
    _key,
    value-> { title, slug, language }
  }
}

6. Localized Singletons (Homepage per Locale)

For singletons like homepages that need a separate document per locale, combine document-level localization with the singleton pattern.

Schema Definition

// schemaTypes/homePage.ts
import { HomeIcon } from '@sanity/icons/Home'
import { defineType, defineField } from 'sanity'

export const homePageType = defineType({
  name: 'homePage',
  title: 'Home Page',
  type: 'document',
  icon: HomeIcon,
  fields: [
    defineField({
      name: 'language',
      type: 'string',
      readOnly: true,
      hidden: true,
    }),
    defineField({ name: 'title', type: 'string' }),
    defineField({ name: 'pageBuilder', type: 'pageBuilder' }),
    // ... other fields
  ],
  preview: {
    select: { language: 'language' },
    prepare({ language }) {
      return {
        title: 'Home Page',
        subtitle: language?.toUpperCase() || 'No language',
      }
    },
  },
})

Initial Value Templates

Create templates that pre-set the language for each locale:

// sanity.config.ts
import { defineConfig } from 'sanity'
import type { Template } from 'sanity'

// Define your supported locales
const LOCALES = [
  { id: 'en', title: 'English' },
  { id: 'fr', title: 'French' },
  { id: 'de', title: 'German' },
]

export default defineConfig({
  // ...
  schema: {
    templates: (prev) => {
      // Create a template for each locale
      const homePageTemplates: Template[] = LOCALES.map((locale) => ({
        id: `homePage-${locale.id}`,
        title: `Home Page (${locale.title})`,
        schemaType: 'homePage',
        parameters: [{ name: 'language', type: 'string' }],
        value: { language: locale.id },
      }))

      return [...prev, ...homePageTemplates]
    },
  },
})

Structure: Localized Singleton Helper

Create a helper to show one singleton per locale in the Structure:

// src/structure/index.ts
import { StructureBuilder, StructureResolver } from 'sanity/structure'
import { HomeIcon } from '@sanity/icons/Home'

const LOCALES = ['en', 'fr', 'de']

function createLocalizedSingleton(
  S: StructureBuilder,
  typeName: string,
  title: string,
  icon?: React.ComponentType
) {
  return S.listItem()
    .title(title)
    .icon(icon)
    .child(
      S.list()
        .title(title)
        .items(
          LOCALES.map((locale) =>
            S.listItem()
              .title(`${title} (${locale.toUpperCase()})`)
              .icon(icon)
              .child(
                S.document()
                  .schemaType(typeName)
                  .documentId(`${typeName}-${locale}`) // Fixed ID per locale
                  .title(`${title} (${locale.toUpperCase()})`)
              )
          )
        )
    )
}

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Content')
    .items([
      // Localized singletons
      createLocalizedSingleton(S, 'homePage', 'Home Page', HomeIcon),

      S.divider(),

      // Filter localized singletons from default list
      ...S.documentTypeListItems().filter(
        (item) => !['homePage'].includes(item.getId() as string)
      ),
    ])

Querying Localized Singletons

// Get homepage for specific locale
*[_type == "homePage" && language == $locale][0]{
  title,
  pageBuilder[]{...}
}

// Or by fixed document ID
*[_id == "homePage-" + $locale][0]{...}

Key Points

  • Fixed IDs: Use ${typeName}-${locale} only for localized singletons; let Sanity generate IDs for ordinary localized content
  • Initial Value Templates: Essential for the "New document" menu to work correctly
  • Structure: Group all locale versions under one list item for cleaner navigation
  • See also: studio-structure.md for more singleton patterns

7. Field-Level Localization

Use sanity-plugin-internationalized-array (NOT localized objects — they hit attribute limits).

npm install sanity-plugin-internationalized-array

Configuration

// sanity.config.ts
import { internationalizedArray } from 'sanity-plugin-internationalized-array'

export default defineConfig({
  plugins: [
    internationalizedArray({
      languages: (client) =>
        client.fetch(`*[_type == "locale"]{ "id": tag, "title": name }`),
      fieldTypes: ['string', 'text', 'simpleBlockContent'],
    }),
  ],
})

Usage in Schema

// The plugin creates types like `internationalizedArrayString`
defineField({
  name: 'jobTitle',
  type: 'internationalizedArrayString', // Localized string field
})

Portable Text Localization

Create a reusable block content type, then add it to fieldTypes:

// schemaTypes/simpleBlockContent.ts
export default defineType({
  name: 'simpleBlockContent',
  type: 'array',
  of: [
    {
      type: 'block',
      styles: [{ title: 'Normal', value: 'normal' }],
      lists: [],
    },
  ],
})

// sanity.config.ts
fieldTypes: ['string', 'simpleBlockContent']

// In your schema
defineField({
  name: 'bio',
  type: 'internationalizedArraySimpleBlockContent',
})

Querying Internationalized Arrays

// Get specific locale value
*[_type == "author"][0] {
  "jobTitle": jobTitle[_key == $locale][0].value
}

// With fallback
*[_type == "author"][0] {
  "jobTitle": coalesce(
    jobTitle[_key == $locale][0].value,
    jobTitle[_key == "en"][0].value
  )
}

8. AI-Powered Translation

Use @sanity/assist for automated translations.

npm install @sanity/assist
// sanity.config.ts
import { assist } from '@sanity/assist'

export default defineConfig({
  plugins: [
    assist({
      translate: {
        // For document-level localization
        document: {
          languageField: 'language',
        },
        // For field-level localization
        field: {
          languages: (client) =>
            client.fetch(`*[_type == "locale"]{ "id": tag, "title": name }`),
          documentTypes: ['author', 'category'],
        },
      },
    }),
  ],
})

9. UI Enhancement

Use @sanity/language-filter to let editors show/hide locales:

npm install @sanity/language-filter

10. Frontend URL Best Practices

Always include locale in the URL for SEO:

  • yoursite.com/en/my-page → yoursite.com/fr/my-page
  • yoursite.com/my-page → redirects to default locale

Avoid: Having the default locale at root without prefix — causes SEO edge cases.

Use Next.js middleware (or framework equivalent) to redirect paths missing a locale prefix to the default locale.

Source: SKILL.md on GitHub

2 warnings14d5 checks · Risk SAFE
  • Gen Agent Trust Hub14d

    This skill provides comprehensive Sanity.io development best practices, covering schema design, GROQ queries, and integration with major frontend frameworks. It promotes secure development habits, such as proper management of API tokens and environment variables.

  • Socket14d

    1 alert: gptAnomaly

  • Snyk14d

    Risk: LOW · No issues

  • Runlayer6mo

    8/24 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at fc8116b. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 months ago
  • Next.js
  • Nuxt
  • sanity
  • groq
  • schema
  • typegen
  • portable-text
  • visual-editing
  • astro
  • remix
  • sveltekit
  • migrations
  • localization
  • cms

README badge

README badge for sanity-io/agent-toolkit/sanity-best-practices

Provides guidelines and reference materials for Sanity schema design, GROQ queries, TypeGen, Visual Editing, Portable Text, Studio structure, localization, migrations, Sanity Functions, and framework integrations including Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, and Hydrogen. Use this skill when setting up Sanity projects, designing content models, writing queries, implementing live preview, or integrating Sanity with a frontend framework.

Generated from the current SKILL.md.

Does this skill cover framework integrations like Next.js, Nuxt, and Astro?
Yes. The skill includes integration guides for Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, Hydrogen, and standalone Studio patterns.
What does this skill cover for GROQ queries?
The skill provides GROQ query patterns, type safety approaches, and performance optimization guidelines, with reference materials for detailed examples.
Does this skill include guidance on Sanity Functions and event automation?
Yes. The skill covers Sanity Functions for automating content workflows, including documentEventHandler and defineDocumentFunction patterns.
Can I use this skill for schema design and content modeling?
Yes. The skill provides schema design best practices, field definitions, validation patterns, and content modeling guidance for different use cases.
Does this cover Visual Editing and live preview setup?
Yes. The skill includes guidance on the Presentation Tool, Stega, overlays, and live preview configuration for frontend frameworks.

Generated from the current SKILL.md. These answers refresh after source changes.