Skip to content

Studio Customization

Trokky Studio is the React-based admin interface for managing content. It can be customized to match your brand and workflow.

Studio features include:

  • Document editing with auto-generated forms
  • Media library with drag-and-drop uploads
  • Reference browsing and linking
  • Dark mode support
  • Customizable navigation and branding
const trokky = await TrokkyExpress.create({
studio: {
enabled: true,
branding: {
title: 'My CMS',
},
},
// ...
});
studio: {
branding: {
title: 'Acme CMS',
logo: '/logo.svg', // URL or path
logoAlt: 'Acme Logo',
favicon: '/favicon.ico',
},
}
studio: {
branding: {
colors: {
primary: '#3b82f6', // Primary brand color
secondary: '#64748b', // Secondary color
accent: '#f59e0b', // Accent color
},
},
}
studio: {
branding: {
customCss: `
:root {
--studio-sidebar-bg: #1a1a2e;
--studio-header-bg: #16213e;
}
`,
},
}

By default, Studio generates navigation from your schemas. Customize with the structure option:

studio: {
structure: [
{
type: 'list',
schemaType: 'post',
title: 'Blog Posts',
icon: 'document',
},
{
type: 'list',
schemaType: 'author',
title: 'Authors',
icon: 'user',
},
{
type: 'divider',
},
{
type: 'singleton',
schemaType: 'siteSettings',
title: 'Settings',
icon: 'cog',
},
],
}
studio: {
structure: [
{
type: 'group',
title: 'Content',
icon: 'folder',
items: [
{ type: 'list', schemaType: 'post', title: 'Posts' },
{ type: 'list', schemaType: 'page', title: 'Pages' },
],
},
{
type: 'group',
title: 'Taxonomy',
items: [
{ type: 'list', schemaType: 'category', title: 'Categories' },
{ type: 'list', schemaType: 'tag', title: 'Tags' },
],
},
{
type: 'group',
title: 'Settings',
items: [
{ type: 'singleton', schemaType: 'siteSettings' },
{ type: 'singleton', schemaType: 'navigation' },
],
},
],
}

Generate structure dynamically based on user:

studio: {
structure: (user) => {
const items = [
{ type: 'list', schemaType: 'post', title: 'Posts' },
{ type: 'list', schemaType: 'page', title: 'Pages' },
];
// Admin-only sections
if (user.role === 'admin') {
items.push({
type: 'group',
title: 'Administration',
items: [
{ type: 'singleton', schemaType: 'siteSettings' },
{ type: 'link', title: 'Users', url: '/studio/users' },
],
});
}
return items;
},
}
{
type: 'list',
schemaType: 'post',
title: 'Published Posts',
filter: {
status: 'published',
},
}
{
type: 'list',
schemaType: 'post',
title: 'Posts',
defaultOrdering: {
field: 'publishedAt',
direction: 'desc',
},
}
{
type: 'list',
schemaType: 'post',
columns: [
{ field: 'title', title: 'Title', width: '40%' },
{ field: 'author.name', title: 'Author', width: '20%' },
{ field: 'status', title: 'Status', width: '15%' },
{ field: '_updatedAt', title: 'Updated', width: '25%' },
],
}

Organize fields into collapsible groups:

// In schema definition
{
name: 'post',
title: 'Blog Post',
groups: [
{ name: 'content', title: 'Content' },
{ name: 'meta', title: 'Meta', collapsed: true },
{ name: 'seo', title: 'SEO', collapsed: true },
],
fields: [
{ name: 'title', type: 'string', group: 'content' },
{ name: 'content', type: 'richtext', group: 'content' },
{ name: 'author', type: 'reference', group: 'meta' },
{ name: 'publishedAt', type: 'datetime', group: 'meta' },
{ name: 'seoTitle', type: 'string', group: 'seo' },
{ name: 'seoDescription', type: 'text', group: 'seo' },
],
}

Show/hide fields based on other values:

{
name: 'type',
type: 'select',
options: {
list: [
{ value: 'internal', title: 'Internal Link' },
{ value: 'external', title: 'External Link' },
],
},
},
{
name: 'internalPage',
type: 'reference',
options: { to: 'page' },
hidden: ({ document }) => document.type !== 'internal',
},
{
name: 'externalUrl',
type: 'string',
hidden: ({ document }) => document.type !== 'external',
}
{
name: 'slug',
type: 'slug',
readOnly: ({ currentUser }) => currentUser.role !== 'admin',
}

Add custom actions to documents:

studio: {
documentActions: [
{
label: 'Publish',
icon: 'publish',
action: async (doc, { client }) => {
await client.documents.update(doc._type, doc._id, {
status: 'published',
publishedAt: new Date().toISOString(),
});
},
visible: (doc) => doc.status === 'draft',
},
{
label: 'Unpublish',
icon: 'unpublish',
action: async (doc, { client }) => {
await client.documents.update(doc._type, doc._id, {
status: 'draft',
});
},
visible: (doc) => doc.status === 'published',
},
],
}

Add actions to the Studio toolbar:

studio: {
globalActions: [
{
label: 'Deploy',
icon: 'rocket',
action: async ({ client }) => {
await fetch('/api/deploy', { method: 'POST' });
},
roles: ['admin'],
},
],
}

Customize the Studio dashboard:

studio: {
dashboard: {
widgets: [
{
type: 'recentDocuments',
title: 'Recent Posts',
schemaType: 'post',
limit: 5,
},
{
type: 'documentCount',
title: 'Content Stats',
schemaTypes: ['post', 'page', 'author'],
},
{
type: 'custom',
title: 'Quick Links',
component: 'QuickLinksWidget',
},
],
},
}

Configure live preview for documents:

studio: {
preview: {
enabled: true,
url: (doc) => {
if (doc._type === 'post') {
return `http://localhost:3001/posts/${doc.slug}`;
}
return null;
},
},
}

Show preview alongside the editor:

studio: {
preview: {
position: 'right', // 'right', 'bottom', or 'popup'
width: '50%',
refreshOnChange: true,
},
}
studio: {
access: {
// Hide schemas from certain roles
schemas: {
siteSettings: ['admin'], // Admin only
post: ['admin', 'editor', 'writer'],
page: ['admin', 'editor'],
},
},
}
studio: {
features: {
mediaLibrary: true,
darkMode: true,
search: true,
bulkActions: ['admin', 'editor'], // Role-restricted
},
}

In development, Studio supports hot reloading:

studio: {
dev: {
hotReload: true,
port: 5173,
},
}
studio: {
debug: {
showFieldNames: true, // Show field names in forms
logApiCalls: true, // Log API requests
},
}
Terminal window
npm run studio:build

This generates optimized static files that are served by the Trokky server.

studio: {
buildDir: './studio-build',
basePath: '/admin', // Serve at /admin instead of /studio
}