umbraco-example-generator
Umbraco Example Generator
Generate complete, testable example extensions for the Umbraco backoffice and run them using the Umbraco source's dev infrastructure.
When to Use
- Creating demonstration extensions
- Building testable extension examples
- Rapid development with hot reload
- Testing extensions without .NET backend
Related Skills
- umbraco-unit-testing - Add unit tests to examples
- umbraco-mocked-backoffice - E2E testing patterns
- umbraco-backoffice - Extension type blueprints
Quick Start
1. Clone Umbraco source (one-time setup)
git clone https://github.com/umbraco/Umbraco-CMS
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
npm install
2. Create your extension folder
my-extension/
├── index.ts # REQUIRED - exports manifests
└── my-element.ts # Your element(s)
3. Create index.ts (exports manifests)
import './my-element.js';
export const manifests = [
{
type: 'dashboard',
alias: 'My.Dashboard',
name: 'My Dashboard',
element: 'my-element',
meta: { label: 'My Dashboard', pathname: 'my-dashboard' },
conditions: [{ alias: 'Umb.Condition.SectionAlias', match: 'Umb.Section.Content' }]
}
];
4. Create your element
// my-element.ts
import { LitElement, html, customElement } from '@umbraco-cms/backoffice/external/lit';
@customElement('my-element')
export class MyElement extends LitElement {
render() {
return html`<uui-box headline="Hello">It works!</uui-box>`;
}
}
5. Run it
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
VITE_EXTERNAL_EXTENSION=/full/path/to/my-extension npm run dev:external
Open http://localhost:5173 - your extension appears in the Content section.
How It Works
The Umbraco source (Umbraco-CMS/src/Umbraco.Web.UI.Client) provides two ways to load extensions:
1. Internal Examples (npm run example)
Examples placed in the examples/ folder inside the Umbraco source.
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
npm run example
# Select from list of examples
How it works: Sets VITE_EXAMPLE_PATH and imports ./examples/{name}/index.ts
2. External Extensions (npm run dev:external)
Extensions from any location on your filesystem - perfect for developing packages.
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
VITE_EXTERNAL_EXTENSION=/path/to/your/extension npm run dev:external
How it works:
- Sets
VITE_UMBRACO_USE_MSW=on(mocked APIs) - Creates
@external-extensionalias pointing to your extension path - Imports
@external-extension/index.tsand registers exports withumbExtensionsRegistry - Resolves
@umbraco-cms/backoffice/*imports from the main project (avoids duplicate element registrations)
Extension Loading (index.ts)
// From Umbraco-CMS/src/Umbraco.Web.UI.Client/index.ts
if (import.meta.env.VITE_EXTERNAL_EXTENSION) {
const js = await import('@external-extension/index.ts');
if (js) {
Object.keys(js).forEach((key) => {
const value = js[key];
if (Array.isArray(value)) {
umbExtensionsRegistry.registerMany(value);
} else if (typeof value === 'object') {
umbExtensionsRegistry.register(value);
}
});
}
}
Key point: Your index.ts must export manifests (arrays or objects) that get registered automatically.
Setup
Prerequisites
Clone and set up the Umbraco source:
git clone https://github.com/umbraco/Umbraco-CMS
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
npm install
Extension Structure
Your extension needs this minimal structure:
my-extension/
├── index.ts # Exports manifests array (REQUIRED)
├── my-element.ts # Your element(s)
├── my-context.ts # Context (if needed)
├── package.json # Optional - for IDE support and tests
├── tsconfig.json # Optional - for IDE support
└── README.md # Documentation
Required: index.ts
Your index.ts must export manifests that will be registered:
import './my-dashboard.element.js';
export const manifests = [
{
type: 'dashboard',
alias: 'My.Dashboard',
name: 'My Dashboard',
element: 'my-dashboard',
weight: 100,
meta: {
label: 'My Dashboard',
pathname: 'my-dashboard'
},
conditions: [
{
alias: 'Umb.Condition.SectionAlias',
match: 'Umb.Section.Content'
}
]
}
];
Optional: package.json (for IDE support)
{
"name": "my-extension",
"type": "module",
"devDependencies": {
"@umbraco-cms/backoffice": "^17.0.0",
"typescript": "~5.8.0"
}
}
Important: The @umbraco-cms/backoffice dependency is only for IDE TypeScript support. At runtime, imports are resolved from the main Umbraco project.
Running Your Extension
Start the mocked backoffice
cd /path/to/Umbraco-CMS/src/Umbraco.Web.UI.Client
VITE_EXTERNAL_EXTENSION=/absolute/path/to/my-extension npm run dev:external
Open in browser
Navigate to http://localhost:5173 - your extension is loaded automatically.
Hot reload
Changes to your extension files trigger hot reload - no restart needed.
Patterns
Basic Element
// my-dashboard.element.ts
import { LitElement, html, css, customElement } from '@umbraco-cms/backoffice/external/lit';
@customElement('my-dashboard')
export class MyDashboardElement extends LitElement {
static override styles = css`
:host {
display: block;
padding: var(--uui-size-layout-1);
}
`;
override render() {
return html`
<uui-box headline="My Extension">
<p>Running in the mocked backoffice!</p>
</uui-box>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'my-dashboard': MyDashboardElement;
}
}
Element with Context
import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { EXAMPLE_MY_CONTEXT } from './my-context.js';
@customElement('example-my-feature-view')
export class ExampleMyFeatureViewElement extends UmbLitElement {
@state()
private _value?: string;
constructor() {
super();
this.consumeContext(EXAMPLE_MY_CONTEXT, (context) => {
this.observe(context.value, (value) => {
this._value = value;
});
});
}
override render() {
return html`
<uui-box headline="My Feature Example">
<p>Current value: ${this._value ?? 'Loading...'}</p>
</uui-box>
`;
}
}
export default ExampleMyFeatureViewElement;
declare global {
interface HTMLElementTagNameMap {
'example-my-feature-view': ExampleMyFeatureViewElement;
}
}
Context
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class ExampleMyContext extends UmbContextBase {
#value = new UmbStringState('initial');
readonly value = this.#value.asObservable();
constructor(host: UmbControllerHost) {
super(host, EXAMPLE_MY_CONTEXT);
}
setValue(value: string) {
this.#value.setValue(value);
}
getValue() {
return this.#value.getValue();
}
public override destroy(): void {
this.#value.destroy();
super.destroy();
}
}
export const EXAMPLE_MY_CONTEXT = new UmbContextToken<ExampleMyContext>(
'ExampleMyContext'
);
export { ExampleMyContext as api };
Adding Tests
Unit Tests
Add unit tests using @open-wc/testing. See umbraco-unit-testing skill for full setup.
npm install --save-dev @open-wc/testing @web/test-runner @web/test-runner-playwright
E2E Tests (Playwright)
Add E2E tests that run against the mocked backoffice. See umbraco-mocked-backoffice skill for patterns.
npm install --save-dev @playwright/test
npx playwright install chromium
Examples
Reference Example
Location: ./examples/workspace-feature-toggle/
A complete standalone example demonstrating:
- Workspace context with
UmbArrayState - Workspace view consuming context
- Workspace action executing context methods
- Workspace footer app showing summary
- 38 unit tests + 13 E2E tests
cd examples/workspace-feature-toggle
npm install
npm test # Unit tests
npm run test:e2e # E2E tests (requires mocked backoffice running)
Official Umbraco Examples
Location: Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/
27 official examples covering all extension types. Run any example:
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
npm run example
# Select from list
Naming Conventions
| Item | Convention | Example |
|---|---|---|
| Directory | kebab-case describing feature | workspace-context-counter |
| Alias prefix | example. |
example.workspaceView.counter |
| Element prefix | example- |
example-counter-view |
| Context token | EXAMPLE_ + SCREAMING_CASE |
EXAMPLE_COUNTER_CONTEXT |
Troubleshooting
Extension not appearing
- Check
index.tsexports amanifestsarray - Verify the path in
VITE_EXTERNAL_EXTENSIONis absolute - Check browser console for
📦 Loading external extension from:message - Ensure condition matches the section you're viewing
Import errors
Imports should use @umbraco-cms/backoffice/*. The Vite plugin resolves these from the main project.
"CustomElementRegistry" already defined
Your extension's node_modules is being used instead of the main project's. The external-extension-resolver plugin should handle this, but ensure:
- You're using
npm run dev:external - Imports use
@umbraco-cms/backoffice/*not relative paths to node_modules
Changes not hot reloading
Ensure the file is within the path specified by VITE_EXTERNAL_EXTENSION. Only files in that directory tree are watched.