skills/pimcore-studio-ui-bundle-structure/SKILL.md
Organization and structure of Pimcore Studio bundles - modules, components, file layout
npx skillsauth add pimcore/skills pimcore-studio-ui-bundle-structureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill covers BUNDLE structure (outside studio-ui-bundle)
BEFORE writing any import, read CRITICAL-IMPORT-PATHS.md. All bundle imports use @pimcore/studio-ui-bundle/*.
Understanding the structure and organization of Pimcore Studio bundles:
Use this when:
{bundle-name}/ # e.g., personalization-bundle
│
├── assets/studio/ # Frontend code (TypeScript/React)
│ ├── js/
│ │ ├── src/
│ │ │ ├── modules/ # All Studio modules
│ │ │ │ ├── {module-name}/ # e.g., targeting-rules, personalization
│ │ │ │ │ ├── components/ # React components
│ │ │ │ │ │ ├── editor/
│ │ │ │ │ │ ├── detail/
│ │ │ │ │ │ └── list/
│ │ │ │ │ ├── hooks/ # Custom React hooks
│ │ │ │ │ ├── api/ # API utilities (if complex)
│ │ │ │ │ └── {module-name}-api.tsx # RTK Query endpoints
│ │ │ │ └── index.tsx # Re-exports all modules
│ │ │ └── index.tsx # Main entry point
│ │ └── tsconfig.json
│ ├── package.json # Frontend dependencies
│ └── webpack.config.js # Build configuration
│
├── public/studio/ # Build output (committed to Git)
│ └── build/
│ └── {hash}/ # Unique build hash
│ ├── remoteEntry.js # Module federation entry
│ └── *.js # Compiled bundles
│
├── src/ # PHP backend code
│ ├── Controller/ # API controllers
│ │ └── Studio/
│ │ └── {ModuleName}Controller.php
│ ├── EventSubscriber/ # Event subscribers
│ ├── Service/ # Business logic
│ └── DependencyInjection/
│
├── translations/ # Translation files
│ ├── studio.en.yaml # English (Studio UI)
│ └── admin.en.yaml # Legacy admin (if any)
│
├── config/ # Configuration
│ └── services.yaml
│
└── composer.json # PHP dependencies
modules/{module-name}/
├── components/ # React components
│ ├── editor/ # Edit/create views
│ │ ├── {module}-editor.tsx
│ │ └── tabs/ # If editor has tabs
│ ├── detail/ # Detail/view components
│ │ └── {module}-detail.tsx
│ ├── list/ # List/table views
│ │ └── {module}-list.tsx
│ └── shared/ # Shared components
│
├── hooks/ # Custom hooks
│ ├── use-{module}-helper.tsx
│ └── use-{module}-form.tsx
│
└── {module}-api.tsx # RTK Query API definition
modules/targeting-rules/
├── components/
│ ├── editor/
│ │ └── targeting-rule-editor.tsx # Main editor component
│ ├── list/
│ │ └── targeting-rules-list.tsx # List view
│ └── shared/
│ └── rule-status-badge.tsx # Shared UI component
│
├── hooks/
│ └── use-targeting-rules-helper.tsx # Helper hook
│
└── targeting-rules-api.tsx # API endpoints
// modules/{module-name}/{module-name}-api.tsx
import { api as baseApi } from '@Pimcore/modules/app/app-api'
// Type definitions
export interface EntityDetail {
id: number
settings: {
name: string
active: boolean
// ... other fields
}
}
export interface EntityListItem {
id: number
name: string
// ... abbreviated fields
}
// API endpoints
export const api = baseApi.injectEndpoints({
endpoints: (build) => ({
// List endpoint
getEntityList: build.query<EntityListItem[], void>({
query: () => ({
url: '/pimcore-bundle-{bundle}/{entity}/list',
}),
}),
// Detail endpoint
getEntityById: build.query<EntityDetail, { id: number }>({
query: ({ id }) => ({
url: `/pimcore-bundle-{bundle}/{entity}/${id}`,
}),
}),
// Update mutation
updateEntity: build.mutation<void, { id: number; body: any }>({
query: ({ id, body }) => ({
url: `/pimcore-bundle-{bundle}/{entity}/${id}`,
method: 'PUT',
body,
}),
}),
// Delete mutation
deleteEntity: build.mutation<void, { id: number }>({
query: ({ id }) => ({
url: `/pimcore-bundle-{bundle}/{entity}/${id}`,
method: 'DELETE',
}),
}),
}),
})
// Export hooks
export const {
useGetEntityListQuery,
useGetEntityByIdQuery,
useUpdateEntityMutation,
useDeleteEntityMutation,
} = api
// modules/targeting-rules/targeting-rules-api.tsx
export const api = baseApi.injectEndpoints({
endpoints: (build) => ({
bundlePersonalizationTargetingRuleGetById: build.query<
TargetingRuleDetail,
{ ruleId: number }
>({
query: ({ ruleId }) => ({
url: `/pimcore-bundle-personalization/targeting-rule/${ruleId}`,
}),
}),
bundlePersonalizationTargetingRuleUpdate: build.mutation<
void,
{ ruleId: number; body: UpdateBody }
>({
query: ({ ruleId, body }) => ({
url: `/pimcore-bundle-personalization/targeting-rule/${ruleId}`,
method: 'PUT',
body,
}),
}),
}),
})
export const {
useBundlePersonalizationTargetingRuleGetByIdQuery,
useBundlePersonalizationTargetingRuleUpdateMutation,
} = api
targeting-rule-editor.tsxTargetingRuleEditor// Editor component
export const TargetingRuleEditor: FC<Props> = ({ rule }) => {
// Edit existing or create new
}
// Detail component
export const TargetingRuleDetail: FC<Props> = ({ ruleId }) => {
// View-only display
}
// List component
export const TargetingRulesList: FC = () => {
// Table/grid of items
}
translations/studio.en.yaml
# Module name (top level)
personalization:
# Entity/feature (second level)
targeting-rules:
# Context (third level)
list:
title: 'Targeting Rules'
empty: 'No targeting rules found'
editor:
title: 'Edit Targeting Rule'
save: 'Save'
cancel: 'Cancel'
# Actions (third level)
create:
success: 'Targeting rule created'
error: 'Failed to create targeting rule'
update:
success: 'Saved successfully'
error: 'Failed to save targeting rule'
delete:
success: 'Targeting rule deleted'
error: 'Failed to delete targeting rule'
confirm: 'Are you sure you want to delete this rule?'
# Another entity
target-groups:
list:
title: 'Target Groups'
update:
success: 'Saved successfully'
// src/Controller/Studio/{Module}Controller.php
namespace Pimcore\Bundle\{BundleName}\Controller\Studio;
use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/pimcore-bundle-{bundle}/{entity}', name: 'pimcore_bundle_{bundle}_{entity}_')]
class EntityController extends AbstractApiController
{
#[Route('/{id}', name: 'get', methods: ['GET'])]
public function getById(int $id): JsonResponse
{
// Implementation
}
#[Route('/{id}', name: 'update', methods: ['PUT'])]
public function update(int $id): JsonResponse
{
// Implementation
}
}
// src/Service/{Module}Service.php
namespace Pimcore\Bundle\{BundleName}\Service;
class EntityService
{
public function getById(int $id): array
{
// Business logic
}
public function update(int $id, array $data): void
{
// Business logic
}
}
// assets/studio/js/src/index.tsx
export * from './modules/targeting-rules/targeting-rules-api'
export * from './modules/personalization/personalization-api'
// Export other modules
// assets/studio/js/src/modules/index.tsx
export * from './targeting-rules/targeting-rules-api'
export * from './personalization/personalization-api'
public/studio/build/{hash}/
remoteEntry.js: Module federation entry point (loaded by Studio)*.js: Compiled JavaScript bundles*.js.map: Source maps for debuggingtargeting-rules)assets/studio/js/src/modules/{module}/components/editor/, list/, detail/assets/studio/js/src/modules/{module}/{module}-api.tsxtranslations/studio.en.yamltargeting-rules)src/Controller/Studio/{Module}Controller.phpmodules/data-importer/
├── components/
│ ├── editor/
│ │ ├── data-importer-editor.tsx
│ │ └── tabs/
│ │ ├── general-tab.tsx
│ │ ├── mapping-tab.tsx
│ │ └── schedule-tab.tsx
│ └── list/
│ └── data-importer-list.tsx
modules/workflow/
├── components/
│ ├── editor/
│ │ ├── workflow-editor.tsx
│ │ └── forms/
│ │ ├── basic-info-form.tsx
│ │ ├── transitions-form.tsx
│ │ └── places-form.tsx
modules/reports/
├── components/
│ ├── dashboard/
│ │ └── reports-dashboard.tsx
│ ├── viewer/
│ │ └── report-viewer.tsx
│ └── builder/
│ └── report-builder.tsx
studio.en.yaml/pimcore-bundle-{name}/{entity}/{id}assets/studio/js/src/modules/{module-name}/{module-name}-api.tsxcomponents/translations/studio.en.yamlsrc/Controller/Studio/{Module}Controller.phpmodules/index.tsxnpm run check-types && npm run lint-fixnpm run dev (bundles) or npm run dev-app (SDK)targeting-rules not targetingRules)editor/, list/, detail/index.tsx files for re-exportsmodules/{module}/{module}-api.tsxtranslations/studio.en.yamlmodule.entity.action.resultFirst: Check dev server status
If dev server IS running:
If dev server NOT running:
npm run dev-appnpm run devpublic/studio/build/{hash}/Always safe to run:
npm run check-types
Fix common issues:
AI/Assistants: NEVER start or stop dev servers! They are managed by the user.
The user operates in ONE of two modes:
Mode 1: Dev Server Running (Development)
npm run check-types, npm run lint-fixnpm run build, npm run dev, npm run dev-appMode 2: Build Mode (Production/Testing)
ALWAYS ask the user first:
Is your dev server currently running?
- If YES → Run only npm run check-types and npm run lint-fix
- If NO → Proceed with build commands
Location: vendor/pimcore/studio-ui-bundle/assets/studio/
# Development build (when dev server NOT running)
npm run dev-app
# Type checking (safe to run anytime)
npm run check-types
# Linting with auto-fix (safe to run anytime)
npm run lint-fix
# Production build
npm run build
When to build studio-ui-bundle:
Location: {your-bundle}/assets/studio/
# Development build (when dev server NOT running)
npm run dev
# Type checking (safe to run anytime)
npm run check-types
# Linting with auto-fix (safe to run anytime)
npm run lint-fix
# Production build
npm run build
When developing a bundle that uses SDK changes:
Step 1: Check Dev Server Status
AI: "Is your dev server currently running?"
Step 2a: If Dev Server Running
# Only validate, don't build
cd {your-bundle}/assets/studio
npm run check-types
npm run lint-fix
Step 2b: If Dev Server NOT Running
# Build SDK changes first
cd vendor/pimcore/studio-ui-bundle/assets/studio
npm run dev-app
# Then build your bundle
cd ../../../../{your-bundle}/assets/studio
npm run dev
Users start dev servers manually in this order:
Terminal 1: SDK Server
cd vendor/pimcore/studio-ui-bundle/assets/studio
npm run dev-server-sdk # Runs on port 3030
Terminal 2: Bundle Server
cd {your-bundle}/assets/studio
npm run dev-server # Runs on port 3031
Why this order:
When dev servers are running:
These can run regardless of dev server status:
# Type checking
npm run check-types
# Linting with auto-fix
npm run lint-fix
# Both together
npm run check-types && npm run lint-fix
If dev server is running:
If dev server is NOT running:
public/studio/build/{hash}/ for outputRunning builds while dev server is active will:
Always ask first!
tools
UX and UI design conventions for Pimcore Studio - layout, spacing, action labels, writing style, and design principles for consistent extensions
tools
Widget system in Pimcore Studio UI - registering widgets, opening them in layout areas, WidgetManagerTabConfig, and connecting widgets to navigation
tools
How bundles consume the Pimcore Studio UI SDK - plugins, modules, DI, registries, and imports
development
TypeScript coding standards and best practices for Pimcore Studio UI - type safety, null checks, and code quality