Skip to main content

Sanity Studio CMS

The Sanity Studio application serves as Charles Schwab's comprehensive content management system, providing content creators, editors, and administrators with powerful tools for authoring, managing, and publishing content across multiple digital properties. Built on Sanity CMS v3, this studio application offers multi-workspace support, internationalization, advanced content modeling, and enterprise-grade editorial workflows.

Overview

  • Application: apps/sanity-studio
  • Port: 3333
  • Version: 1.0.9
  • Framework: Next.js 15.3.2 with Sanity Studio v3.81.0
  • Purpose: Content management and editorial publishing system
  • Access: Multi-workspace CMS for content teams and administrators

Key Features

  • Multi-Workspace Architecture: Separate editorial environments for different brands/properties
  • SAML Authentication: Enterprise Single Sign-On with Charles Schwab identity systems
  • Internationalization: Multi-language content support with document-level translations
  • Advanced Content Schemas: Custom document types for stories, landing pages, and structured content
  • Asset Management: Integrated Bynder and Unsplash asset sourcing
  • Presentation Tool: Real-time preview and content editing workflows
  • Taxonomy Management: Hierarchical content categorization and tagging system
  • AI-Powered Assistance: Sanity Assist integration for content optimization
  • Custom UI Components: Branded Charles Schwab studio interface

Technical Architecture

Framework Stack

TechnologyVersionPurpose
Next.js15.3.2React framework hosting Sanity Studio
Sanity3.81.0Headless CMS platform and studio interface
React19.1.0Component library for custom UI elements
TypeScript5.8.2Type safety for schemas and configurations
Styled Components6.1.16Component-level styling and theming
RxJS7.8.2Reactive programming for real-time updates

Core Dependencies

Key dependencies from package.json
{
"@sanity/assist": "^3.2.2",
"@sanity/code-input": "^5.1.2",
"@sanity/document-internationalization": "^3.3.1",
"@sanity/image-url": "^1.1.0",
"@sanity/table": "^1.1.3",
"@sanity/ui": "^2.15.10",
"@sanity/vision": "^3.81.0",
"next-sanity": "^9.9.6",
"sanity": "^3.81.0",
"sanity-plugin-asset-source-unsplash": "^3.0.3",
"sanity-plugin-bynder-input": "^2.2.0",
"sanity-plugin-studio-smartling": "^4.3.0",
"sanity-plugin-taxonomy-manager": "^3.2.9"
}

Sanity Plugins Ecosystem

  • @sanity/assist: AI-powered content assistance and translation
  • @sanity/vision: GROQ query sandbox for administrators
  • @sanity/table: Rich table editing capabilities
  • sanity-plugin-bynder-input: Digital asset management integration
  • sanity-plugin-taxonomy-manager: Hierarchical content organization
  • sanity-plugin-studio-smartling: Professional translation workflow

Directory Structure

