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
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 15.3.2 | React framework hosting Sanity Studio |
| Sanity | 3.81.0 | Headless CMS platform and studio interface |
| React | 19.1.0 | Component library for custom UI elements |
| TypeScript | 5.8.2 | Type safety for schemas and configurations |
| Styled Components | 6.1.16 | Component-level styling and theming |
| RxJS | 7.8.2 | Reactive programming for real-time updates |
Core Dependencies
{
"@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 capabilitiessanity-plugin-bynder-input: Digital asset management integrationsanity-plugin-taxonomy-manager: Hierarchical content organizationsanity-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
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
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
// 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
| Schema | Purpose | Features |
|---|---|---|
story | Articles and editorial content | Multi-language support, SEO metadata |
landingPage | Marketing and product pages | Component-based layout system |
cardDeck | Reusable card components | Icon cards, CTA cards, content cards |
dataTable | Structured data presentation | Sortable, filterable table content |
dynamicCTA | Call-to-action components | A/B testing, conversion tracking |
taxonomyTerm | Content categorization | Hierarchical taxonomy management |
Story Schema Example
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
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
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
documentInternationalization({
supportedLanguages,
schemaTypes: ['story', 'landingPage'],
})
AI-Powered Translation
assist({
translate: {
document: {
languageField: 'language',
documentTypes: ['story'],
},
},
})
Studio Structure Configuration
Content Organization
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
bynderInputPlugin({
portalDomain: process.env.NEXT_PUBLIC_SANITY_STUDIO_BYNDER_PORTAL_DOMAIN ||
'https://wave-trial.getbynder.com/',
})
Unsplash Integration
unsplashImageAsset()
Asset Flow
Presentation Tool Integration
Live Preview Configuration
presentationTool({
previewUrl: {
origin: SANITY_STUDIO_PREVIEW_URL,
draftMode: {
enable: '/api/draft',
},
},
locate,
})
Content Location Mapping
// 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
# 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
# Extract schema definitions
sanity schema extract
# Generate TypeScript types
sanity typegen generate
# Deploy schema changes
sanity deploy
Testing Architecture
Testing Configuration
{
"@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
// 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
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
{
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
// 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: {
unstable_comments: {
enabled: true,
},
}
Taxonomy Management
taxonomyManager({
baseUri: 'https://www.schwab.com/vocab/'
})
Content Publishing Workflow
Editorial Process
Common Use Cases
Content Creation 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-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
| Service | Integration | Purpose |
|---|---|---|
| Bynder | Asset source plugin | Digital asset management |
| Unsplash | Asset source plugin | Stock photography |
| Smartling | Translation plugin | Professional translation |
| Charles Schwab Identity | SAML authentication | Enterprise SSO |
| Next.js Preview | Presentation tool | Real-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
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
| Issue | Cause | Solution |
|---|---|---|
| Port 3333 in use | Previous process running | Use kill script in dev command |
| SAML authentication fails | Configuration mismatch | Verify SAML provider settings |
| Schema validation errors | Type definition conflicts | Run pnpm typegen |
| Asset upload failures | Plugin configuration | Check Bynder/Unsplash settings |
| Build failures | TypeScript errors | Run pnpm type-check |
| Plugin conflicts | Version incompatibilities | Update plugin dependencies |
Debug Tips
Use the Vision tool (admin-only) to test GROQ queries and debug content relationships in real-time.
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.