Best practices for storing all site text and navigation in Sanity

16 replies
Last updated: May 13, 2022
Hey everyone. I’m starting my second multi-language Sanity-based project and I’d like to have all the site data within, that means all navigation links, button labels, footer text, and every snippet of text possible. Is there a guide to best practices for this scenario? My previous project was hybrid, in which part of the site’s text strings were in JSON files and the rest on Sanity documents.
I want to create all the navigation structure and site data in Sanity Studio.
AI Update

Great question! Managing all site data including navigation, labels, and text snippets in Sanity is definitely achievable and a common pattern. Here's how to approach this for a multi-language project:

Singleton Documents for Global Content

For navigation, footer, and site-wide text, you'll want to use singleton documents. These are document types that only have one instance and are perfect for global settings. You'd typically create schemas like:

  • siteSettings - for global configuration
  • navigation - for main menu structure
  • footer - for footer content
  • labels - for UI strings like button text, form labels, etc.

Setting up singletons requires configuring three things in your sanity.config.js: the Structure Builder to create direct links to these documents, template filtering to prevent creating duplicates, and document actions to disable deletion. The singleton document guide walks through this, or you can use the singleton-tools plugin to simplify setup.

Choosing Your Localization Strategy

Sanity offers two main approaches for multi-language content, and based on the localization documentation, here's what works best:

Document-level localization (recommended for most cases): Each language gets its own complete document. The @sanity/document-internationalization plugin handles this by creating separate documents linked through a shared reference. This means you'd have navigation-en, navigation-es, etc. This approach:

  • Avoids attribute limit issues
  • Provides cleaner content structures
  • Works great for all content types including Portable Text

Field-level localization: Store all translations within the same document. While convenient for simple use cases, this isn't recommended for complex content or Portable Text fields as it can quickly hit attribute limits and create unwieldy data structures.

Structuring Navigation and Labels

For navigation specifically, you might create a schema like:

{
  name: 'navigation',
  type: 'document',
  fields: [
    {
      name: 'items',
      type: 'array',
      of: [{
        type: 'object',
        fields: [
          { name: 'label', type: 'string' },
          { name: 'url', type: 'string' },
          // or reference to pages
          { name: 'page', type: 'reference', to: [{ type: 'page' }] }
        ]
      }]
    }
  ]
}

For UI labels and strings, consider grouping them logically:

{
  name: 'labels',
  type: 'document',
  fields: [
    { name: 'submitButton', type: 'string' },
    { name: 'learnMore', type: 'string' },
    { name: 'contactUs', type: 'string' }
    // etc.
  ]
}

Additional Considerations

  • Language Filter: Use the Language Filter to help editors work with specific languages in the Studio interface
  • Studio UI localization: You can also localize the Studio interface itself so editors work in their preferred language
  • Fallback strategy: Plan how your frontend handles missing translations - typically falling back to a default language
  • Translation workflows: If using professional translation services, Sanity integrates with Smartling and Transifex

The document-level approach with the @sanity/document-internationalization plugin combined with singleton documents for global content is generally the most scalable and maintainable solution for what you're describing. It keeps your data clean and makes it easy for editors to manage translations independently.

Show original thread
16 replies
Hey Guillermo!
One approach to consider for globally translated labels is what I do on our Course Platform demo. It’s a singleton document with an array field of key-value pairs. The schema looks like this, the resultant field looks like the below.


https://github.com/sanity-io/demo-course-platform/blob/main/studio/src/schema/documents/labelGroup.js
Hey
user N
, I would say that when it comes to language (localisation) on your page the best practice depends on the scope / goals of your project.

Field level approach If you would like to have identical structure / content on the page regardless of the language then the field level approach is the best as you can create translations for the content that needs translation while keep the logical parts (e.g. show meny item on the page or not) shared between the languages.
Read more:
https://www.sanity.io/docs/localization#cd568b11a09c

Document level approach If you would potentially have different structure of the content for every language then the doc level approach is good. Then you can also make it so some subpages would be only found in particular language but not found in other languages - that can be useful for marketing purposes. You can then also connect each language to different URL based on the localisation. Doc:
https://www.sanity.io/docs/localization#8b6eddd0570f

