ArtisanPack UI
DocsCMS FrameworkSite Editor - Hooks and Events

Site Editor - Hooks and Events

CMS Framework v2.16.0

The Site Editor module exposes its resolved entities through the filter family. These filters are how the optional package consumes cms-framework's...

The Site Editor module exposes its resolved entities through the ap.visual-editor.* filter family. These filters are how the optional artisanpack-ui/visual-editor package consumes cms-framework's templates, patterns, global styles, and menus.

Added in 2.0.0.

All filter registrations are gated behind class_exists(ArtisanPackUI\VisualEditor\VisualEditor::class) so cms-framework boots cleanly when visual-editor isn't installed.

Filter registrations

Filter name Shape Source
ap.visual-editor.templates array<string, ResolvedEntity> keyed by slug TemplateResolver::all()
ap.visual-editor.template-parts array<string, ResolvedEntity> keyed by slug TemplatePartResolver::all()
ap.visual-editor.patterns array<string, array> keyed by storage slug PatternResolver::toFilterMap()
ap.visual-editor.global-styles ?ResolvedGlobalStyles (singleton, nullable) GlobalStylesResolver::resolve()
ap.visual-editor.navigation array<string, array> keyed by location MenuResolver::all()
ap.visual-editor.resources array<class-string, array> of registered content types Auto from HasBlockContent adopters

Merge behavior

For the keyed-map filters (templates, template-parts, patterns, navigation), cms-framework's resolved map merges under the existing map so app-level config or earlier filter contributors win on key collision.

For the singleton global-styles filter, cms-framework replaces the value (returning its own ResolvedGlobalStyles when one resolves and falling through otherwise).

For resources, cms-framework merges its content-type registrations into the existing array.

Schema guards

All filter callbacks check Schema::hasTable() for their backing tables before resolving. This lets cms-framework boot during CI bootstrap and fresh installs where migrations haven't run yet. When a table is missing, the callback returns the existing filter value unchanged.

Adding your own filter consumers

The filters are dispatched via the artisanpack-ui/hooks package. Listen to them with addFilter():

use function addFilter;

addFilter('ap.visual-editor.patterns', function (array $patterns): array {
    // Add a runtime-generated pattern to the editor inserter
    $patterns['my-app/dynamic-pattern'] = [
        'name'        => 'my-app/dynamic-pattern',
        'title'       => 'Dynamic Pattern',
        'description' => 'A pattern generated by app code.',
        'content'     => '<!-- wp:paragraph --><p>Hi</p><!-- /wp:paragraph -->',
        'categories'  => ['featured'],
        'block_types' => [],
        'source'      => 'app',
        'synced'      => false,
    ];

    return $patterns;
});

Blade directives

@cmsGlobalStyles

Renders a <style id="cms-global-styles"> block with the resolved global-styles CSS. Themes opt in by including this directive in their root layout:

<head>
    @cmsGlobalStyles
</head>

The output is cached on a content-hash key; the cache busts on GlobalStyles model saves and deletes via a model observer wired in SiteEditorServiceProvider.

Framework filters

