A shortcode is the bridge between the visual editor and your PHP: editors drag a block, fill a small form, and the page stores a tag like <x-shortcode-testimonials limit="6"></x-shortcode-testimonials> that your Blade component renders with live data on every request.
PHP Editor (GrapesJS) Public site
ShortcodeContract::definition() ----> block + component type + settings form
id, name, tag, category, icon, drag onto page, edit traits ----> saved HTML contains
defaults, traits, assets, ai live preview from server <x-shortcode-ID attr="..."></...>
Shortcode::registerIfMissing(Class) |
Blade::component('shortcode-ID', Class) <--------------------------------------------- Blade::render(content)
Component::render() -> theme/module view assets of tags present are injectedApp\Services\PageBuilder\Contracts\ShortcodeContract: a static id() and a static definition(): ShortcodeDefinition.Shortcode::registerIfMissing(MyShortcode::class)). The editor page embeds ShortcodeRegistry::all() as JSON.PageSlugRenderer compiles the saved content with Blade::render(), so the component class runs with current data.A complete, tested example is in Tutorial step 8 (Testimonials block). This page is the reference.
final class NoteShortcode implements ShortcodeContract
{
public static function id(): string { return 'playground-note'; } // unique, kebab-case, module-prefixed
public static function definition(): ShortcodeDefinition
{
return ShortcodeDefinition::make(
id: self::id(),
name: __('playground::p.note'), // block label
tag: 'x-shortcode-playground-note', // Blade tag; convention: x-shortcode-{id}
category: 'playground', // block-panel group
priority: 20, // order inside the group
)
->icon('<i class="ph ph-note" aria-hidden="true"></i>') // HTML or inline SVG
->description('Shows one note.')
->defaults(['note' => '']) // attributes written on a freshly dropped block
->aliases(['x-playground-note']) // extra tags this definition also owns (assets are matched for them too)
->assets([...]) // see "Assets"
->ai(['enabled' => true, 'module' => 'playground'])
->traits([ /* the settings form */ ]);
}
}ShortcodeDefinition method | Purpose |
|---|---|
make($id, $name, $tag, $category = 'shortcodes', $priority = 100) | Create. The preview URL (admin.page-builder.shortcodes.preview) is derived from $id |
icon(?string), description(?string) | Block panel presentation |
defaults(array) | Initial attribute values |
traits(array) / trait($t) | The settings form (below) |
wrapper(?string) | Optional wrapper hint for the editor |
assets(array) | Assets to load when the tag is on a page |
aliases(array) | Additional tags handled by the same definition |
ai(array) | Opt in to the AI assistant: ['enabled' => true, 'module' => 'alias'] |
enabled(bool) / disabled() | Hide a definition without unregistering it |
category(), priority() | Change after creation |
Each trait is created with XTrait::make($attributeName, $label) and writes to the attribute of that name on the tag.
| Trait | Control | Specific methods |
|---|---|---|
TextTrait | Text input | — |
TextareaTrait | Multiline | rows(int) |
NumberTrait | Number | min(), max(), step() |
SelectTrait | Fixed dropdown | options([['id' => 'cards', 'name' => 'Cards'], ...]) |
AsyncSelectTrait | Search-as-you-type dropdown | provider('notes'), multiple(), minimumInputLength(n) |
CheckboxTrait | Checkbox | values('true', 'false') — the strings written for on/off |
ColorTrait | Colour picker | — |
ImageTrait | Image picker | mediaLibrary(bool) |
HiddenTrait | No UI, carries a value | — |
All traits share: placeholder(), default(), required() (the preview waits until required fields are filled), hint(), changeProp() (property vs attribute) and append(array) for raw editor options.
Implement HasShortcodeOptions on the same class. An AsyncSelectTrait with ->provider('notes') gets an options URL; the editor calls it while the user types and your options() returns [['id' => '1', 'name' => 'Alpha'], ...]:
public function options(string $field, array $context = []): array
{
if ($field !== 'notes') {
return [];
}
return Note::query()
->when(filled($context['q'] ?? null), fn ($q) => $q->where('title', 'like', '%' . $context['q'] . '%'))
->limit(20)->get()
->map(fn (Note $note): array => ['id' => (string) $note->id, 'name' => $note->title])
->all();
}
// GET /admin/page-builder/shortcodes/playground-note/options/notes?q=Alp (admin session, route name admin.page-builder.shortcodes.options)Cap results (20–50) and match on the translated name with whereTranslationLike() for translatable models. This endpoint runs inside the admin group (auth, verified, 2fa) but has no extra permission; expose only data any page editor may see.
class NoteBlock extends Component
{
public ?Note $record;
// Tag attributes arrive as STRINGS. Hyphenated names become camelCase arguments:
// note="3" heading="Hi" show-rating="true" => __construct($note, $heading, $showRating)
public function __construct(public string $note = '', public string $heading = '')
{
$this->record = $note !== '' ? Note::find((int) $note) : null;
}
public function render(): View
{
return view('playground::components.note-block');
}
}
// Provider boot(): make the tag resolvable
Blade::component('shortcode-playground-note', NoteBlock::class);filter_var(..., FILTER_VALIDATE_BOOLEAN), and fall back to defaults (or to settings, as the tutorial does).{{ }}; never {!! !!} on attribute-derived text.<x-shortcode-foo></x-shortcode-foo>; GrapesJS cannot round-trip self-closing custom tags.resources/themes/{theme}/vendor/{alias}/components/….Deactivating the plugin breaks every shortcode on pages that use it
If a saved page still contains a tag whose component no longer exists, Blade::render() throws. PageSlugRenderer swallows the exception and prints the stored HTML raw, so a valid shortcode placed beside one orphaned tag stops rendering too. Document this for site owners; consider registering a tiny fallback component that renders nothing when a plugin is unloaded.
The editor loads GET admin.page-builder.shortcodes.preview?attributes[...] and shows the returned HTML in the canvas. By default the server renders the tag itself (<x-shortcode-id attr="..."> through Blade). Implement HasShortcodePreview::preview(array $attributes): string for a cheaper or sandboxed preview.
defaults or a trait survive; names starting with __ are dropped; undefined/null strings and empties are removed; select values outside the trait's options fall back to the trait default.X-TruCMS-Styles and X-TruCMS-Scripts (JSON arrays) so the editor can inject them once into the canvas.required() trait left empty makes the preview show "Select … to load the preview" instead of calling the server.GET render-shortcode/{component} (front theme active, used by older blocks and template insertion).->assets([
'bundle' => 'playground.note', // de-duplication group name
'priority' => 50, // ascending; lower loads first
'vite' => ['Modules/Playground/resources/js/front/note.js'], // needs a Vite entry in the root config
'styles' => ['playground-note' => '/modules/playground/note.css'], // key => url (or ['url' => ..., 'attributes' => [...]])
'scripts' => ['playground-note' => ['url' => '/modules/playground/note.js', 'attributes' => ['defer' => true]]],
])ShortcodeRegistry::registerContentAssets($content): for every registered definition whose tag (or alias) appears in the content, its assets are queued in the per-request PageBuilderAssetRegistry, then printed by <x-page-builder-assets placement="head|body"> in the layouts. Pages without the tag load nothing..js/.ts Vite entries and script URLs go before </body>. Duplicate URLs/entries are emitted once.Shortcode::registerMarker('data-lm-', ['bundle' => 'library.motion', 'vite' => [...]], 'library-motion') loads them when the content contains that string (the template library uses this for its motion and widget scripts).PageSlugRenderer (a module view), register assets yourself: app(PageBuilderAssetRegistry::class)->registerVite([...], 'bundle', 40).App\Facades\Shortcode)| Call | Purpose |
|---|---|
category($id, $label, $priority = 100) | Block-panel group. Priority scale shared with the editor: 10–29 editor basics, 30–99 module categories (Booking 30, TruCommerce several categories in that range), 100+ everything else (core content 200, forms 210) |
registerIfMissing($class) | Register unless the id is taken. Use this |
register($class) | Register; throws InvalidArgumentException if the id exists |
replace($class) | Override an existing id (replace a core block with yours) |
group($shortcodeId, $category) | Move a registered shortcode to a category |
has($id), get($id), handler($id), all(), grouped(), categories() | Introspection (get() returns the definition array) |
registerMarker($marker, $assets, $id), registerContentAssets($content) | Asset matching (above) |
definitionForTag($tag) | Find a definition by tag or alias |
__('alias::...')) and keep ids stable once released: saved pages contain them.Blade::render('<x-shortcode-id ... />') and asserts the registry contains the definition (see the tutorial test file).