apps/sanity-studio/
├── src/
│ ├── app/ # Next.js App Router structure
│ │ ├── [[...tool]]/ # Catch-all route for Sanity Studio
│ │ │ └── page.tsx # Main studio page component
│ │ ├── api/ # API route handlers
│ │ ├── favicon.ico # Studio favicon
│ │ ├── globals.css # Global CSS styles
│ │ ├── layout.tsx # Root layout component
│ │ └── page.module.css # Page-specific styles
│ ├── components/ # Custom Sanity Studio components
│ │ ├── inputs/ # Custom input components
│ │ ├── preview/ # Content preview components
│ │ └── schwab-logo.tsx # Charles Schwab branding
│ ├── data/ # Static data and configurations
│ ├── desk/ # Studio structure configuration
│ │ └── deskStructure.ts # Content navigation structure
│ ├── presentation/ # Presentation tool configuration
│ │ └── locate.ts # Content location mapping
│ └── schemas/ # Content schema definitions
│ ├── documents/ # Document type schemas
│ │ ├── bynder-block.tsx # Bynder asset integration
│ │ ├── card-deck.tsx # Card component schemas
│ │ ├── data-table.tsx # Data table document type
│ │ ├── dynamic-cta.tsx # Call-to-action components
│ │ ├── landing-page.ts # Landing page schema
│ │ ├── query-set.tsx # Query-based content
│ │ ├── story.tsx # Story/article schema
│ │ ├── taxonomy-*.tsx # Taxonomy management schemas
│ │ └── index.ts # Schema exports
│ ├── objects/ # Object type schemas
│ │ ├── button.tsx # Button component schema
│ │ ├── marquee.tsx # Marquee component schema
│ │ ├── seo-*.tsx # SEO metadata schemas
│ │ └── index.ts # Object exports
│ └── index.ts # Main schema index
├── public/ # Static assets (minimal for studio)
├── jest.config.js # Jest testing configuration
├── jest.cicd.config.js # CI/CD Jest configuration
├── next.config.mjs # Next.js configuration
├── sanity.cli.ts # Sanity CLI configuration
├── sanity.config.ts # Main Sanity Studio configuration
└── tsconfig.json # TypeScript configuration

Multi-Workspace Configuration

Workspace Architecture

Multi-workspace setup
const allConfigs: Config = [
{
name: 'default',
title: 'Charles Schwab',
basePath: '/default',
},
{
name: 'charitable',
title: 'Charitable',
basePath: '/charitable',
},
].map((config) => {
return {
...sharedConfig,
...config,
};
});

export default defineConfig(allConfigs);

Shared Configuration

Shared workspace configuration
const sharedConfig: Config = {
icon: SchwabLogo,
projectId: SANITY_STUDIO_PROJECT_ID,
dataset: 'production',
auth: createAuthStore({
projectId: 'fvuvea00',
dataset: 'production',
redirectOnSingle: false,
mode: 'replace',
providers: [
{
name: 'saml',
title: 'Login with Schwab',
url: 'https://api.sanity.io/v2021-10-01/auth/saml/login/c6e8fbc1',
},
],
loginMethod: 'dual',
}),
}

Authentication System

SAML Integration

Role-Based Access Control

Role-based tool access
// Admin-only tools configuration
const adminTools = [visionTool()];

// Role-based workspace filtering (commented for current implementation)
// const editorConfigs = allConfigs.filter(
// (config) => config.name !== 'charitable',
// );
// const configs = currentUser.role === 'administrator' ? allConfigs : editorConfigs

Content Schema Architecture

Document Types

SchemaPurposeFeatures
storyArticles and editorial contentMulti-language support, SEO metadata
landingPageMarketing and product pagesComponent-based layout system
cardDeckReusable card componentsIcon cards, CTA cards, content cards
dataTableStructured data presentationSortable, filterable table content
dynamicCTACall-to-action componentsA/B testing, conversion tracking
taxonomyTermContent categorizationHierarchical taxonomy management

Story Schema Example

Story document schema
export default defineType({
name: 'story',
title: 'Story',
type: 'document',
icon: () => <BookText />,
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
group: 'content',
},
{
name: 'slug',
type: 'slug',
title: 'Slug',
options: {
source: 'title',
},
group: 'content',
},
{
name: 'summary',
type: 'text',
title: 'Summary',
group: 'content',
},
// Additional fields for internationalization, SEO, etc.
],
});

Schema Templates System

Custom schema templates
schema: {
types: schemaTypes,
templates: (prev) => {
return [
...prev,
{
id: 'story-language',
title: 'Story with Language',
schemaType: 'story',
parameters: [{ name: 'language', type: 'string' }],
value: (params: { language: string }) => ({
language: params.language,
}),
},
{
id: 'icon-card-deck',
title: 'Icon card',
schemaType: 'cardDeck',
value: {
cardType: 'iconCard',
// Preset card configuration
},
},
];
},
}

