Widget Development
Practical guide to building and testing SveltyCMS widgets.
On this page
Building widgets for SveltyCMS is a straightforward process using our 3-Pillar system.
๐ Creating a Custom Widget
Create a kebab-case folder in src/widgets/custom/ (same rules as marketplace packages).
Naming (required)
| Folder (kebab-case) | Factory Name (PascalCase) |
|---|---|
phone-number/ |
Name: "PhoneNumber" |
seo/ |
Name: "SEO" |
Invariant: widgetNameToFolder(Name) === folder. Invalid custom widgets are not registered. See Marketplace.
Derived names are title-cased, not acronym-preserving. If a factory omits
Name, it is derived from the folder (seo/โSeo), so schemas referencing{ Name: "SEO" }would miss the registry entry. Always declareNameexplicitly โ especially for acronyms.
Folder Structure
index.ts: Definition (createWidget({ Name: "โฆ" }))input.svelte: Input componentdisplay.svelte: Display componenttests/: Portable unit tests (Drupal-style)
Try It Live
Open the starter template below in StackBlitz and follow along:
The starter includes a working widget scaffold with the 3-pillar structure, Valibot validation, and pre-configured tests. Click the file tree to explore.
๐งช Testing
Custom widgets must contain their own tests within a tests/ subdirectory.
// Example: src/widgets/custom/my-widget/tests/my-widget.test.ts
import { describe, it, expect } from "vitest";
import Widget from "../index";
import { safeParse } from "valibot";
describe("MyWidget", () => {
it("validates data", () => {
const field = Widget({ label: "Test" });
const schema = (field.widget.validationSchema as any)(field);
expect(safeParse(schema, "valid").success).toBe(true);
});
});
๐ Multi-Tenant Safety
Widgets are UI components โ they render data provided by the server and never query the database directly. However, widget definitions (index.ts) that include server-side hooks or validation must be tenant-aware:
// โ
Correct: Widget definition accepts tenant context
const MyWidget = createWidget({
Name: "MyWidget",
// Server-side validation receives tenant context
modifyRequest: async ({ data, type, tenantId }) => {
// tenantId is automatically injected by the widget system
return data;
},
});
Key rules for widget developers:
- Never access
fetch()or API routes directly from a widget input/display component - Always use the provided
valueandupdateprops โ they maintain tenant context - If your widget definition has a
modifyRequesthook, always passtenantIdthrough