├── .eslintrc ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── sanity.cli.ts ├── sanity.config.tsx ├── schemas ├── formType.ts ├── heroType.ts ├── imageGalleryType.ts ├── index.ts ├── pageType.ts ├── promotionType.ts ├── textWithIllustrationType.ts └── videoType.ts ├── static └── .gitkeep └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/eslint-config-studio", 3 | "plugins": ["prettier", "simple-import-sort"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "simple-import-sort/imports": "error", 7 | "simple-import-sort/exports": "error" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # Compiled Sanity Studio 9 | /dist 10 | 11 | # Temporary Sanity runtime, generated by the CLI on every dev server start 12 | /.sanity 13 | 14 | # Logs 15 | /logs 16 | *.log 17 | 18 | # Coverage directory used by testing tools 19 | /coverage 20 | 21 | # Misc 22 | .DS_Store 23 | *.pem 24 | 25 | # Typescript 26 | *.tsbuildinfo 27 | 28 | # Dotenv and similar local-only files 29 | *.local 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Page Building Example 2 | 3 | A Sanity Studio project configured with a very simple page builder. Companion code for the guide "[How to use structured content for page building](https://www.sanity.io/guides/how-to-use-structured-content-for-page-building)" on the Sanity.io Exchange. 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "page-builder", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "package.json", 6 | "license": "UNLICENSED", 7 | "scripts": { 8 | "dev": "sanity dev", 9 | "start": "sanity start", 10 | "build": "sanity build", 11 | "deploy": "sanity deploy", 12 | "deploy-graphql": "sanity graphql deploy" 13 | }, 14 | "keywords": [ 15 | "sanity" 16 | ], 17 | "dependencies": { 18 | "@sanity/vision": "^3.12.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-is": "^18.2.0", 22 | "sanity": "^3.12.1", 23 | "sanity-plugin-asset-source-unsplash": "^1.1.0", 24 | "styled-components": "^5.3.9" 25 | }, 26 | "devDependencies": { 27 | "@sanity/eslint-config-studio": "^2.0.1", 28 | "@types/react": "^18.0.25", 29 | "@types/styled-components": "^5.1.26", 30 | "eslint": "^8.6.0", 31 | "eslint-plugin-prettier": "^4.2.1", 32 | "eslint-plugin-simple-import-sort": "^10.0.0", 33 | "prettier": "2.8.8", 34 | "typescript": "^4.9.5" 35 | }, 36 | "prettier": { 37 | "semi": false, 38 | "printWidth": 100, 39 | "bracketSpacing": false, 40 | "singleQuote": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sanity.cli.ts: -------------------------------------------------------------------------------- 1 | import {defineCliConfig} from 'sanity/cli' 2 | 3 | export default defineCliConfig({ 4 | api: { 5 | projectId: '2ak90yby', 6 | dataset: 'production' 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /sanity.config.tsx: -------------------------------------------------------------------------------- 1 | import {visionTool} from '@sanity/vision' 2 | import {defineConfig} from 'sanity' 3 | import {deskTool} from 'sanity/desk' 4 | import {unsplashImageAsset} from 'sanity-plugin-asset-source-unsplash' 5 | 6 | import {schemaTypes} from './schemas' 7 | 8 | export default defineConfig({ 9 | name: 'default', 10 | title: 'Page Builder', 11 | 12 | projectId: '2ak90yby', 13 | dataset: 'production', 14 | 15 | plugins: [deskTool(), visionTool(), unsplashImageAsset()], 16 | 17 | schema: { 18 | types: schemaTypes, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /schemas/formType.ts: -------------------------------------------------------------------------------- 1 | // ./schemas/formType.js 2 | 3 | import {EnvelopeIcon} from '@sanity/icons' 4 | import {defineField, defineType} from 'sanity' 5 | 6 | export const formType = defineType({ 7 | name: 'form', 8 | type: 'object', 9 | fields: [ 10 | defineField({ 11 | name: 'label', 12 | type: 'string', 13 | }), 14 | defineField({ 15 | name: 'heading', 16 | type: 'string', 17 | }), 18 | defineField({ 19 | name: 'form', 20 | type: 'string', 21 | description: 'Select form type', 22 | options: { 23 | list: ['newsletter', 'register', 'contact'], 24 | }, 25 | }), 26 | ], 27 | icon: EnvelopeIcon, 28 | preview: { 29 | select: { 30 | heading: 'heading', 31 | form: 'form', 32 | }, 33 | prepare({heading, form}) { 34 | return { 35 | title: heading || 'Untitled', 36 | subtitle: form ? `${form.charAt(0).toUpperCase() + form.slice(1)} form` : 'Form', 37 | media: EnvelopeIcon, 38 | } 39 | }, 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /schemas/heroType.ts: -------------------------------------------------------------------------------- 1 | // ./schemas/heroType.ts 2 | 3 | import {DocumentTextIcon} from '@sanity/icons' 4 | import {defineField, defineType} from 'sanity' 5 | 6 | export const heroType = defineType({ 7 | name: 'hero', 8 | type: 'object', 9 | title: 'Hero', 10 | fields: [ 11 | defineField({ 12 | name: 'heading', 13 | type: 'string', 14 | }), 15 | defineField({ 16 | name: 'tagline', 17 | type: 'string', 18 | }), 19 | defineField({ 20 | name: 'image', 21 | type: 'image', 22 | options: {hotspot: true}, 23 | fields: [ 24 | defineField({ 25 | name: 'alt', 26 | type: 'string', 27 | title: 'Alternative text', 28 | }), 29 | ], 30 | }), 31 | ], 32 | icon: DocumentTextIcon, 33 | preview: { 34 | select: { 35 | title: 'heading', 36 | image: 'image', 37 | }, 38 | prepare({title, image}) { 39 | return { 40 | title: title || 'Untitled', 41 | subtitle: 'Hero', 42 | media: image || DocumentTextIcon, 43 | } 44 | }, 45 | }, 46 | }) 47 | -------------------------------------------------------------------------------- /schemas/imageGalleryType.ts: -------------------------------------------------------------------------------- 1 | // imageGallery.js 2 | 3 | import {ImagesIcon} from '@sanity/icons' 4 | import {defineField, defineType} from 'sanity' 5 | 6 | export const imageGalleryType = defineType({ 7 | name: 'gallery', 8 | type: 'object', 9 | title: 'Gallery', 10 | fields: [ 11 | { 12 | name: 'images', 13 | type: 'array', 14 | of: [ 15 | defineField({ 16 | name: 'image', 17 | type: 'image', 18 | options: {hotspot: true}, 19 | fields: [ 20 | { 21 | name: 'alt', 22 | type: 'string', 23 | title: 'Alternative text', 24 | }, 25 | ], 26 | }), 27 | ], 28 | options: { 29 | layout: 'grid', 30 | }, 31 | }, 32 | ], 33 | icon: ImagesIcon, 34 | preview: { 35 | select: { 36 | images: 'images', 37 | }, 38 | prepare({images}) { 39 | return { 40 | title: images 41 | ? `${images.length === 1 ? `1 image` : `${images.length} images`} ` 42 | : 'No images', 43 | subtitle: 'Gallery', 44 | media: images ? images[0] : ImagesIcon, 45 | } 46 | }, 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /schemas/index.ts: -------------------------------------------------------------------------------- 1 | // ./schemas/index.ts 2 | 3 | import {formType} from './formType' 4 | import {heroType} from './heroType' 5 | import {imageGalleryType} from './imageGalleryType' 6 | import {pageType} from './pageType' 7 | import {promotionType} from './promotionType' 8 | import {textWithIllustrationType} from './textWithIllustrationType' 9 | import {videoType} from './videoType' 10 | 11 | export const schemaTypes = [ 12 | pageType, 13 | promotionType, 14 | heroType, 15 | textWithIllustrationType, 16 | imageGalleryType, 17 | formType, 18 | videoType, 19 | ] 20 | -------------------------------------------------------------------------------- /schemas/pageType.ts: -------------------------------------------------------------------------------- 1 | // ./schemas/pageType.ts 2 | 3 | import {DocumentIcon} from '@sanity/icons' 4 | import {defineArrayMember, defineField, defineType} from 'sanity' 5 | 6 | export const pageType = defineType({ 7 | name: 'page', 8 | type: 'document', 9 | title: 'Page', 10 | fields: [ 11 | defineField({name: 'title', type: 'string'}), 12 | defineField({ 13 | name: 'pageBuilder', 14 | type: 'array', 15 | title: 'Page builder', 16 | of: [ 17 | defineArrayMember({ 18 | name: 'hero', 19 | type: 'hero', 20 | }), 21 | defineArrayMember({ 22 | name: 'textWithIllustration', 23 | type: 'textWithIllustration', 24 | }), 25 | defineArrayMember({ 26 | name: 'gallery', 27 | type: 'gallery', 28 | }), 29 | defineArrayMember({ 30 | name: 'form', 31 | type: 'form', 32 | }), 33 | defineArrayMember({ 34 | name: 'video', 35 | type: 'video', 36 | }), 37 | defineArrayMember({ 38 | name: 'callToAction', 39 | type: 'reference', 40 | to: [{type: 'promotion'}], 41 | }), 42 | // etc... 43 | ], 44 | }), 45 | ], 46 | icon: DocumentIcon, 47 | }) 48 | -------------------------------------------------------------------------------- /schemas/promotionType.ts: -------------------------------------------------------------------------------- 1 | // ./schemas/promotionType.ts 2 | 3 | import {StarIcon} from '@sanity/icons' 4 | import {defineField, defineType} from 'sanity' 5 | 6 | export const promotionType = defineType({ 7 | name: 'promotion', 8 | type: 'document', 9 | title: 'Promotion', 10 | fields: [ 11 | defineField({ 12 | name: 'title', 13 | type: 'string', 14 | }), 15 | defineField({ 16 | name: 'link', 17 | type: 'url', 18 | }), 19 | ], 20 | icon: StarIcon, 21 | preview: { 22 | select: { 23 | title: 'title', 24 | }, 25 | prepare({title}) { 26 | return { 27 | title: title || 'Untitled', 28 | subtitle: 'Promotion', 29 | media: StarIcon, 30 | } 31 | }, 32 | }, 33 | }) 34 | -------------------------------------------------------------------------------- /schemas/textWithIllustrationType.ts: -------------------------------------------------------------------------------- 1 | // ./schemas/textWithIllustration.js 2 | 3 | import {ImageIcon} from '@sanity/icons' 4 | import {defineField, defineType} from 'sanity' 5 | 6 | export const textWithIllustrationType = defineType({ 7 | name: 'textWithIllustration', 8 | type: 'object', 9 | title: 'Text with Illustration', 10 | fields: [ 11 | defineField({ 12 | name: 'heading', 13 | type: 'string', 14 | }), 15 | defineField({ 16 | name: 'tagline', 17 | type: 'string', 18 | }), 19 | defineField({ 20 | name: 'excerpt', 21 | type: 'text', 22 | }), 23 | defineField({ 24 | name: 'image', 25 | type: 'image', 26 | options: {hotspot: true}, 27 | fields: [ 28 | defineField({ 29 | name: 'alt', 30 | type: 'string', 31 | title: 'Alternative text', 32 | }), 33 | ], 34 | }), 35 | ], 36 | icon: ImageIcon, 37 | preview: { 38 | select: { 39 | title: 'heading', 40 | image: 'image', 41 | }, 42 | prepare({title, image}) { 43 | return { 44 | title: title || 'Untitled', 45 | subtitle: 'Text with Illustration', 46 | media: image || ImageIcon, 47 | } 48 | }, 49 | }, 50 | }) 51 | -------------------------------------------------------------------------------- /schemas/videoType.ts: -------------------------------------------------------------------------------- 1 | // ./schemas/videoType.js 2 | 3 | import {DocumentVideoIcon} from '@sanity/icons' 4 | import {defineField, defineType} from 'sanity' 5 | 6 | export const videoType = defineType({ 7 | name: 'video', 8 | type: 'object', 9 | fields: [ 10 | defineField({ 11 | name: 'videoLabel', 12 | type: 'string', 13 | }), 14 | defineField({ 15 | name: 'url', 16 | type: 'string', 17 | title: 'URL', 18 | }), 19 | ], 20 | icon: DocumentVideoIcon, 21 | preview: { 22 | select: { 23 | title: 'videoLabel', 24 | }, 25 | prepare({title}) { 26 | return { 27 | title: title || 'Untitled', 28 | subtitle: 'Video', 29 | media: DocumentVideoIcon, 30 | } 31 | }, 32 | }, 33 | }) 34 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- 1 | Files placed here will be served by the Sanity server under the `/static`-prefix 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------