Internationalization System

Supported Languages

Multi-language configuration
export const supportedLanguages = [
{ id: 'zh-CN', title: 'Chinese (China)' },
{ id: 'zh-TW', title: 'Chinese (Taiwan-Traditional)' },
{ id: 'es-US', title: 'Spanish (US)' },
{ id: 'en-US', title: 'English (US)' },
];

Document Internationalization

i18n plugin configuration
documentInternationalization({
supportedLanguages,
schemaTypes: ['story', 'landingPage'],
})

AI-Powered Translation

Sanity Assist translation
assist({
translate: {
document: {
languageField: 'language',
documentTypes: ['story'],
},
},
})

Studio Structure Configuration

Content Organization

Desk structure configuration
export const deskStructure = (S: StructureBuilder): ListBuilder => {
return S.list()
.title('Content')
.items([
S.listItem()
.title('Landing pages')
.icon(Layout)
.child(S.documentTypeList('landingPage').title('All pages')),

S.listItem()
.title('Stories')
.icon(BookText)
.child(
S.list()
.title('Stories')
.items([
...supportedLanguages.map((language) =>
S.listItem()
.title(`Stories (${language.id.toLocaleUpperCase()})`)
.schemaType('story')
.child(
S.documentList()
.title(`${language.title} Stories`)
.filter('_type == "story" && language == $language')
.params({ language: language.id })
)
)
])
),
// Additional content types...
]);
};

Asset Management Integration

Bynder Integration

Bynder asset plugin
bynderInputPlugin({
portalDomain: process.env.NEXT_PUBLIC_SANITY_STUDIO_BYNDER_PORTAL_DOMAIN ||
'https://wave-trial.getbynder.com/',
})

Unsplash Integration

Unsplash image sourcing
unsplashImageAsset()

Asset Flow

Presentation Tool Integration

Live Preview Configuration

Presentation tool setup
presentationTool({
previewUrl: {
origin: SANITY_STUDIO_PREVIEW_URL,
draftMode: {
enable: '/api/draft',
},
},
locate,
})

Content Location Mapping

Content location resolution
// src/presentation/locate.ts
export const locate = (params, context) => {
// Map document types to preview URLs
if (params.type === 'story') {
return {
locations: [
{
title: params.slug || 'New Story',
href: `/stories/${params.slug}`,
},
],
};
}
// Additional document type mappings...
};

Development Workflow

Local Development

Development commands
# Install dependencies
pnpm install

# Start development server on port 3333
pnpm dev

# Build for production
pnpm build

# Start production server
pnpm start

# Generate TypeScript types from schemas
pnpm typegen

# Type checking
pnpm type-check

# Code conformance checking
pnpm conformance

Schema Development

Schema management commands
# Extract schema definitions
sanity schema extract

# Generate TypeScript types
sanity typegen generate

# Deploy schema changes
sanity deploy

Testing Architecture

Testing Configuration

Jest testing setup
{
"@faker-js/faker": "^8.4.1",
"@jest/globals": "^29.7.0",
"@swc/jest": "^0.2.37",
"jest": "^29.7.0",
"jest-expect-message": "^1.1.3",
"jest-extended": "^4.0.2",
"jest-mock-extended": "^3.0.7"
}

Schema Testing

Schema validation testing
// Example schema test
import { faker } from '@faker-js/faker';
import { expect, test } from '@jest/globals';

test('story schema validation', () => {
const mockStory = {
_type: 'story',
title: faker.lorem.sentence(),
slug: { current: faker.lorem.slug() },
summary: faker.lorem.paragraph(),
};

expect(mockStory).toMatchSchema('story');
});

Security Configuration

Next.js Security Headers

Security headers configuration
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'Referrer-Policy', value: 'origin-when-cross-origin' },
{ key: 'Content-Security-Policy', value: "default-src 'self';" },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
],
},
];
}

API Security

