ArtisanPack UI
DocsVisual EditorSite editor

Site editor

Visual Editor v1.18.0

The site editor is the surface for editing site-wide chrome — templates, template parts, global styles, navigation menus, and patterns — without...

The site editor is the surface for editing site-wide chrome — templates, template parts, global styles, navigation menus, and patterns — without touching post content. It mounts at /visual-editor/site and uses the same Gutenberg block-editor primitives as the post editor, wrapped in a custom shell that owns the navigation, tab structure, and entity switching.

The site editor requires artisanpack-ui/cms-framework to be installed — templates, template parts, global styles, patterns, and navigation menus are persisted by cms-framework's models. When cms-framework is missing, the install gate (#432) surfaces a "cms-framework required" page instead of mounting the editor.


1. Layout

┌─────────────────────────────────────────────────────────────────────┐
│  Topbar: Site · entity title · Save · Preview                      │
├─────────────────────┬───────────────────────────────────────────────┤
│                     │                                               │
│   Navigator         │            Canvas frame                       │
│   sidebar           │            (iframe + block editor)            │
│                     │                                               │
│   ── Templates      │                                               │
│   ── Template parts │                                               │
│   ── Patterns       │                                               │
│   ── Styles         │                                               │
│   ── Navigation     │                                               │
│                     │                                               │
└─────────────────────┴───────────────────────────────────────────────┘
  • Topbar — entity-title, save, preview, "back to site" link.
  • Navigator sidebar (left) — tree of all editable entities, grouped into five sections: Templates, Template Parts, Patterns, Styles, Navigation. Click an entity to load it into the canvas.
  • Canvas frame (center) — iframe-wrapped block editor. Same component set as the post editor, scoped to the selected entity.

2. Access gate

Site-editor access is fail-closed. The default SiteEditorAccessGate binding is DenyByDefaultGate — every request returns 403 until you bind something permissive:

// AppServiceProvider::register()
$this->app->bind(
    \ArtisanPackUI\VisualEditor\SiteEditor\Contracts\SiteEditorAccessGate::class,
    \App\Auth\AllowAdminsGate::class,
);

When cms-framework is installed it auto-binds CmsFrameworkInstallGate, which:

  1. Confirms cms-framework is installed and migrations have run.
  2. Confirms the current user has the visual_editor.site.edit permission (or the legacy "any authenticated" baseline in V1.0).

Override the binding to integrate with your own RBAC. See Content model §3 and Access Gate.


3. Sections

Templates

Server-stored template records (wp_template shape) that drive full-page rendering. The navigator lists every template the resolver knows about — both theme-provided defaults and user overrides — and the editor lets authors edit either.

See Templates for the hierarchy, fallback chain, and how the front-end renderer picks a template for a route.

Template parts

Reusable chunks (wp_template_part shape) that get included into templates via the core/template-part block. Headers, footers, sidebars — anything shared across templates lives here.

See Templates §3 for the core/template-part contract.

Patterns

Reusable block snippets. Two flavours:

  • Synced — pattern lives in the pattern store; every reference updates when the pattern changes.
  • Unsynced — the snippet is dropped inline at insert time; later edits to the pattern don't propagate.

See Patterns.

Styles

The theme-wide style record (theme.json-shaped). Edits here change typography, color palette, spacing scale, layout defaults, and per-block style overrides for every page on the site.

See Global styles.

The Typography controls here (and in every per-block/per-element style panel) carry a "Manage fonts…" entry point that opens the Font Library — the modal for browsing Google Fonts and Bunny Fonts, uploading custom faces, and installing self-hosted fonts. Installed families then appear in every font-family picker.

See Fonts.

Menus and menu items. Each menu can be assigned to one or more theme-declared menu locations; the core/navigation block resolves location → menu at render time.

See Navigation.


4. Canvas and entity loading

The canvas is the same iframe-wrapped block editor as the post editor. When you select an entity in the navigator:

  1. The site-editor shell dispatches core/editor to load the entity by id.
  2. Gutenberg's core-data shim fetches the entity from the REST surface (/visual-editor/api/templates/{slug}, /template-parts/{slug}, etc.).
  3. The canvas renders the entity's block tree.
  4. Edits autosave back through the same REST endpoint.

Switching entities saves pending edits to the previous entity first (blocking on the network round-trip), then loads the new entity.


5. URL routing

The site editor is a single Laravel route (GET /visual-editor/site/{path?}) that hands off to React routing client-side. Direct links to a specific entity work:

/visual-editor/site/templates/single
/visual-editor/site/template-parts/header
/visual-editor/site/patterns/hero
/visual-editor/site/styles
/visual-editor/site/navigation/primary

These are shareable — bookmark or send to a colleague to jump straight into editing that entity.

The trailing path segment is an opaque entity identifier — the router hands it to the entity fetch unchanged — but every link the site editor produces for itself addresses the entity by database id. Callers outside the SPA usually do not have one: the post editor's composed view, for instance, knows only the slug of the template it composed against. For those, the site editor accepts a query-string deep link on the mount URL:

