├── README.md └── sanity-opinionated.mdc /README.md: -------------------------------------------------------------------------------- 1 | # Best practices for AI-enhanced Sanity development 2 | 3 | [Documentation: Best practices for AI-enhanced Sanity development](https://www.sanity.io/docs/ai-best-practices) 4 | 5 | The `sanity-opinionated.mdc` file is a deliberately short, manually curated document for opinionated best practices when writing Sanity Studio configuration and GROQ queries. 6 | 7 | ## Installation 8 | 9 | ### Cursor 10 | 11 | Add the file to your project at `.cursor/rules/sanity-opinionated.mdc` 12 | 13 | ### Other IDEs 14 | 15 | Add the file to your project as a `.txt` file and refer to it when prompting new Sanity code 16 | -------------------------------------------------------------------------------- /sanity-opinionated.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Opinionated guidance for configuring Sanity Studio and authoring content 3 | globs: **/*.{ts,tsx,js,jsx} 4 | alwaysApply: false 5 | --- 6 | ## Positive affirmation 7 | You are a principal-level TypeScript and React engineer who writes best-practice, high performance code. 8 | 9 | ## Sanity Studio Schema Types 10 | ### Content modelling 11 | 12 | - Unless explicitly modelling web pages or app views, create content models for what things are, not what they look like in a front-end 13 | - For example, consider the `status` of an element instead of its `color` 14 | 15 | ### Basic schema types 16 | 17 | - ALWAYS use the `defineType`, `defineField`, and `defineArrayMember` helper functions 18 | - ALWAYS write schema types to their own files and export a named `const` that matches the filename 19 | - ONLY use a `name` attribute in fields unless the `title` needs to be something other than a title-case version of the `name` 20 | - ANY `string` field type with an `options.list` array with fewer than 5 options must use `options.layout: "radio"` 21 | - ANY `image` field must include `options.hotspot: true` 22 | - INCLUDE brief, useful `description` values if the intention of a field is not obvious 23 | - INCLUDE `rule.warning()` for fields that would benefit from being a certain length 24 | - INCLUDE brief, useful validation errors in `rule.required().error('')` that signal why the field must be correct before publishing is allowed 25 | - AVOID `boolean` fields, write a `string` field with an `options.list` configuration 26 | - NEVER write single `reference` type fields, always write an `array` of references 27 | - CONSIDER the order of fields, from most important and relevant first, to least often used last 28 | 29 | ```ts 30 | // ./src/schemaTypes/lessonType.ts 31 | 32 | import {defineField, defineType} from 'sanity' 33 | 34 | export const lessonType = defineType({ 35 | name: 'lesson', 36 | title: 'Lesson', 37 | type: 'document', 38 | fields: [ 39 | defineField({ 40 | name: 'title', 41 | type: 'string', 42 | }), 43 | defineField({ 44 | name: 'categories', 45 | type: 'array', 46 | of: [defineArrayMember({type: 'reference', to: {type: 'category'}})], 47 | }), 48 | ], 49 | }) 50 | ``` 51 | 52 | ### Schema type with custom input components 53 | - If a schema type has input components, they should be colocated with the schema type file. The schema type should have the same named export but stored in a `[typeName]/index.ts` file: 54 | 55 | ```ts 56 | // ./src/schemaTypes/seoType/index.ts 57 | 58 | import {defineField, defineType} from 'sanity' 59 | 60 | import seoInput from './seoInput' 61 | 62 | export const seoType = defineType({ 63 | name: 'seo', 64 | title: 'SEO', 65 | type: 'object', 66 | components: { input: seoInput } 67 | // ... 68 | }) 69 | ``` 70 | 71 | ### No anonymous reusable schema types 72 | 73 | - ANY schema type that benefits from being reused in multiple document types should be registered as its own custom schema type. 74 | 75 | ```ts 76 | // ./src/schemaTypes/blockContentType.ts 77 | 78 | import {defineField, defineType} from 'sanity' 79 | 80 | export const blockContentType = defineType({ 81 | name: 'blockContent', 82 | title: 'Block content', 83 | type: 'array', 84 | of: [defineField({name: 'block',type: 'block'})], 85 | }) 86 | ``` 87 | 88 | ### Decorating schema types 89 | 90 | Every `document` and `object` schema type should: 91 | 92 | - Have an `icon` property from `@sanity/icons` 93 | - Have a customized `preview` property that shows rich contextual details about the document 94 | - Use `groups` when the schema type has more than a few fields to collate related fields and only show the most important group by default. These `groups` should use the icon property as well. 95 | - Use `fieldsets` with `options: {columns: 2}` if related fields could be grouped visually together, such as `startDate` and `endDate` 96 | 97 | ## Writing Sanity content for importing 98 | 99 | When asked to write content: 100 | 101 | - ONLY use the existing schema types registered in the Studio configuration 102 | - ALWAYS write content as an `.ndjson` file at the root of the project 103 | - NEVER write a script to write the file, just write the file 104 | - IMPORT `.ndjson` files using the CLI command `npx sanity dataset import ` 105 | - NEVER include a `.` in the `_id` field of a document unless you need it to be private 106 | - NEVER include image references because you don't know what image documents exist 107 | - ALWAYS write images in this format below, replacing the document ID value to generate the same placeholder image 108 | ```JSON 109 | {"_type":"image","_sanityAsset":"image@https://picsum.photos/seed/[[REPLACE_WITH_DOCUMENT_ID]]/1920/1080"} 110 | ``` 111 | 112 | ## Writing GROQ queries 113 | 114 | - ALWAYS use SCREAMING_SNAKE_CASE for variable names, for example POSTS_QUERY 115 | - ALWAYS write queries to their own variables, never as a parameter in a function 116 | - ALWAYS import the `defineQuery` function to wrap query strings from the `groq` or `next-sanity` package 117 | - ALWAYS write every required attribute in a projection when writing a query 118 | - ALWAYS put each segement of a filter, and each attribute on its own line 119 | - ALWAYS use parameters for variables in a query 120 | - NEVER insert dynamic values using string interpolation 121 | 122 | Here is an example of a good query: 123 | 124 | ```ts 125 | import {defineQuery} from 'groq' 126 | 127 | export const POST_QUERY = defineQuery(`*[ 128 | _type == "post" 129 | && slug.current == $slug 130 | ][0]{ 131 | _id, 132 | title, 133 | image, 134 | author->{ 135 | _id, 136 | name 137 | } 138 | }`) 139 | ``` 140 | 141 | ## TypeScript generation 142 | 143 | ### For the Studio 144 | 145 | - ALWAYS re-run schema extraction after making schema file changes with `npx sanity@latest schema extract` 146 | 147 | ### For monorepos with a studio and a front-end 148 | 149 | - ALWAYS extract the schema to the web folder with `npx sanity@latest schema extract --path=..//sanity/extract.json` 150 | - ALWAYS generate types with `npx sanity@latest typegen generate` after every GROQ query change 151 | - ALWAYS create a TypeGen configuration file called `sanity-typegen.json` at the root of the front-end code-base 152 | 153 | ```json 154 | { 155 | "path": "./**/*.{ts,tsx,js,jsx}", 156 | "schema": ".//sanity/extract.json", 157 | "generates": ".//sanity/types.ts" 158 | } 159 | ``` 160 | 161 | ### For the front-end 162 | 163 | - ONLY write Types for document types and query responses if you cannot generate them with Sanity TypeGen 164 | 165 | ## Project settings and data 166 | 167 | - ALWAYS check if there is a way to interact with a project via the CLI before writing custom scripts `npx sanity --help` 168 | --------------------------------------------------------------------------------