Hey there! if you are here looking for some hacks on improving your sanity based JAMstack sites then we have a heck-ton of stuff for you.
In this blog we assume that you have some amount of familiarity in terms of sanity structure, schemas etc and if not then I personally got started with this youtube video by the good folks at sanity and freecodecamp!
But obviously not everything can be covered in a single video, so here’s a bunch of secret sauces we are spilling out.
Naming Conventions and reusability
Despite our best efforts, sometimes reusability will come at a cost of complexity, it’s generally best to build stuff out & get your hands dirty to make a naming convention that would suit your project and style of programming. That being said, we find categorising our sanity schema into three folders / categories as the best way to keep things simple:
- Documents: This is where our main categories go in, let’s say you are making a landing page for a company, here is where our pages like people, about, contact reside.
- Definitions: This is where we store re-usable objects / array components like buttons, links etc.
- Blocks: This is where actual building blocks like sections, carousels etc go in. They use definition components and give you a section of sorts that you use to build an actual page.
Here’s an example of a blog page:
Here, the portableTextBlock will be a reusable component under blocks (where we have a portableText type which goes in definitions) since we never know how many places / pages would require a rich text editing block, so we abstract it away like this:
import {defineField} from 'sanity'import {FileText} from 'lucide-react'export const portableTextBlock = {name: 'portableTextBlock',title: 'Portable text block',type: 'object',icon: FileText,fields: [defineField({title: 'Title',name: 'title',type: 'string',}),defineField({title: 'portableText',name: 'portableText',type: 'portableText',}),],preview: {select: {title: 'title',},prepare({title}: any) {return {title: title,subtitle: 'Portable Text Block',media: FileText,}},},}
portable text / rich text schema:
import {ImageIcon} from '@sanity/icons'import {defineType} from 'sanity'export const portableText = defineType({name: 'portableText',title: 'Rich Text',type: 'array',of: [{type: 'block',styles: [{title: 'Normal', value: 'normal'},{title: 'H2', value: 'h2'},{title: 'H3', value: 'h3'},{title: 'H4', value: 'h4'},{title: 'H5', value: 'h5'},{title: 'H6', value: 'h6'},{title: 'Quote', value: 'blockquote'},],lists: [{title: 'Numbered', value: 'number'},{title: 'Bullet', value: 'bullet'},],marks: {decorators: [{title: 'Strong', value: 'strong'},{title: 'Emphasis', value: 'em'},],},},{name: 'image',title: 'Image',type: 'image',icon: ImageIcon,fields: [{name: 'alt',type: 'string',title: 'Alt Text',},],},],})
If you need to add videos then we recommend reading through this article, mux video just makes everything simple and efficient.
Now with all of this in place, we can make a document called article which uses all of these to add articles with ease.
By structuring and re-using some components like this in your schema, you have a plug and play system that can help you minimise the amount of GROQ queries that you write. Albeit it can get out of hand and make things more complicated, and that’s where your dev instincts will kick in and just do what’s best in your case.
Schema for the article.ts
import { defineField } from 'sanity';export const article = {name: 'article',title: 'Articles',type: 'document',fields: [defineField({name: 'title',title: 'Title',description:"Every page should have a title that isn't too long or too short. We recommend page titles are between 10 and 60 characters.",type: 'string',validation: (Rule) => Rule.required(),}),defineField({name: 'portableText',title: 'Portable text',type: 'portableText',}),// .... and so on]}
Also, while we are at it, here are some utility packages for sanity schemas
- Asset-utils: You can use this to generate aspect ratios and pull widths/heights from images (commonly asked question)
- Sanity icons: These are bundled by default, can drop these in and out project to project without having to install an additional icon set.
Auto-generate sanity schema types
Little bit of a pre-face, this tool has technically been put on hiatus but is open for sponsorship. If you're part of a giant conglomerate and reading this - do us all a favour and see if they'll throw some mulah his way.
Defining schema types will help you in a long way when building your web application, So we will be using this package called sanity-codegen. Here's the command to install it
npm i -D sanity-codegen
and add a config file like this at a location like this:
root_dir/sanity_folder/sanity-codegen.config.ts
import { SanityCodegenConfig } from 'sanity-codegen';const config: SanityCodegenConfig = {schemaPath: './schemas', //path to all the schemas definedoutputPath: '../web/src/schema.ts', //path to the shared schema fileprettierResolveConfigPath: '../web/.prettier.js', //optional};export default config;
make sure to install these as a devDependency and if you run into any errors then here’s a link that most likely will fix your problem.
A bit of a hassle to setup these workflows but once they are done you will fly by your development, we highly recommend that you spend time in simplifying tasks like this.
Custom hotkeys
You just moved from notion, hashnode (etc..) and now you miss the keyboard shortcut keys while editing your article content. Guess what, sanity has got you covered. Here's a lazy mans version on how you can add custom hotkeys.
As of writing, I didn't find a hotkey to have a strike-through decorator on highlighted text content, so here we are defining one.
This is in portableText file where I have added a customInput component to add this hotkey. Since we are importing react here, make sure you modify your file to portableText.tsx or richText.tsx
import {useMemo} from 'react'import {PortableTextInput, PortableTextInputProps, defineType} from 'sanity'const customInput = (props: PortableTextInputProps) => {const {path, onItemOpen, onPathFocus} = propsconst hotKeys: PortableTextInputProps['hotkeys'] = useMemo(() => ({marks: {'Ctrl+Shift+s': 'strike-through',},}),[onPathFocus, onItemOpen, path])return <PortableTextInput {...props} hotkeys={hotKeys} />}export const portableText = defineType({name: 'portableText',title: 'Portable text',type: 'array',of: [{type: 'block',styles: [{title: 'Normal', value: 'normal'},{title: 'H2', value: 'h2'},{title: 'H3', value: 'h3'},{title: 'H4', value: 'h4'},{title: 'H5', value: 'h5'},{title: 'H6', value: 'h6'},{title: 'Quote', value: 'blockquote'},],lists: [{title: 'Numbered', value: 'number'},{title: 'Bullet', value: 'bullet'},],marks: {annotations: [{name: 'customLink',type: 'object',title: 'Internal/External Link',fields: [{name: 'customLink',type: 'url',},],},],decorators: [{title: 'Strong', value: 'strong'},{title: 'Emphasis', value: 'em'},{title: 'Strike-through', value: 'strike-through'}],},},{name: 'image',title: 'Image',type: 'image',fields: [{name: 'alt',type: 'string',title: 'Alt Text',},],},],components: {input: customInput,},})
Here's the link to the full article where you can go deep into it.
Now with all these changes done, you will have to run sanity deploy for the changes to reflect on your main CMS domain / site. A bit of a hassle to do it every time innit? Lucky you, cause our in-house wizard hrithik got you covered with this nifty blog where a github action will auto deploy each time a change is pushed, make sure you check that out!
Thank you so much for reading through this article, we hope that it helps you in one form or the other! if you have any thoughts or doubts then please feel free to reach out to us at hello@roboto.studio