API route security
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
{ key: 'Access-Control-Allow-Origin', value: '*' },
{ key: 'Access-Control-Allow-Methods', value: 'GET,OPTIONS' },
{
key: 'Access-Control-Allow-Headers',
value: 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version,x-vercel-protection-bypass,correlatorid,schwab-client-appid,schwab-client-channel,schwab-client-correlid,schwab-environment,schwab-environment-region',
},
],
}

Advanced Features

Custom Input Components

Custom taxonomy input
// ParentAttributes component for taxonomy terms
form: {
components: {
input: (props) => {
if (
props.id === 'root' &&
props.schemaType.type?.name === 'document' &&
props.schemaType.name === 'taxonomyTerm'
) {
return ParentAttributes(props);
}
return props.renderDefault(props);
},
},
}

Comments System

Document comments feature
document: {
unstable_comments: {
enabled: true,
},
}

Taxonomy Management

Taxonomy manager configuration
taxonomyManager({ 
baseUri: 'https://www.schwab.com/vocab/'
})

Content Publishing Workflow

Editorial Process

Common Use Cases

Content Creation Workflow

Typical content workflow
// 1. Editor creates new story
const newStory = {
_type: 'story',
title: 'Investment Strategies for 2025',
language: 'en-US',
summary: 'Comprehensive guide to...',
};

// 2. Use AI assist for optimization
// 3. Add translations for other languages
// 4. Preview in presentation tool
// 5. Publish to production

Multi-Language Content Management

Language-specific content handling
// Language-based content filtering in desk structure
S.documentList()
.title(`${language.title} Stories`)
.filter('_type == "story" && language == $language')
.params({ language: language.id })

Integration Patterns

External Service Integration

ServiceIntegrationPurpose
BynderAsset source pluginDigital asset management
UnsplashAsset source pluginStock photography
SmartlingTranslation pluginProfessional translation
Charles Schwab IdentitySAML authenticationEnterprise SSO
Next.js PreviewPresentation toolReal-time content preview

API Integration

  • Sanity Client: Headless CMS data layer
  • GROQ Queries: Content retrieval and filtering
  • Webhook Integration: Real-time content synchronization
  • CDN Integration: Optimized asset delivery

Performance Optimization

Studio Configuration

Performance optimizations
const nextConfig = {
reactStrictMode: true,
trailingSlash: false,
// Optimized for Sanity Studio hosting
}

Caching Strategy

  • Asset CDN: Sanity's global CDN for image delivery
  • Query Caching: GROQ query result caching
  • Studio Caching: React component and UI caching
  • Build Optimization: Next.js static generation

Troubleshooting

Common Issues

IssueCauseSolution
Port 3333 in usePrevious process runningUse kill script in dev command
SAML authentication failsConfiguration mismatchVerify SAML provider settings
Schema validation errorsType definition conflictsRun pnpm typegen
Asset upload failuresPlugin configurationCheck Bynder/Unsplash settings
Build failuresTypeScript errorsRun pnpm type-check
Plugin conflictsVersion incompatibilitiesUpdate plugin dependencies

Debug Tips

Schema Development

Use the Vision tool (admin-only) to test GROQ queries and debug content relationships in real-time.

Production Changes

Always test schema changes in development before deploying to production. Schema changes can affect existing content.

Future Enhancements

  • Advanced Workflow Management: Custom editorial approval workflows
  • Enhanced AI Integration: GPT-powered content generation and optimization
  • Advanced Analytics: Content performance tracking and optimization
  • Custom Dashboard: Editorial team productivity metrics
  • Enhanced Personalization: Dynamic content based on user segments
  • API Extensions: Custom Sanity API endpoints for specialized workflows

This Sanity Studio application serves as Charles Schwab's central content management hub, providing enterprise-grade editorial tools, multi-workspace collaboration, and sophisticated content workflows that power digital experiences across multiple properties and languages.