Visual Editor - v1.5.0
Block Visibility
Runtime rules that decide whether an individual block renders. Ships in 1.4.0 across three issues:
- #491 — Contextual rules: master Hide, screen-size, query-string, referrer, browser / OS / device.
- #492 — User & auth rules: login state, user role, specific user.
- #493 — Scheduling rules: single date/time window, recurring weekly schedule, per-rule timezone override.
All rules evaluate server-side so hidden blocks never emit markup — there is no flash of content and no client-only privacy leak.
For editors
Every block that opts in surfaces a single Visibility panel in the inspector with a subsection per rule family. Only families you've enabled render — an untouched panel stays minimal. Combine any number of rules on a single block; the block renders only when every active rule passes.
Rule shape at a glance:
| Rule | What it checks |
|---|---|
| Hide block | Master toggle — the block disappears from output entirely. |
| Screen size | Emits @media rules per named breakpoint. Zero runtime JS. |
| Query string | ?utm_source=newsletter, wildcard ?debug=*, Any / All. |
| Referrer | Literal twitter.com, wildcard *.example.com, or (direct). |
| Browser / OS / Device | Chrome / Firefox / Safari / …, iOS / Android / …, mobile / tablet. |
| Login state | Logged in / Logged out / Either. |
| User role | Any / All of a list of role slugs. |
| Specific user | Match by email or id. Autocomplete backed by /users/search. |
| Date/time window | Start and/or end datetime. Per-rule timezone override. |
| Recurring schedule | Up to 14 weekly windows (day + HH:MM start/end). |
The editor canvas dims hidden blocks so authors can still see and select them while they're toggled off.
For developers
Configuration
// config/artisanpack/visual-editor.php
'visibility' => [
'enabled' => true,
'user_model' => null, // defaults to auth.providers.users.model
'user_search_columns' => null, // defaults to [ 'email', 'name' ] when present
],
Set enabled to false to bypass every visibility rule site-wide —
useful during incident response if a mis-published rule accidentally
hides critical content. When disabled the evaluator returns
VisibilityDecision::visible() for every block without touching the
rule registry.
Server-side integration
The Blade renderer calls VisibilityEvaluator::evaluate() for every
block before rendering it. Blocks that return hidden() are dropped
outright; blocks that return cssHidden() (only the screen-size rule
today) are wrapped in a per-block <style> block emitting
breakpoint-scoped display:none !important rules.
For the React and Vue renderers, pipe your tree through
\ArtisanPackUI\VisualEditor\Visibility\TreePruner::prune() before
serialising it into the page payload. Hidden blocks are removed and
CSS-hidden blocks receive a _veHiddenBreakpoints side-channel
attribute the client picks up via stampVisibilityScopes().
use ArtisanPackUI\VisualEditor\Visibility\TreePruner;
$tree = $pruner->prune( $post->content );
return Inertia::render( 'PostShow', [ 'blocks' => $tree ] );
import { BlockTree, filterVisibleBlocks, stampVisibilityScopes } from '@artisanpack-ui/visual-editor-renderer-react';
const { tree, css } = stampVisibilityScopes( filterVisibleBlocks( blocks ) );
return (
<>
{css !== '' && <style>{css}</style>}
<BlockTree tree={tree} />
</>
);
Debug hook
Every evaluation fires
ap.visualEditor.visibility.evaluated( decision, blockName, attributes, context ).
Listen from a service provider to answer "why is this block hidden?":
addAction( 'ap.visualEditor.visibility.evaluated', function ( $decision, $name, $attrs, $ctx ) {
if ( $decision->isHidden() ) {
\Log::debug( sprintf(
'%s hidden by %s (viewer: %s)',
$name,
implode( ', ', $decision->reasons ),
$ctx->isAuthenticated ? $ctx->userEmail : 'anonymous',
) );
}
} );
Adding a custom rule
Implement the VisibilityRule interface and register through the
ap.visualEditor.visibility.registerRules filter:
use ArtisanPackUI\VisualEditor\Visibility\RuleRegistry;
addFilter( 'ap.visualEditor.visibility.registerRules', function ( RuleRegistry $registry ) {
$registry->register( new \App\Visibility\CookieRule() );
return $registry;
} );
Per-block opt-out
Every block ships opted in. Blocks that must not be conditionally
hidden — e.g. a document title on a post editor — set
supports.artisanpackVisibility: false in their block.json. Opt-out
suppresses both the attribute injection and the inspector panel.
Audit command
php artisan visual-editor:audit-scheduled-blocks walks every resource
registered under artisanpack.visual-editor.resources and prints a
table of blocks with active dateTimeWindow or recurring rules.
Filter to a single resource with --resource=pages.
Timezones
Both scheduling rules interpret their configured times in the
timezone field. When omitted they fall back to
config( 'app.timezone' ), which itself falls back to UTC. Wall-clock
schedules ("10:00 America/Chicago every Sunday") stay correct across
DST transitions — the rule matches at 10:30 whether that instant is
CST or CDT.
Cache implications
User- and role-based rules make a page's rendered output dependent on
the visitor. If a scheduled or auth-dependent block sits inside a
cached template part or output cache, the cache TTL must be shorter
than the window's precision (for a 5-minute recurring window, the
cache TTL must be <5 minutes). A cache-tightening layer that
auto-shortens TTLs when scheduled blocks are discovered is planned
for a follow-up release; until then, hosts running the cache-heavy
plans should either scope their cache keys to Auth::id() or set a
short TTL for pages containing visibility-gated content.
Roles
The evaluator reads the viewer's role list from cms-framework's
RoleManager when installed, falling back to
Auth::user()->getRoleNames() (Spatie-compatible), ->roles (Eloquent
relation with slug or name), or an empty list. Hosts on a
non-standard role stack can override
VisibilityEvaluator::resolveRoles() via container rebinding.
User-agent parsing
The Browser / OS / Device rule uses a bundled regex parser covering
the top ~95% of real traffic. Hosts that need finer classification
install jenssegers/agent
and swap the bound UserAgentParser in their own service provider.