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:
- Confirms cms-framework is installed and migrations have run.
- Confirms the current user has the
visual_editor.site.editpermission (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.
Navigation
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:
- The site-editor shell dispatches
core/editorto load the entity by id. - Gutenberg's core-data shim fetches the entity from the REST surface
(
/visual-editor/api/templates/{slug},/template-parts/{slug}, etc.). - The canvas renders the entity's block tree.
- 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.
Deep links by slug
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.
SiteEditorAccessGateruns 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 —
HasBlockContentand 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.photoGridand ride the same global-styles plumbing.