Separate Dataset for each language I you want to have 3 different webpage for each language (localisation) then it makes sense to use 3 different datasets for each language. In this case you also will have a lot of flexibility to customise studio (content / content structure) for each page.
Hi
user T
This is great! it’s very similar to my previous approach in which I stored the key:value pairs in JSON files. From what I gather it’s an array of objects, one for each label. Seems pretty simple.My other project used regular manual translations, something I took from the
official Sanity guide using field level translations. There wasn’t a ton of content and only two languages so it’s totally manageable. I see you’ve implemented automatic translations from Google… I’ll definitely look into that.
user M
I was considering the field level approach, but only because I’m familiar with it since I used it for my last project. It’s easier to manage and the site won’t be too big.
Yep Google Translate is a sneaky custom input plugin 🙂 https://www.sanity.io/plugins/sanity-plugin-google-translate
And yeah the idea of this labelGroup schema was to mirror the sort of translation common across sites that use JSON for content. But with GROQ you can query for just the right language across the entire array!


*[_id == "labelGroup"][0].labels{ key, "text": text[$lang] }
That demo course platform uses multiple approaches where it makes sense.

lesson
schema has document-level translations because there’s no common fields between languages
presenter
schema uses field-level because only some fields are localised
labelGroup
uses key/value pairs and field-level because they’re globally relevant content
Sorry to bother you, I’
Sorry to bother you again
user T
, I just have a quick question. I created a schema following you example to the translated labels. The schema is very similar to yours:

export default {
  name: "commonLabels",
  title: "Etiquetas y Textos",
  type: "document",
  fields: [
    {
      name: "labels",
      title: "Etiquetas",
      description: `Etiquetas y textos comunes que requieran traducciones.`,
      type: "array",
      of: [
        {
          name: "label",
          title: "Etiqueta",
          type: "object",
          fields: [
            {
              name: "key",
              title: "Key",
              type: "string",
              description: `Identifica la etiqueta en el código`,
            },
            { name: "value", title: "Value", type: "localeString" },
          ],
        },
      ],
    },
  ],
};
Running a simple QROQ query for it:

*[_type == "commonLabels"][0]{
labels[]
}
Returns the following:

"result":{1 item
"labels":[1 item
0:{4 items
"_key":"332212de888c"
"_type":"label"
"key":"home.introText"
"value":{3 items
"_type":"localeString"
"en":"Welcome to EK roboter S.A."
"es":"Bienvenido a EK roboter S.A."
}
}
]
}
My dumb questing is, how do I display the
home.IntroText
label? I’m using
getStaticProps
in a test page to fetch all labels:

const commonLabelsQuery = groq`*[_type == "commonLabels"][0]{
labels[]
}`

export const getStaticProps: GetStaticProps = async () => {
  const commonLabels = await sanityClient.fetch(commonLabelsQuery)
  return {
    props: {
      commonLabels,
    },
  }
}
I’ve tried with
<http://commonLabels.home.introText.es|commonLabels.home.introText.es>
but doesn’t work. Probably becase my key is named
home.introText
? I wanted to organized the labels by page or section, for instance all the strings for the homepage would begin with
home.
this is how I did it before with JSON files.
You could either post-process array of
commonLabels

const labels = {}

commonLabels.forEach(label => labels[label.key] = label.value)
Then you could get
labels['home.introText']

===

Or use a find method on your array

const label = commonLabels.find(label => label.key === 'home.introText')
Also if you don’t want all languages, filter your labels with GROQ and only get the language text you need 😄

*[_type == "commonLabels"][0]{
  labels[]{
    key,
    "value": value[$lang]
  }
}
Interesting. I only have two languages, so it’s not a lot of data. It also helps when switching languages instantaneously.The post-process approach sound logical, as I may have pages which take in strings and labels from different categories. It’s throwing me a TS error though:

TypeError: commonLabels.forEach is not a function. (In 'commonLabels.forEach(function(label) {
        return labels[label.key] = label.value;
    })', 'commonLabels.forEach' is undefined)
Target the array. Maybe it’s
commonLabels.labels
?
Or extend your GROQ to only return the labels 😄

*[_type == "commonLabels"][0]{
  labels[]{
    key,
    "value": value[$lang]
  }
}.labels
Awesome, I'll try it after lunch. Thanks so much for your help. I'm still learning a lot as you may notice. Took a look at your source code and there's still a ton to learn.
Every day’s a school day, there’s always more to learn!! 😅
Tell me about it! four months ago I didn’t know the first thing about React and NextJS and couldn’t even begin to try to do anything… now I can comfortably go through your code and understand almost everything in it. I returned to web dev after a few years and the landscape had changed enormously.
Yea my first experience with React was hacking together a gatsby site on Sanity … and the rest is history!

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?