Filter Arguments Since Purpose
ap.cmsFramework.globalStyles.css (string $css, ResolvedGlobalStyles $resolved) 2.15.0 Filters the emitted global-styles CSS. Runs on every GlobalStylesEmitter::emit() call, after the cache, so whatever a callback adds is never frozen into the cached CSS. Feeds @cmsGlobalStyles and GET /api/v1/global-styles/css. Use it to append rules the emitter doesn't produce, such as block style variations on hosts without visual-editor.
ap.cmsFramework.siteEditor.blockContent (array $blocks, ?array $storedBlocks, string $context) 2.15.0 Filters the block tree a Site Editor API write is about to save. Runs on every create, update and upsert in TemplatesController, TemplatePartsController, BlocksController and BlockPatternsController, and (since 2.15.1) when PatternResolver::cloneToUser() copies a theme pattern into a user pattern (context block-pattern, with the theme file's blocks as both $blocks and $storedBlocks), before the row is written. $storedBlocks is the record's current block content (decoded; [] when it has none). When the write creates the first DB override of a theme file (template, template part or theme pattern), it is that file's parsed blocks, the version the Site Editor showed; otherwise a create passes null. $context is template, template-part, block or block-pattern. visual-editor 1.15 uses it to enforce its visual-editor.edit-block-css ability on per-block Custom CSS.
ap.cmsFramework.abilities.siteEditor.read (string $ability) 2.15.0 Ability every Site Editor API route requires. Default manage-content.
ap.cmsFramework.abilities.siteEditor.structure (string $ability) 2.15.0 Ability for writes to templates, template parts, global styles and menus. Default manage-themes.
ap.cmsFramework.abilities.siteEditor.content (string $ability) 2.15.0 Ability for writes to patterns and synced blocks. Default manage-content.

See API Reference: Authorization for how the abilities combine.

use ArtisanPackUI\CMSFramework\Modules\SiteEditor\Resolution\ResolvedGlobalStyles;

addFilter( 'ap.cmsFramework.globalStyles.css', function ( string $css, ResolvedGlobalStyles $resolved ): string {
    return $css . "\n\n:root :where(.wp-block-group.is-style-card) { padding: 2rem; }";
} );

Every callback receives both arguments. A callback that returns something other than a string is ignored. The appended CSS is printed inside the directive's <style> element, so keep it to CSS you control: the filter output isn't validated the way theme.json values are.

Filtering block content before it is saved

ap.cmsFramework.siteEditor.blockContent lets a host veto or rewrite parts of a block tree that comes in through the Site Editor API, the way visual-editor's CustomCssGuard keeps only the Custom CSS already stored on a record:

addFilter( 'ap.cmsFramework.siteEditor.blockContent', function ( array $blocks, ?array $storedBlocks, string $context ): array {
    if ( auth()->user()?->can( 'visual-editor.edit-block-css' ) ) {
        return $blocks;
    }

    // Drop incoming Custom CSS on top-level blocks. A full guard would walk
    // innerBlocks and restore each block's value from $storedBlocks instead.
    return array_map( function ( array $block ): array {
        unset( $block['attrs']['artisanpackEffects']['customCss'] );

        return $block;
    }, $blocks );
} );

Every callback receives all three arguments. A callback that returns something other than an array is ignored and the incoming blocks are saved as sent.

The filter runs in two places:

  • API writes. A Site Editor API write runs the filter only when the request carries a block_content array; a write that leaves the blocks alone, or sets them to null, skips it. The first write that overrides a theme-file template, template part or pattern passes that file's parsed blocks (from the same resolver the index and show endpoints use) as $storedBlocks, so Custom CSS the theme ships survives the first save. Blade templates are read-only and never reach the filter.
  • Theme pattern clones. Since 2.15.1, PatternResolver::cloneToUser() always runs the theme pattern's blocks through the filter, with the same blocks as $storedBlocks and block-pattern as $context. No request is involved, so a model-level guard such as visual-editor's doesn't strip the theme's Custom CSS from the clone.

Framework actions

Action Arguments Since Purpose
ap.cmsFramework.siteEditor.reverted (string $type, ?string $slug, string $theme) 2.16.0 Fires after a revert deletes a DB override for the active theme. $type is template, template-part, global-styles or menu-location. $slug is the template or part slug, or the menu location key; it is null for global-styles, which has one row per theme. $theme is the theme slug the row belonged to.

The action fires from TemplateResolver::revert(), TemplatePartResolver::revert(), GlobalStylesResolver::revert() and Menus::unassign() (which MenuResolver::revert() calls), so it covers the DELETE API endpoints as well as reverts from console commands, jobs and direct calls. It fires only when a row was actually deleted. Nothing fires when there was no override to revert, or when a deleting listener cancels the delete by returning false; in both cases the method returns false.

Use it to invalidate caches the same way you do on save:

addAction( 'ap.cmsFramework.siteEditor.reverted', function ( string $type, ?string $slug, string $theme ): void {
    Cache::forget( "my-app.site-editor.{$theme}.{$type}." . ( $slug ?? 'global' ) );
} );

Model events

Since 2.16.0 (#423), reverts delete the Template, TemplatePart, GlobalStyles and MenuLocationAssignment row through the model, so eloquent.deleting and eloquent.deleted fire for reverts just as eloquent.saved does for saves. Before 2.16.0 they used a query-level delete and fired no model event.

The GlobalStyles model has a registered observer (in SiteEditorServiceProvider::boot()) that:

  • On saved — invalidates the GlobalStylesEmitter cache so the next render re-emits CSS.
  • On deleted — same. Since 2.16.0 this includes GlobalStylesResolver::revert() and DELETE /api/v1/global-styles.

This is the only event-driven cache invalidation in the module; the other entities resolve fresh on each request (and lean on per-request request-local caches where appropriate).