Github Url: https://github.com/frappe/studio
Frappe Studio is a visual application builder for the Frappe Framework that enables developers to create applications through a drag-and-drop interface. It combines Vue.js frontend technology with Frappe Framework's backend capabilities to provide a comprehensive low-code development environment.
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Frontend (Vue) │ │ Frappe Backend │ │ Generated Apps │
│ │ │ │ │ │
│ • Studio Builder │◄──►│ • DocTypes │◄──►│ • Published Pages │
│ • Canvas System │ │ • API Endpoints │ │ • App Routes │
│ • Component Library │ │ • Export System │ │ • Runtime Views │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
- Frontend: Vue 3 + TypeScript + Vite
- UI Library: Frappe UI (Vue-based components)
- State Management: Pinia stores
- Backend: Frappe Framework (Python)
- Database: Frappe DocTypes (MariaDB/PostgreSQL)
- Build System: Vite for development, Frappe for production
Purpose: Represents a complete application built in Studio
app_title: Human readable app nameapp_name: Unique app identifier (auto-naming field)route: URL route for the appapp_home: Link to the home pagepublished: Boolean publication statusis_standard: Flag for export to Frappe appsfrappe_app: Target Frappe app for export
Purpose: Individual pages within an app, contains the layout structure
page_name: Unique page identifier (auto-naming field)page_title: Display name for the pagestudio_app: Link to parent Studio Approute: URL route for the pageblocks: JSON field storing the published layout structuredraft_blocks: JSON field storing the draft layout structureresources: Child table of data sources (Studio Page Resource)variables: Child table of page variables (Studio Page Variable)watchers: Child table of reactive scripts (Studio Page Watcher)published: Boolean publication status
Purpose: Reusable custom components
component_name: Display namecomponent_id: Unique identifier (auto-naming field)block: JSON structure of the componentinputs: Child table defining component props (Studio Component Input)
Purpose: Data sources for pages (API endpoints, DocType queries)
resource_name: Unique identifierresource_type: "Document List", "Document", or "API Resource"document_type: Link to Frappe DocTypefields: JSON array of fields to fetchfilters: JSON object with filter conditionsurl: API endpoint URLmethod: HTTP method (GET, POST, PUT, DELETE)transform: JavaScript function for data transformation
Purpose: Page-level reactive variables
variable_name: Variable identifiervariable_type: "String", "Number", "Boolean", or "Object"initial_value: JavaScript code for initial value
Purpose: Reactive scripts that respond to data changes
source: JavaScript expression to watchscript: JavaScript code to execute when source changesimmediate: Boolean flag to run script on page load
The main Vue application is initialized with essential plugins:
import { createApp } from "vue"
import { createPinia } from "pinia"
import { resourcesPlugin } from "frappe-ui"
const studio = createApp(App)
const pinia = createPinia()
studio.use(studio_router) // Vue Router for navigation
studio.use(resourcesPlugin) // Frappe UI resource management
studio.use(pinia) // State managementApp.vue
├── StudioCanvas.vue // Main visual editor canvas
│ ├── StudioComponent.vue // Individual component renderer
│ └── StudioComponentWrapper.vue // Component interaction wrapper
├── StudioLeftPanel.vue // Component palette & pages panel
├── StudioRightPanel.vue // Properties & styles panel
└── StudioToolbar.vue // Top toolbar with actions
Central store managing the overall application state:
- App Management: Current app, app pages, navigation
- Page Management: Active page, blocks, saving/publishing workflow
- Data Management: Resources, variables, reactive data binding
Key methods:
setApp(appName): Load app and its pagessetPage(pageName): Load page and initialize blockssavePage(): Serialize blocks to JSON and save to backendpublishPage(): Convert draft to published blocks
Manages the visual editor canvas state:
- Selection: Currently selected components and multi-selection
- Editing: Component editing modes, drag/drop operations
- Layout: Canvas scaling, panning, responsive breakpoints
Manages the component library:
- Component Library: Available Frappe UI components
- Custom Components: Studio-created reusable components
- Component Loading: Dynamic component registration
The Block class is the core abstraction representing every UI element:
class Block {
componentId: string // Unique identifier
componentName: string // Component type (Button, Input, etc.)
componentProps: object // Component properties
componentSlots: object // Slot content for complex components
componentEvents: object // Event handlers
children: Block[] // Child components
baseStyles: object // CSS styles for desktop
mobileStyles: object // Mobile-specific style overrides
tabletStyles: object // Tablet-specific style overrides
visibilityCondition: string // Conditional visibility logic
}- Hierarchy Management:
addChild(),removeChild(),getParentBlock() - Style Management:
setStyle(),getStyles(), responsive breakpoint handling - Slot Management:
addSlot(),updateSlot()for complex component composition - Event Management:
addEvent(),updateEvent()for component interactivity
User Action → Canvas Interaction → Block Creation → Store Update → UI Refresh
↓ ↓ ↓ ↓ ↓
Drag Component → Drop on Canvas → new Block() → studioStore → Re-render
// When user drags a component from palette:
const newBlock = new Block({
componentName: 'Button',
componentProps: { variant: 'solid', text: 'Click me' },
baseStyles: { padding: '8px 16px', backgroundColor: '#3b82f6' },
componentEvents: { click: 'handleButtonClick()' }
})
// Add to parent container:
parentBlock.addChild(newBlock)// Convert Block tree to JSON for database storage
function jsToJson(blocks: Block[]): string {
const serialized = blocks.map(block => getBlockCopyWithoutParent(block))
return JSON.stringify(serialized, null, 2)
}
// Stored in Studio Page.blocks or Studio Page.draft_blocks field// Convert JSON from database back to Block instances
function jsonToJs(jsonString: string): Block[] {
const parsed = JSON.parse(jsonString)
return parsed.map(blockData => getBlockInstance(blockData))
}When a page is published, the JSON block structure is converted to Vue render functions:
// StudioComponent.vue renders blocks dynamically
const renderBlock = (block: Block) => {
return h(
COMPONENTS[block.componentName].component, // Vue component
{
...block.componentProps, // Component props
style: block.getStyles(breakpoint), // Computed styles
...block.componentEvents // Event handlers
},
renderChildren(block.children) // Recursive children
)
}Resources and variables are made reactive and available to components:
// Resources are fetched and made reactive
const resources = computed(() => {
return studioStore.resources // Auto-updates when data changes
})
// Variables are reactive and can be referenced in expressions
const variables = reactive({
userName: 'John Doe',
isLoggedIn: true,
userProfile: { /* ... */ }
})Studio leverages Frappe UI components as building blocks:
// Component definitions with Studio metadata
export const COMPONENTS: FrappeUIComponents = {
Button: {
component: defineAsyncComponent(() => import('frappe-ui/Button')),
icon: LucideMousePointer2,
initialState: { variant: 'solid', text: 'Button' },
initialSlots: [],
category: 'Input'
},
// ... 50+ more components including Input, Select, Table, etc.
}Users can create reusable components stored as Studio Component DocType:
// Custom component usage
const customComponent = new Block({
componentName: 'UserCard',
isStudioComponent: true,
block: savedComponentStructure, // JSON from Studio Component.block
inputs: componentInputDefinitions // Props interface
})const breakpoints = {
desktop: { width: 1200, device: 'desktop' },
tablet: { width: 768, device: 'tablet' },
mobile: { width: 375, device: 'mobile' }
}
// Style inheritance: mobile inherits tablet inherits desktop
const computedStyles = {
...block.baseStyles, // Desktop (base)
...block.tabletStyles, // Tablet overrides
...block.mobileStyles // Mobile overrides
}Studio provides a unified interface for various data sources:
{
resource_type: "Document List",
document_type: "Customer",
fields: ["name", "customer_name", "email"],
filters: { "status": "Active" },
limit: 20
}{
resource_type: "API Resource",
url: "https://api.example.com/users",
method: "GET",
params: { "page": 1 },
transform: "return data.results.map(user => ({ id: user.id, name: user.full_name }))"
}- String: Text values, form inputs
- Number: Numeric calculations, counters
- Boolean: Toggle states, conditions
- Object: Complex data structures, API responses
// Watcher configuration
{
source: "variables.searchTerm", // What to watch
script: "resources.customers.reload()", // What to execute
immediate: false // Run on page load?
}# studio/studio/doctype/studio_app/studio_app.py
def generate_app_build(self):
"""Generate static files for published app"""
pages = get_published_pages(self.name)
for page in pages:
generate_page_files(page)
generate_component_files(page)apps/[frappe_app]/
├── public/
│ └── studio/
│ └── [app_name]/
│ ├── pages/
│ │ └── [page_name].json
│ └── components/
│ └── [component_name].json
└── templates/
└── generators/
└── [app_name]/
└── [page_name].html
- Blocks as Building Blocks: Every UI element is a Block instance
- Hierarchical Structure: Parent-child relationships for layout
- Slot System: Complex components support named slots for content
- Unidirectional Flow: Data flows from stores to components
- Computed Properties: Derived values update automatically
- Watchers: Side effects triggered by data changes
- JSON Storage: Block trees serialized as JSON in database
- Hydration: JSON converted back to reactive Block instances
- Export: Published pages generate static Vue templates
This architecture enables rapid application development while maintaining the flexibility and power of the underlying Frappe Framework.
Frappe Studio stores CSS styles in the Block's JSON structure using three separate style objects for responsive design:
// Block class properties
class Block {
baseStyles: BlockStyleMap // Desktop/default styles
mobileStyles: BlockStyleMap // Mobile-specific overrides
tabletStyles: BlockStyleMap // Tablet-specific overrides
rawStyles: BlockStyleMap // Raw CSS styles
}{
"componentName": "container",
"componentId": "container-abc123",
"baseStyles": {
"display": "flex",
"flexDirection": "column",
"flexWrap": "wrap",
"flexShrink": 0,
"alignItems": "center",
"justifyContent": "center",
"backgroundColor": "#f4f4f4",
"padding": "20px",
"width": "100%",
"height": "200px"
},
"tabletStyles": {
"flexDirection": "row",
"padding": "15px"
},
"mobileStyles": {
"flexDirection": "column",
"padding": "10px"
},
"children": [...]
}Studio uses camelCase for CSS property names in the JSON structure, converting from kebab-case:
// Utility function in helpers.ts
function kebabToCamelCase(str: string) {
// convert border-color to borderColor
return str.replace(/-([a-z])/g, function (g) {
return g[1].toUpperCase();
});
}
// Usage in Block class
setStyle(style: styleProperty, value: StyleValue) {
style = kebabToCamelCase(style) as styleProperty
// Store as: flexDirection, backgroundColor, etc.
}// Block.getStyles() method
getStyles(breakpoint: string = "desktop"): BlockStyleMap {
let styleObj = { ...this.baseStyles }
if (["mobile", "tablet"].includes(breakpoint)) {
styleObj = { ...styleObj, ...this.tabletStyles }
if (breakpoint === "mobile") {
styleObj = { ...styleObj, ...this.mobileStyles }
}
}
styleObj = { ...styleObj, ...this.rawStyles }
return styleObj
}// StudioComponent.vue
const styles = computed(() => {
const _styles = { ...props.block.getStyles(props.breakpoint) }
// Handle dynamic values (expressions)
Object.entries(_styles).forEach(([key, value]) => {
if (value && isDynamicValue(value.toString())) {
_styles[key] = getDynamicValue(value.toString(), evaluationContext.value)
}
})
return _styles
})<!-- StudioComponent.vue template -->
<component
:is="componentName"
:style="styles" <!-- Computed styles applied here -->
:class="classes"
v-bind="componentProps"
>{
"baseStyles": {
"display": "flex", // or "inline-flex"
"flexDirection": "row", // "column", "row-reverse", "column-reverse"
"flexWrap": "nowrap", // "wrap", "wrap-reverse"
"flexShrink": 0, // numeric values
"flexGrow": 1, // numeric values
"alignItems": "center", // "flex-start", "flex-end", "stretch"
"justifyContent": "center", // "flex-start", "flex-end", "space-between"
"alignContent": "center", // for wrapped flex containers
"gap": "10px", // spacing between flex items
"order": 1 // flex item order
}
}// Block templates with flex defaults
const getBlockTemplate = (element: string) => {
if (element === "body") {
return {
componentName: "div",
originalElement: "body",
baseStyles: {
display: "flex",
flexWrap: "wrap",
flexDirection: "column",
flexShrink: 0,
alignItems: "center"
}
}
}
}While Studio doesn't extensively use CSS custom properties (CSS variables), it does use some for layout:
/* Used in layout components */
:root {
--toolbar-height: 56px; /* Toolbar height */
}
/* Applied in templates */
.canvas-container {
top: var(--toolbar-height);
}Studio supports dynamic expressions in style values:
{
"baseStyles": {
"width": "{{ variables.containerWidth }}px",
"backgroundColor": "{{ variables.theme.primaryColor }}",
"display": "{{ variables.isVisible ? 'flex' : 'none' }}"
}
}These expressions are evaluated at runtime using the getDynamicValue() function with access to:
- Page variables
- Resource data
- Route parameters
- Component context
- Desktop (baseStyles): Default styles applied to all breakpoints
- Tablet (tabletStyles): Inherits desktop + tablet overrides
- Mobile (mobileStyles): Inherits desktop + tablet + mobile overrides
// Example of style resolution for mobile
const mobileStyles = {
...block.baseStyles, // Desktop base
...block.tabletStyles, // Tablet overrides
...block.mobileStyles // Mobile overrides
}This comprehensive styling system allows for responsive design with granular control over CSS properties while maintaining a clean JSON structure for database storage.
Frappe Studio implements a two-tier component system with Standard Components (Frappe UI) and Custom Components (Studio Components), each serving different purposes and implemented differently.
- Source: Pre-built components from the Frappe UI library
- Count: 36+ components (Button, Input, Select, Table, etc.)
- Storage: Defined in code, not in database
- Customization: Props and styling only
- Reusability: Global across all Studio apps
- Source: User-created reusable components
- Storage: Stored in
Studio ComponentDocType - Customization: Full layout, props interface, and logic
- Reusability: Available across pages within Studio
// /frontend/src/data/components.ts
export const COMPONENTS: FrappeUIComponents = {
Button: {
name: "Button",
title: "Button",
icon: LucideMousePointer2,
initialState: { variant: "solid", text: "Button" },
category: "Input"
},
Input: {
name: "Input",
title: "Text Input",
icon: LucideEdit,
initialState: { placeholder: "Enter text..." },
category: "Input"
},
// ... 50+ more components
}Standard components are organized into categories:
- Input: Button, Input, Select, Checkbox, Switch
- Display: Alert, Badge, Avatar, Progress
- Layout: Card, Tabs, Divider
- Data: ListView, Tree
- Charts: AxisChart, DonutChart, NumberChart
// Standard components are loaded from Frappe UI
import { Button, Input, Select } from "frappe-ui"
// Components are registered globally in the component registry
const componentMap = new Map([
["Button", Button],
["Input", Input],
// ...
])-- Studio Component DocType structure
CREATE TABLE `tabStudio Component` (
`name` varchar(140) PRIMARY KEY,
`component_name` varchar(140), -- Display name
`component_id` varchar(140), -- Unique identifier
`block` longtext, -- JSON structure of the component
-- Child table for component inputs/props
)
-- Studio Component Input (child table)
CREATE TABLE `tabStudio Component Input` (
`input_name` varchar(140), -- Prop name
`type` varchar(50), -- Data type
`description` text, -- Documentation
`default` longtext, -- Default value (code)
`required` int(1), -- Required flag
`options` text -- Additional options
)// 1. User creates component from selected blocks
async function createComponent(componentName: string, block?: Block) {
const component = {
component_name: componentName,
block: getBlockObject(block) // Serialize block tree to JSON
}
return studioComponents.insert.submit(component)
}
// 2. Component is stored in database
// 3. Component becomes available in component palette// Example Studio Component JSON in database
{
"component_name": "UserCard",
"component_id": "user-card-abc123",
"block": {
"componentName": "div",
"baseStyles": {
"display": "flex",
"flexDirection": "column",
"padding": "16px",
"borderRadius": "8px",
"backgroundColor": "#f9fafb"
},
"children": [
{
"componentName": "Avatar",
"componentProps": {
"src": "{{ inputs.avatarUrl }}",
"size": "lg"
}
},
{
"componentName": "TextBlock",
"componentProps": {
"text": "{{ inputs.userName }}",
"fontSize": "text-lg",
"fontWeight": "font-semibold"
}
}
]
},
"inputs": [
{
"input_name": "userName",
"type": "String",
"description": "User's display name",
"default": "John Doe",
"required": true
},
{
"input_name": "avatarUrl",
"type": "String",
"description": "URL to user's avatar image",
"default": "",
"required": false
}
]
}// /stores/componentStore.ts
export const useComponentStore = defineStore("componentStore", () => {
const componentMap = reactive<Map<string, Block>>(new Map())
const componentDocMap = reactive<Map<string, StudioComponent>>(new Map())
async function loadComponent(componentName: string) {
if (!componentMap.has(componentName)) {
// Fetch from database
const componentDoc = await fetchComponent(componentName)
cacheComponent(componentDoc)
}
}
function cacheComponent(componentDoc: StudioComponent) {
componentDocMap.set(componentDoc.component_id, componentDoc)
if (componentDoc.block) {
// Convert JSON to Block instance
componentMap.set(componentDoc.component_id,
markRaw(getBlockInstance(componentDoc.block)))
}
}
})- Standard Components: Loaded synchronously from imports
- Custom Components: Loaded asynchronously from database when first used
- Caching: Components cached in memory after first load
- Missing Components: Fallback to "missing component" template
// Block creation for standard component
const buttonBlock = new Block({
componentName: "Button", // Maps to Frappe UI Button
isStudioComponent: false, // Standard component flag
componentProps: {
variant: "solid",
text: "Click me",
onClick: "handleClick()"
}
})// Block creation for custom component
const userCardBlock = new Block({
componentName: "user-card-abc123", // Maps to Studio Component ID
isStudioComponent: true, // Custom component flag
componentProps: {
userName: "{{ variables.currentUser.name }}",
avatarUrl: "{{ variables.currentUser.avatar }}"
}
})Standard Component Rendering:
<!-- Direct Vue component rendering -->
<component
:is="Button" <!-- Frappe UI component -->
:variant="props.variant"
:text="props.text"
@click="handleClick"
/>Custom Component Rendering:
<!-- Wrapper component for custom components -->
<StudioComponentWrapper
v-if="block.isStudioComponent"
:studioComponent="block"
:evaluationContext="context"
>
<!-- Renders the stored block structure -->
<StudioComponent :block="componentBlock" />
</StudioComponentWrapper>Custom components support typed inputs/props:
// Component input definition
interface ComponentInput {
input_name: string // Property name (e.g., "userName")
type: string // Data type ("String", "Number", "Boolean", "Object")
description?: string // Documentation
default?: any // Default value
required: boolean // Required flag
options?: string // Additional configuration
}// StudioComponentWrapper.vue
const componentContext = computed(() => {
const context = { ...studioComponent.componentProps }
const componentDoc = componentStore.getComponentDoc(componentName)
// Apply default values from input definitions
componentDoc.inputs?.forEach((input) => {
if (!(input.input_name in context) && input.default !== undefined) {
context[input.input_name] = input.default
}
})
return { inputs: context }
})
// Provide context to child components
provide("componentContext", componentContext)- Canvas Mode: Edit component layout visually
- Interface Tab: Define component inputs/props
- Preview Mode: Test component with sample data
// Component editor for defining inputs
const componentInputs = ref<ComponentInput[]>([
{
input_name: "title",
type: "String",
description: "Card title",
default: "Default Title",
required: true
}
])| Aspect | Standard Components | Custom Components |
|---|---|---|
| Source | Frappe UI library | User-created in Studio |
| Storage | Code-based registry | Database (Studio Component DocType) |
| Props | Fixed component API | User-defined input interface |
| Layout | Single Vue component | Nested block structure |
| Reusability | Global (all apps) | Studio-wide |
| Customization | Props + styling only | Full layout + logic |
| Performance | Direct component rendering | Wrapper + block rendering |
| Loading | Synchronous import | Async database fetch |
| Icon | Blue border in editor | Purple border in editor |
Components are distinguished at runtime:
// Block property determines component type
if (block.isStudioComponent) {
// Custom component - load from database
return <StudioComponentWrapper studioComponent={block} />
} else {
// Standard component - use directly
return <component is={block.componentName} {...props} />
}This dual-component architecture provides both the reliability of pre-built UI components and the flexibility of user-created custom components, enabling rapid application development while maintaining extensibility.
Frappe Studio provides 52 standard components organized into two main categories: Frappe UI Components (36 components) and Studio Components (16 components). Each component comes with predefined initial states and specific capabilities.
These are direct integrations of the Frappe UI library components with Studio-specific configurations.
1. Button
- Purpose: Interactive buttons for actions
- Props:
label(string): Button text - "Submit"variant(string): Button style - "solid", "outline", "ghost", "subtle"
- Icon: Rectangle Horizontal
- Use Cases: Form submissions, actions, navigation
2. FormControl
- Purpose: Enhanced form input with label and validation
- Props:
type(string): Input type - "text", "email", "password", "number"label(string): Field label - "Name"placeholder(string): Placeholder text - "John Doe"autocomplete(string): Browser autocomplete - "off", "on"modelValue(any): Bound value (optional)
- Icon: Book Type
- Use Cases: Text input with built-in labeling
3. TextInput
- Purpose: Basic text input field
- Props:
placeholder(string): Placeholder text - "Enter your name"modelValue(string): Input value
- Icon: A Large Small
- Use Cases: Simple text entry
4. Textarea
- Purpose: Multi-line text input
- Props:
placeholder(string): Placeholder text - "Enter your message"modelValue(string): Textarea contentrows(number): Number of visible rows
- Icon: Letter Text
- Use Cases: Comments, descriptions, long text
5. Select
- Purpose: Dropdown selection with options
- Props:
placeholder(string): Placeholder text - "Person"options(array): Selection options withlabel,value,disabledpropertiesmodelValue(any): Selected value
- Example Options:
[{ label: "John Doe", value: "john-doe" }, { label: "Jane Smith", value: "jane-smith", disabled: true }] - Icon: Mouse Pointer 2
- Use Cases: Single selection from predefined options
6. Autocomplete
- Purpose: Search-enabled selection with images
- Props:
placeholder(string): Placeholder text - "Select Person"options(array): Options withlabel,value,imagepropertiesmodelValue(any): Selected value
- Example Options:
[{ label: "John Doe", value: "john-doe", image: "https://randomuser.me/api/portraits/men/59.jpg" }] - Icon: Text Search
- Use Cases: User selection, searchable dropdowns
7. Checkbox
- Purpose: Boolean selection with label
- Props:
label(string): Checkbox label - "Enable feature"padding(boolean): Add padding - true/falsechecked(boolean): Checked state - true/falsemodelValue(boolean): Bound value
- Icon: Circle Check
- Use Cases: Feature toggles, agreements
8. Switch
- Purpose: Toggle switch with description
- Props:
label(string): Switch label - "Enable Notifications"description(string): Help text - "Get notified when someone mentions you"modelValue(boolean): Switch state - true/false
- Icon: Toggle Left
- Use Cases: Settings, preferences
9. FileUploader
- Purpose: File upload interface
- Props:
label(string): Upload button text - "Upload File"fileTypes(array): Accepted file types -['image/*'],['.pdf', '.doc']multiple(boolean): Allow multiple files
- Icon: File Up
- Use Cases: Document uploads, image selection
10. DatePicker
- Purpose: Date selection widget
- Props:
placeholder(string): Placeholder text - "Select Date"modelValue(string/Date): Selected dateformat(string): Date display format
- Icon: Calendar Check
- Use Cases: Event dates, deadlines
11. DateTimePicker
- Purpose: Date and time selection
- Props:
placeholder(string): Placeholder text - "Select Date Time"modelValue(string/Date): Selected datetimeformat(string): DateTime display format
- Icon: Calendar Clock
- Use Cases: Appointments, precise timing
12. DateRangePicker
- Purpose: Date range selection
- Props:
placeholder(string): Placeholder text - "Select Date Range"modelValue(array): Date range[startDate, endDate]format(string): Date display format
- Icon: Calendar Search
- Use Cases: Reporting periods, bookings
13. Alert
- Purpose: Status messages and notifications
- Props:
title(string): Alert message - "This user is inactive"type(string): Alert type - "warning", "error", "success", "info"description(string): Additional details (optional)
- Icon: Circle Alert
- Use Cases: Error messages, warnings, success notifications
14. Badge
- Purpose: Status indicators and labels
- Props:
variant(string): Badge style - "subtle", "solid", "outline"theme(string): Color theme - "green", "red", "blue", "yellow", "gray"size(string): Badge size - "sm", "md", "lg"label(string): Badge text - "Active"
- Icon: Badge Check
- Use Cases: Status tags, categories, counts
15. Avatar
- Purpose: User profile images
- Props:
shape(string): Avatar shape - "circle", "square"size(string): Avatar size - "xs", "sm", "md", "lg", "xl"image(string): Image URL - "https://avatars.githubusercontent.com/u/499550"label(string): Fallback text - "EY"
- Icon: User
- Use Cases: User representation, profile displays
16. Progress
- Purpose: Progress indicators
- Props:
value(number): Progress percentage - 50 (0-100)size(string): Progress bar size - "xs", "sm", "md", "lg"label(string): Progress label - "Progress"color(string): Progress color theme
- Icon: Ellipsis
- Use Cases: Loading states, completion tracking
17. ErrorMessage
- Purpose: Error display component
- Props:
message(string): Error text - "Transaction failed due to insufficient balance"title(string): Error title (optional)
- Icon: Circle X
- Use Cases: Form validation, error handling
18. Tooltip
- Purpose: Contextual help text
- Props:
text(string): Tooltip content - "This is a tooltip"placement(string): Tooltip position - "top", "bottom", "left", "right"hoverDelay(number): Show delay in ms
- Icon: Message Square
- Use Cases: Help text, additional information
19. FeatherIcon
- Purpose: Icon display component
- Props:
name(string): Icon name - "activity", "home", "settings", "user"class(string): CSS classes - "h-6 w-6"size(string): Icon size preset
- Icon: Feather
- Use Cases: UI icons, visual indicators
20. FormLabel
- Purpose: Standalone form labels
- Props:
label(string): Label text - "Form Label"required(boolean): Show required indicatordescription(string): Help text (optional)
- Icon: Tag
- Use Cases: Custom form layouts
21. Card
- Purpose: Content containers with title/subtitle
- Props:
title(string): Card title - "John Doe"subtitle(string): Card subtitle - "Engineering Lead"image(string): Header image URL (optional)padding(boolean): Add internal padding
- Icon: ID Card
- Use Cases: Profile cards, content sections
22. Tabs
- Purpose: Tabbed content organization
- Props:
as(string): HTML element - "div", "section"tabs(array): Tab configuration withlabelandcontentmodelValue(string): Active tab identifier
- Example Tabs:
[{ label: "Github", content: "Github is a code hosting platform..." }, { label: "Twitter", content: "Twitter is an American microblogging..." }] - Icon: Arrow Right Left
- Use Cases: Content organization, multi-view interfaces
23. TabButtons
- Purpose: Tab-style button navigation
- Props:
buttons(array): Button configuration withlabelandvaluemodelValue(string): Selected button value
- Example Buttons:
[{ label: "My Tasks", value: "mytasks" }, { label: "Team Tasks", value: "teamtasks" }] - Icon: Arrow Right Left
- Use Cases: View switching, filters
24. Breadcrumbs
- Purpose: Navigation path display
- Props:
items(array): Navigation items withlabelandrouteseparator(string): Custom separator character
- Example Items:
[{ label: "Home", route: { name: "Home" } }, { label: "List", route: "/components/breadcrumbs" }] - Icon: Chevrons Right
- Use Cases: Navigation hierarchy, current location
25. Divider
- Purpose: Visual content separation
- Props:
orientation(string): "horizontal", "vertical"thickness(string): Line thicknesscolor(string): Divider color
- Icon: Minus
- Use Cases: Section breaks, visual organization
26. Dialog
- Purpose: Modal dialogs and popups
- Props:
modelValue(boolean): Dialog visibility - falsedisableOutsideClickToClose(boolean): Prevent outside click close - trueoptions(object): Dialog configuration withtitle,message,size,actions
- Example Options:
{ title: "Confirm", message: "Are you sure?", size: "xl", actions: [{ label: "Confirm", variant: "solid", onClick: () => {} }] } - Special Features:
editInFragmentMode: true, usesProxyDialogcomponent - Icon: App Window Mac
- Use Cases: Confirmations, forms, detailed views
27. Dropdown
- Purpose: Action menus and options
- Props:
options(array): Menu items withlabel,onClick,iconbutton(object): Trigger button configurationplacement(string): Menu position
- Example Options:
[{ label: "Edit Title", onClick: () => {}, icon: () => h(FeatherIcon, { name: "edit-2" }) }] - Icon: Chevron Down
- Use Cases: Context menus, action lists
28. ListView
- Purpose: Tabular data display
- Props:
columns(array): Column definitions withlabel,key,width,getLabel,prefixrows(array): Data rows as objectsrowKey(string): Unique row identifier - "id"selectable(boolean): Enable row selection
- Example Columns:
[{ label: "Name", key: "name", width: 3, getLabel: ({ row }) => row.name, prefix: ({ row }) => h(Avatar, { image: row.user_image }) }] - Example Rows:
[{ id: 1, name: "John Doe", email: "john@doe.com", status: "Active", role: "Developer" }] - Icon: List Check
- Use Cases: Data tables, user lists
29. Tree
- Purpose: Hierarchical data display
- Props:
options(object): Tree configuration withshowIndentationGuides,rowHeight,indentWidthnodeKey(string): Node identifier property - "name"node(object): Root node withname,label,children
- Example Options:
{ showIndentationGuides: true, rowHeight: "25px", indentWidth: "15px" } - Example Node:
{ name: "guest", label: "Guest", children: [{ name: "downloads", label: "Downloads", children: [...] }] } - Icon: List Tree
- Use Cases: File browsers, organizational structures
30. Calendar
- Purpose: Event calendar display
- Props:
config(object): Calendar settings withdefaultMode,isEditMode,eventIcons,allowCustomClickEventsevents(array): Event objects withtitle,participant,id,venue,fromDate,toDate,color
- Example Config:
{ defaultMode: "Month", isEditMode: true, allowCustomClickEvents: true, redundantCellHeight: 100 } - Example Events:
[{ title: "English by Ryan Mathew", participant: "Ryan Mathew", fromDate: "2024-07-08 16:30:00", toDate: "2024-07-08 17:30:00", color: "green" }] - Icon: Calendar
- Use Cases: Scheduling, event management
31. AxisChart
- Purpose: Bar/line charts with axes
- Props:
config(object): Chart configuration withdata,title,subtitle,xAxis,yAxis,seriesdata(array): Chart data pointsxAxis(object): X-axis config withkey,type,title,timeGrainyAxis(object): Y-axis config withtitle,echartOptionsseries(array): Data series withname,type
- Example Config:
{ title: "Monthly Sales", xAxis: { key: "month", type: "time", timeGrain: "month" }, yAxis: { title: "Amount ($)" }, series: [{ name: "sales", type: "bar" }] } - Example Data:
[{ month: "2021-01-01", sales: 200 }, { month: "2021-02-01", sales: 300 }] - Icon: Chart Line
- Use Cases: Analytics, trends, reporting
32. DonutChart
- Purpose: Circular chart for proportional data
- Props:
config(object): Chart configuration withdata,title,subtitle,categoryColumn,valueColumndata(array): Chart data with category and value fields
- Example Config:
{ title: "Product Sales Distribution", subtitle: "Sales distribution across products", categoryColumn: "product", valueColumn: "sales" } - Example Data:
[{ product: "Apple Watch", sales: 400 }, { product: "Services", sales: 400 }, { product: "iPhone", sales: 200 }] - Icon: Chart Pie
- Use Cases: Market share, distribution analysis
33. NumberChart
- Purpose: Single metric display with delta
- Props:
config(object): Metric configuration withtitle,value,prefix,delta,deltaSuffix,negativeIsBetter
- Example Config:
{ title: "Total Sales", value: 123456, prefix: "$", delta: 10, deltaSuffix: "% MoM", negativeIsBetter: false } - Icon: Dollar Sign
- Use Cases: KPIs, dashboards, metrics
34. TextEditor
- Purpose: Rich text editing
- Props:
content(string): Editor content - "Type something..."editorClass(string): CSS classes for editoreditable(boolean): Enable editing - truefixedMenu(boolean): Show fixed toolbar - truebubbleMenu(boolean): Show bubble menu - true
- Special Features:
useOverridenPropTypes: true - Icon: Edit
- Use Cases: Content creation, documentation
35. TextBlock
- Purpose: Styled text display
- Props:
fontSize(string): Text size - "text-xs", "text-sm", "text-base", "text-lg", "text-xl"fontWeight(string): Text weight - "font-normal", "font-medium", "font-semibold", "font-bold"text(string): Display text contentcolor(string): Text color
- Icon: Type
- Use Cases: Headings, paragraphs, styled text
These are Studio-specific components built for layout and specialized functionality.
1. Container
- Purpose: Basic layout container
- Props:
padding(string): Internal paddingmargin(string): External marginbackgroundColor(string): Background color
- Use Cases: Content grouping, layout structure
2. FitContainer
- Purpose: Responsive container that fits content
- Props:
minHeight(string): Minimum height constraintmaxWidth(string): Maximum width constraint
- Use Cases: Adaptive layouts, responsive design
3. SplitView
- Purpose: Two-panel layout
- Props:
orientation(string): "horizontal", "vertical"splitRatio(number): Panel size ratio (0-1)resizable(boolean): Allow resizing
- Initial Slots:
["left", "right"] - Icon: Square Split Horizontal
- Use Cases: Master-detail views, comparisons
4. Repeater
- Purpose: Dynamic content repetition
- Props:
data(array): Data source for repetitionitemKey(string): Unique key propertytemplate(object): Item template configuration
- Icon: Repeat
- Use Cases: Lists, grids, dynamic content
5. Header
- Purpose: Page header with navigation
- Props:
title(string): Header title - "Frappe"menuItems(array): Navigation items withlabelandurllogo(string): Logo image URL
- Example MenuItems:
[{ label: "Home", url: "#" }, { label: "Settings", url: "#" }] - Icon: Frame
- Use Cases: Site headers, navigation bars
6. AppHeader
- Purpose: Application-specific header
- Props:
title(string): App title - "Frappe"subtitle(string): App subtitleactions(array): Header action buttons
- Icon: Frame
- Use Cases: App-specific branding
7. Sidebar
- Purpose: Side navigation panel
- Props:
title(string): Sidebar title - "Frappe"menuItems(array): Menu items withlabel,featherIcon,route_tocollapsed(boolean): Collapsed state
- Example MenuItems:
[{ label: "Home", featherIcon: "home", route_to: "/" }, { label: "Settings", featherIcon: "settings", route_to: "/" }] - Icon: Sidebar
- Use Cases: App navigation, menu systems
8. BottomTabs
- Purpose: Bottom navigation tabs
- Props:
tabs(array): Tab configuration withlabel,icon,routeactiveTab(string): Currently active tab
- Example Tabs:
[{ label: "Home", icon: "home", route: "/" }, { label: "Settings", icon: "settings", route: "/settings" }] - Icon: Arrow Right Left
- Use Cases: Mobile navigation, tab bars
9. AvatarCard
- Purpose: Card with avatar/image
- Props:
title(string): Card title - "Up&Up"subtitle(string): Card subtitle - "Coldplay"imageURL(string): Image source URLsize(string): Card size - "sm", "md", "lg"
- Icon: Image
- Use Cases: Music cards, profile displays
10. CardList
- Purpose: List of cards
- Props:
title(string): List title - "Card List"cards(array): Card data withtitle,subtitle,imageURLlayout(string): Grid layout - "grid", "list"
- Example Cards:
[{ title: "Card Title", subtitle: "Subtitle", imageURL: "https://avatars.githubusercontent.com/u/499550" }] - Icon: List
- Use Cases: Product lists, content galleries
11. Audio
- Purpose: Audio player component
- Props:
file(string): Audio file URLautoplay(boolean): Auto-start playbackcontrols(boolean): Show player controlsloop(boolean): Loop playback
- Icon: Music
- Use Cases: Music players, audio content
12. ImageView
- Purpose: Image display component
- Props:
image(string): Image source URLsize(string): Image size - "xs", "sm", "md", "lg", "xl"alt(string): Alt text for accessibilityfit(string): Image fit - "cover", "contain", "fill"
- Icon: Image
- Use Cases: Image galleries, media display
13. TextBlock
- Purpose: Styled text component
- Props:
text(string): Text contentfontSize(string): Text size classesfontWeight(string): Font weight classestextAlign(string): Text alignment
- Icon: Type
- Use Cases: Headings, content blocks
14. MarkdownEditor
- Purpose: Markdown editing interface
- Props:
modelValue(string): Markdown content - "# This is a markdown editor"preview(boolean): Show preview modetoolbar(boolean): Show editing toolbarheight(string): Editor height
- Icon: File Pen Line
- Use Cases: Documentation, content creation
Complete Component Lists:
- FRAPPE_UI_COMPONENTS (36): Alert, Autocomplete, Avatar, Badge, Button, Breadcrumbs, Card, Checkbox, Calendar, DatePicker, DateTimePicker, DateRangePicker, Dialog, Divider, Dropdown, ErrorMessage, FeatherIcon, FileUploader, FormLabel, FormControl, ListView, Progress, Select, Switch, Tabs, TabButtons, Textarea, TextInput, TextEditor, Tooltip, Tree, AxisChart, NumberChart, DonutChart, TextBlock
- STUDIO_COMPONENTS (16): Container, FitContainer, Repeater, Header, Sidebar, SplitView, AvatarCard, CardList, Audio, ImageView, TextBlock, AppHeader, BottomTabs, MarkdownEditor
Some components use proxy components for enhanced editing:
- Dialog: Uses
ProxyDialog.vuefor better modal editing experience
- Dialog:
editInFragmentMode: true- Opens in a dedicated editing mode
- SplitView: Predefined slots for
["left", "right"]content areas
- TextEditor:
useOverridenPropTypes: truefor custom property handling - FormControl: Additional props with
modelValueandplaceholderoptions
// Component registry structure
export const COMPONENTS: FrappeUIComponents = {
// 36 Frappe UI Components
Alert: { name: "Alert", title: "Alert", icon: LucideCircleAlert, ... },
Button: { name: "Button", title: "Button", icon: LucideRectangleHorizontal, ... },
// ... more components
// 16 Studio Components
Repeater: { name: "Repeater", title: "Repeater", icon: LucideRepeat },
Header: { name: "Header", title: "Header", icon: LucideFrame, ... },
// ... more components
}
// Utility functions
export default {
...COMPONENTS,
list: Object.values(COMPONENTS), // Array of all components
names: Object.keys(COMPONENTS), // Array of component names
getProxyComponent, // Get proxy component if exists
isFrappeUIComponent, // Check if component is from Frappe UI
get, // Get component by name
}This comprehensive component library provides developers with a rich set of building blocks covering all aspects of modern web application development, from basic forms and inputs to complex data visualization and navigation patterns.
When a user clicks the Publish button in Studio, a multi-step process is triggered that transforms draft blocks into live, publicly accessible pages. The workflow involves validation, app build generation, block promotion, and optional file export.
The publish button in the toolbar triggers the publishing workflow. When clicked:
- Sets a
publishingloading state to provide user feedback - Calls
store.publishPage()from the studio store - Ensures the loading state is cleared regardless of success or failure
This is the main orchestration method for publishing a page. It executes a three-step process:
Step 1: Generate App Build (Optional)
- Calls
generateAppBuild()to create optimized production assets - Wrapped in try-catch to ensure publishing continues even if build fails
- Build failures are logged but don't block page publication
Step 2: Call Backend Publish Method
- Submits a DocMethod call to
studio_page.publish() - Passes the current page name
- Triggers server-side validation and block promotion
Step 3: Refresh and Open Page
- Fetches the updated page data from backend
- Automatically opens the published page in a new browser window/tab
- Uses
openPageInBrowser()to construct the correct URL
Triggers the Vite bundling process for the current app:
Method Behavior:
- Calls the backend
studio_app.generate_app_build()method - Shows success toast: "App build generated"
- On error, shows warning toast with error details (non-blocking)
- Warning toast persists indefinitely to alert developers of build issues
- Returns a promise that resolves regardless of build outcome
Method: publish(self, **kwargs) - whitelisted for frontend calls
This method executes the core publishing logic on the server side:
Key Actions:
- Validation: Calls
validate_conflicts_with_other_pages()to ensure no duplicate routes or page titles exist in published pages within the same app - Block Promotion: Copies content from
draft_blocksfield toblocksfield, then clearsdraft_blocks - Published Flag: Sets
published = 1to make the page publicly accessible - Save Trigger: Calls
save()which triggerson_update()hook, initiating export workflow ifis_standard = 1
The method accepts kwargs from the frontend but primarily operates on the document's own state. It ensures atomic publishing - either all steps succeed or the page remains unpublished.
Method: validate_conflicts_with_other_pages(self)
Purpose: Prevent routing conflicts and naming collisions within an app
Query Logic:
- Searches for other published pages in the same Studio App
- Excludes the current page from search (by name)
- Uses OR logic to check for duplicate
routeOR duplicatepage_title - Returns matching pages with their route and title information
Error Handling:
- If conflicts found, throws a user-friendly error message listing all conflicting pages
- Error format: "Page(s) with duplicate Route or Page Title already exist in this app: [Page Title - /route]"
- Prevents publish operation from completing, maintaining route uniqueness
Method: generate_app_build(self) - whitelisted for frontend calls
Permission Check:
- Requires "write" permission on Studio App DocType
- Throws
PermissionErrorif user lacks required permissions
Smart Build Detection:
- Calls
get_app_components(app_name, "draft_blocks")to analyze components in draft pages - Calls
get_app_components(app_name)to analyze components in published pages - Uses set symmetric difference to detect if component usage has changed
- Optimization: Skips build entirely if no component changes detected (avoids unnecessary rebuilds)
Build Execution:
- Constructs yarn command:
yarn build-studio-app [app_name] --components [comma-separated-list] - Gets Studio app source path using
frappe.get_app_source_path("studio") - Executes command via
popen()withraise_err=Truefor error propagation - Command runs in Studio app directory with proper working directory context
Build Output:
- Generates optimized JavaScript bundle with tree-shaking
- Creates minified CSS with only used styles
- Output location:
/studio/public/app_builds/[app_name]/assets/ - Generates Vite manifest for asset path resolution
Location: /studio/public/app_builds/[app_name]/.vite/manifest.json
Manifest Purpose: The manifest maps logical entry points to content-hashed physical files, enabling:
- Cache busting through hash-based filenames
- Reliable asset path resolution
- Dynamic stylesheet injection
- Production asset loading
Manifest Entry Structure:
- Key: Original entry point name (e.g.,
renderer-[app_name].js) - file: Hashed output filename (e.g.,
assets/renderer-a3f2b1c9.js) - css: Array of associated CSS files with hashes
Backend Integration:
Method get_assets_from_manifest() (lines 155-196 in studio_app.py) reads this manifest to:
- Find the entry point for the specific app
- Extract script and stylesheet paths
- Pass them to the HTML template for injection
- Handle missing manifest gracefully with fallback to development renderer
When is_standard = 1 and developer_mode = 1, published pages are automatically exported to the file system as JSON for version control and deployment.
Function: can_export(doc) -> bool
Purpose: Gate-keeping function that determines if a document should be exported to the file system
Required Conditions (ALL must be true):
doc.is_standard = 1- Document marked for exportfrappe.conf.developer_mode = 1- Site in development modenot frappe.flags.in_install- Not during app installationnot frappe.flags.in_uninstall- Not during app uninstallationnot frappe.flags.in_import- Not during data import operations
Rationale: Prevents circular exports during migrations, installations, and imports while ensuring only designated standard documents are exported in developer environments.
Trigger Point - on_update(self) hook:
- Called automatically after any document save operation
- Initiates export process if conditions are met
- Ensures file system stays in sync with database
Main Export Method - export_page(self):
- Checks
can_export()before proceeding - Calls
write_document_file()to serialize page JSON to disk - Calls
delete_old_page_file()to clean up renamed pages - Calls
export_components()to export all used Studio Components
Component Export - export_components(self):
- Calls
get_studio_components()to scan page blocks for custom components - Creates component folder with
__init__.pyfor Python module structure - Iterates through each component and writes JSON file
- Ensures components are available for import on other sites
File Operations:
write_document_file()- Serializes document to JSON with proper formattingget_folder_path()- Constructs path:[frappe_app]/studio/[app_name]/studio_page/get_component_folder_path()- Returns:[frappe_app]/studio/[app_name]/studio_components/
[frappe_app]/
├── studio/
│ └── [app_name]/
│ ├── studio_app.json # App configuration
│ ├── studio_page/
│ │ ├── __init__.py
│ │ ├── home_page.json # Page 1 (JSON)
│ │ └── about_page.json # Page 2 (JSON)
│ └── studio_components/
│ ├── __init__.py
│ ├── user_card.json # Custom component 1
│ └── product_card.json # Custom component 2
└── public/
└── app_builds/
└── [app_name]/
├── .vite/
│ └── manifest.json # Vite build manifest
└── assets/
├── renderer-[hash].js # Bundled app code
└── renderer-[hash].css # Bundled styles
Published pages are served through two different rendering paths depending on whether the user is previewing drafts or accessing the published app.
URL Pattern: /dev/[app_route]/[page_route]
Method: update_context(self) - with is_preview() returning True
Context Setup:
- Sets
is_preview = Truein template context - Prepends
/dev/to app route for preview URLs - Uses
studio_renderer.htmltemplate (development renderer) - Loads all pages from Studio App (published + unpublished) for testing access
- Query:
frappe.get_all("Studio Page", {"studio_app": app_name}, ["name", "page_title", "route"])
Rendering Characteristics:
- Uses development assets from Vite dev server (port 8080)
- Enables Hot Module Replacement (HMR) for instant updates during development
- Shows all pages regardless of publication status for testing
- Includes Vite client scripts for live reloading
- No build required - direct TypeScript/Vue compilation
Use Cases: Testing draft changes, developing pages, iterating on layout without publishing
URL Pattern: /[app_route]/[page_route]
Method: update_context(self) - with is_preview() returning False
Context Setup:
- Uses
app_renderer.htmltemplate (production renderer) - Calls
get_assets_from_manifest()to read Vite manifest - Extracts stylesheet and script paths from manifest
- Falls back to
studio_renderer.htmlif manifest not found (withassets_not_found = True)
Manifest Reading Logic:
- Opens
/studio/public/app_builds/[app_name]/.vite/manifest.json - Searches for entry key matching pattern
renderer-[app_name].js - Constructs full asset URLs with base path
/assets/studio/app_builds/[app_name]/ - Handles missing manifest gracefully with fallback renderer
Rendering Characteristics:
- Uses production-built assets from Vite build output
- Shows only published pages (filtered by
published = 1) - Serves optimized, minified, and tree-shaken JavaScript/CSS
- Assets have content-hashed filenames for optimal caching
- No HMR - static production bundle
Use Cases: Live applications, end-user access, production deployments
Production Template (app_renderer.html):
Location: studio/templates/generators/app_renderer.html
Structure:
- Standard HTML5 document with full-height layout classes
- Title set from
app_titlecontext variable - Dynamic asset injection via Jinja2 loops:
- Iterates through
stylesheetsarray for CSS links - Conditionally includes
scriptif present from manifest
- Iterates through
- All assets loaded with
crossoriginattribute for CORS
Mount Points:
#app- Main Vue application mount point#modals- Portal target for modal dialogs#popovers- Portal target for popover components
Window Context Variables:
csrf_token- CSRF token for API callsapp_name- Studio App identifierapp_route- App base routeapp_title- App display titleapp_pages- JSON array of published pages only
Preview Template (studio_renderer.html):
Location: studio/templates/generators/studio_renderer.html
Structure:
- Similar HTML5 structure to production template
- Static asset references pointing to Studio frontend build:
- Hardcoded script:
/assets/studio/frontend/assets/renderer-[hash].js - Hardcoded stylesheet:
/assets/studio/frontend/assets/block-[hash].css
- Hardcoded script:
- Assets are pre-built Studio renderer, not app-specific
Additional Window Variables:
is_preview- Flag indicating preview modeapp_pages- JSON array of all pages (published + unpublished)
Developer Mode Enhancement:
- Conditionally includes HMR scripts when
is_developer_mode = True - Loads Vite client from dev server:
http://[site_name]:8080/@vite/client - Loads renderer source:
http://[site_name]:8080/src/renderer.ts - Enables live reloading during development
Once the HTML is loaded, the Vue renderer takes over:
- App Bootstrap: The renderer script initializes a Vue application
- Page Data Fetch: Uses
window.app_pagesto get available pages - Route Matching: Vue Router matches URL to page based on
routefield - Block Hydration: Fetches page's
blocksJSON from backend - Component Rendering: Recursively renders blocks as Vue components
- Resource Loading: Fetches page resources (Document List, API, etc.)
- Variable Initialization: Sets up reactive variables with initial values
- Watcher Activation: Registers reactive watchers for side effects
draft_blocks(JSON): Work-in-progress layout (editable in Studio)blocks(JSON): Published layout (served to end users)published(Check): Boolean flag enabling public accessroute(Data): URL path for the page (e.g.,/home,/products/:id)is_standard(Check): Enables file export to Frappe appfrappe_app(Link): Target Frappe app for exports
app_name(Data): Unique app identifierapp_title(Data): Human-readable app nameroute(Data): Base URL route (e.g.,myapp)app_home(Link): Default landing pagepublished(Check): App publication statusis_standard(Check): Enables export to Frappe appfrappe_app(Link): Target Frappe app for exports
┌─────────────────────┐
│ Draft State │
│ │
│ • draft_blocks: {} │
│ • blocks: null │
│ • published: 0 │
└──────────┬──────────┘
│
│ Click "Publish"
│
▼
┌─────────────────────┐
│ Validation Phase │
│ │
│ • Check route dups │
│ • Check title dups │
└──────────┬──────────┘
│
│ Validation passes
│
▼
┌─────────────────────┐
│ Build Phase │
│ (if required) │
│ │
│ • Scan components │
│ • Run Vite build │
│ • Generate manifest │
└──────────┬──────────┘
│
│ Build complete (or skipped)
│
▼
┌─────────────────────┐
│ Published State │
│ │
│ • draft_blocks: {} │ ─┐
│ • blocks: {} │ │ Same content
│ • published: 1 │ ─┘
└──────────┬──────────┘
│
│ User makes edits
│
▼
┌─────────────────────┐
│ Draft-Published │
│ State │
│ │
│ • draft_blocks: {} │ ← New changes
│ • blocks: {} │ ← Old published version
│ • published: 1 │
└─────────────────────┘
- Builds are only generated when component usage changes
- Skips build if
draft_blockscomponents ==blockscomponents
- Production builds use content-hashed filenames (e.g.,
renderer-a3f2b1c9.js) - Browser can cache assets indefinitely
- New builds generate new hashes, auto-invalidating cache
- Only components actually used in published pages are bundled
- Unused Frappe UI components are excluded from build
- Reduces bundle size significantly
- If production build fails or is missing, system falls back to development renderer
- Ensures pages remain accessible even with build issues
- Error message logged to browser console
User Action: Click "Publish" Button
│
▼
Frontend: studioStore.publishPage()
│
├─► generateAppBuild() (optional)
│ │
│ └─► Backend: studio_app.generate_app_build()
│ │
│ ├─► Analyze component usage
│ ├─► Run Vite: yarn build-studio-app
│ └─► Generate manifest.json
│
└─► Backend: studio_page.publish()
│
├─► validate_conflicts_with_other_pages()
├─► Set published = 1
├─► Copy draft_blocks → blocks
├─► Clear draft_blocks
└─► save() triggers on_update()
│
└─► export_page() (if is_standard = 1)
│
├─► Write page JSON to file system
└─► Export used Studio Components
▼
Result: Page accessible at /[app_route]/[page_route]
Using optimized production assets
This publishing system enables a smooth workflow from visual editing to production deployment, with built-in optimization, validation, and export capabilities for both hosted Studio apps and exportable Frappe applications.