/visual-editor/site?entity=template&slug=single

On mount the SPA parses the query string, resolves the slug through the templates list endpoint, and navigates to that template's editor view. That landing replaces the current history entry with the canonical path route (/visual-editor/site/templates/12), so the query-string form is an entry point rather than a URL the user is left holding — and Back does not return to it. (A pushed entry would: the popstate handler re-reads the pathname only, so going back to the query-string URL would show the index while the address bar still claimed the deep link.)

Parameter Required Notes
entity yes Only template is supported today. Unknown values are ignored.
slug yes Entity slug. Blank or whitespace-only values are ignored.
entity_id no Pre-resolved id. When supplied the slug lookup is skipped. Reserved for entity kinds that have no slug.

Behaviour worth relying on:

  • Additive. Mounting with no query string behaves exactly as it did before — no lookup is issued and the SPA lands on its default section.
  • Unknown entity is a no-op, not an error. A link written against a future package version lands on the default section rather than throwing.
  • Unresolvable slug lands on the Templates index with a toast reading The template "{slug}" was not found. A failed lookup does the same.
  • Your own navigation wins. If you browse elsewhere in the SPA while the slug lookup is still in flight, the landing is abandoned rather than yanking you to the deep-link target when it resolves. Navigating away and back counts as navigating: the check is the deep-link query still being present, which every in-SPA navigation clears and none restores.
  • The access gate is unchanged. SiteEditorAccessGate runs server-side, before the SPA mounts, so a denied request still renders the deny-by-default page and never reaches this parsing.

Build links with buildTemplateDeepLink() from site-editor/deep-link.ts rather than assembling the query string by hand, so both sides of the contract move together. The first consumer is the post editor's composed-view ribbon, whose Edit template ↗ button opens one of these links in a new tab.

buildTemplateDeepLink() defaults to the package's own mount path (/visual-editor/site) and takes the route base as an optional second argument. The composed-view ribbon currently relies on that default: the post editor has no way to discover a data-route-base the host may have overridden on the site-editor mount element. A host that mounts the site editor at a non-default path will get a ribbon CTA pointing at the default path until that value is threaded through to the post editor.


6. Preview

The topbar Preview button opens the entity's front-end rendering in a new tab. For templates, this is the slug rendered against a representative record (e.g. single → most recent post). For template parts and patterns, it's an isolated preview page. For global styles, it's the home page.

The preview URL is computed by apGetSiteEditorPreviewUrl() and can be overridden per-section by binding your own preview resolver in config('artisanpack.visual-editor.site-editor.previews').


7. REST API surface

Section Endpoints
Templates GET/POST /templates, GET/PUT/DELETE /templates/{slug}
Template parts GET/POST /template-parts, GET/PUT/DELETE /template-parts/{slug}
Patterns GET/POST /patterns, GET/PUT/DELETE /patterns/{slug}
Global styles GET /global-styles/lookup, GET /global-styles/base, GET /global-styles/css, GET/PUT /global-styles/{id}
Navigation GET/POST /menus, GET/PUT/DELETE /menus/{id}, GET/POST/PUT/DELETE /menu-items, GET /menu-locations
Entity search GET /search — backs the link-control picker across all entity types
Fonts GET /fonts, GET /fonts/sources, GET /fonts/sources/{provider}/catalog, GET /fonts/sources/{provider}/preview/{slug}, GET /fonts/sources/{provider}/preview/{slug}/{weight}/{style}, POST /fonts, POST /fonts/upload, POST /fonts/bulk-uninstall, DELETE /fonts/{font} — the Font Library (reads open + read_only; writes gated by manage_fonts)

All under the /visual-editor/api/ prefix, all behind the API middleware stack.


8. Static configuration

Some site-editor entities can be declared in config rather than the database — useful for templates and patterns that ship with the host app's theme:

// config/artisanpack/visual-editor.php
'site-editor' => [
    'templates' => [
        'single' => ['title' => 'Single Post', 'content' => '...'],
    ],
    'template-parts' => [
        'header' => ['title' => 'Header', 'content' => '...'],
    ],
    'patterns' => [
        'hero' => ['title' => 'Hero', 'content' => '...'],
    ],
    'navigation' => [
        'primary' => ['title' => 'Primary Menu'],
    ],
],

Static configs are merged with DB-stored user overrides via the fallback chain — user records win on the same slug. See Templates §4.


See also

  • Templates · Global styles · Navigation · Patterns
  • Access Gate — site-editor access gate contract
  • Composed view — the post-editor surface that deep-links here
  • Content model — HasBlockContent and authorization
  • Fonts — the Font Library launched from the Typography controls
  • Renderers — render saved entities on the public site
  • Photo Grid — container-level Photo Grid setting (group / columns / grid). theme.json defaults live under settings.artisanpack.photoGrid and ride the same global-styles plumbing.