Templates are full-page block trees that drive front-end rendering. The
package follows WordPress's wp_template model: templates are looked up
by slug against a hierarchy, the resolver walks the chain from
specific to generic, and each template can include reusable
core/template-part blocks.
This page covers the template hierarchy, the resolver, template parts, and how the renderer turns it all into HTML.
1. Template hierarchy
A request for a single post (slug hello-world, post type post)
resolves templates in this order, stopping at the first match:
single-post-hello-world (slug-specific)
single-post (post-type-specific)
single (single-record default)
index (final fallback)
The full hierarchy (mirrors WordPress core):
| Context | Slug order |
|---|---|
| Single post | single-{post_type}-{slug} → single-{post_type} → single → index |
| Page | page-{slug} → page-{id} → page → singular → index |
| Category archive | category-{slug} → category-{id} → category → archive → index |
| Tag archive | tag-{slug} → tag-{id} → tag → archive → index |
| Taxonomy archive | taxonomy-{taxonomy}-{term} → taxonomy-{taxonomy} → taxonomy → archive → index |
| Author archive | author-{nicename} → author-{id} → author → archive → index |
| Date archive | date → archive → index |
| Search results | search → index |
| 404 | 404 → index |
| Front page | front-page → home → index |
The host app's route handler picks the context and asks
TemplateResolver::bySlug() to walk the chain.
2. The TemplateResolver
ArtisanPackUI\VisualEditor\SiteEditor\Resolution\TemplateResolver
$resolver = app(\ArtisanPackUI\VisualEditor\SiteEditor\Resolution\TemplateResolver::class);
$template = $resolver->bySlug('single-post');
Returns a ResolvedTemplate value object:
ResolvedTemplate {
public string $slug;
public string $theme;
public string $title;
public string $status; // 'auto-draft' | 'publish'
public string $source; // 'theme' | 'user' | 'custom'
public array $content; // ['raw' => '...', 'blocks' => [...]]
public bool $has_theme_file;
public bool $is_custom;
public ?int $wp_id; // user override id, if present
}
bySlug() returns null if nothing matches. The host renderer is
expected to walk the hierarchy and pick the first non-null result:
foreach (['single-post-hello-world', 'single-post', 'single', 'index'] as $slug) {
$template = $resolver->bySlug($slug);
if ($template !== null) {
break;
}
}
The Blade renderer's <x-ve-template :slug="$slug" /> does this internally.
3. Template parts
Template parts are reusable chunks (header, footer, sidebar) included
into templates via the core/template-part block.
core/template-part block
Attributes:
| Attribute | Type | Purpose |
|---|---|---|
slug |
string | Template part slug to include (e.g. header, footer). |
theme |
string (optional) | Theme slug; defaults to the active theme. |
area |
string (optional) | One of header, footer, sidebar, uncategorized. Drives navigator grouping; doesn't affect rendering. |
The block stores no inner blocks — its content is whatever the linked template part stores at render time. Editing the linked template part updates every template that includes it.
Block name in saved templates. Theme template files reference parts
as core/template-part. When the site editor loads a template it forks
core block names to their artisanpack/* equivalents, so a template
saved from the site editor stores the reference as
artisanpack/template-part. Both names are treated as template-part
references everywhere (BlockShape::TEMPLATE_PART_NAMES).
Server-side rendering
TemplatePartResolver::bySlug($slug, $theme = null) returns a
ResolvedTemplatePart value object with the same shape as
ResolvedTemplate. The Blade renderer's <x-ve-blocks> component
(through TemplatePartInliner) recognizes both core/template-part and
artisanpack/template-part nodes and inlines the resolved part's
content. The React and Vue inliners do the same. The resolved block keeps
the name it was saved with, and any innerBlocks snapshot saved with the
reference is replaced by the live part, so edits to the part always show.
Since v1.12.1 (#822). Earlier versions only expanded
core/template-part, so parts in a template saved from the site editor
rendered as empty wrappers on the front end.
If the linked part is missing, the renderer emits an empty wrapper rather than an error — the canvas already warns the author during editing.
4. Fallback chain
For each slug the resolver checks three sources in order, returning the first match:
- User record — a row in
cms-framework's templates table (source = 'user'). Always wins. - Static config — entries in
config('artisanpack.visual-editor.site-editor.templates')keyed by slug (source = 'theme'). - Theme file — packaged template files shipped by themes
(
source = 'theme',has_theme_file = true).
Authors editing a theme-provided template in the site editor create a user override — the user record then shadows the theme version. The "reset to theme default" button in the editor deletes the user record, restoring the theme version.
5. REST API
| Method | Path | Purpose |
|---|---|---|
GET |
/visual-editor/api/templates |
List all templates (theme + user). |
POST |
/visual-editor/api/templates |
Create a new user template. |
GET |
/visual-editor/api/templates/{slug} |
Fetch a single template. |
PUT |
/visual-editor/api/templates/{slug} |
Update — creates a user record if one doesn't exist. |
DELETE |
/visual-editor/api/templates/{slug} |
Delete the user record (theme version remains). |
GET/POST/GET/PUT/DELETE |
/visual-editor/api/template-parts[/{slug}] |
Same surface for template parts. |
All endpoints behind the API middleware stack. Slug regex allows path-style
segments (single/post/hello-world).
6. Custom templates
Custom templates (not part of the hierarchy) are templates an author created from scratch in the site editor with a free-form name. They don't auto-resolve for any route — the host app opts pages into them explicitly:
// In your page model
public function getTemplate(): string
{
return $this->custom_template_slug ?? 'singular';
}
<x-ve-template :slug="$page->getTemplate()" />
The site editor's New Template button creates these; they're persisted
with is_custom = true.
7. Editing in the site editor
The Templates section of the site-editor navigator lists every template the resolver knows about, grouped by source:
- Theme templates — defaults shipped by the active theme.
- Custom templates — created by authors.
- User-modified — theme templates with user overrides.
Clicking a template loads it into the canvas. The save endpoint always writes a user record; the theme version remains available via "reset to theme default".
Previewing real content
Since v1.15.3 (#993), templates that render a single entry preview their Post Title, Post Excerpt and Post Content blocks with a real entry instead of placeholders. The template slug picks the post type:
| Template slug | Previews |
|---|---|
single, singular, single-post |
The latest post |
single-post-{slug} |
The post with that slug, else the latest post |
single-{type} |
The latest entry of the custom content type {type} |
single-{type}-{slug} |
That entry of {type}, when {type} is a registered content type |
page |
The latest page |
page-{slug} |
The page with that slug, else the latest page |
A Preview content picker switches to another
entry. The previewed values are read-only; edit the entry itself in the
post editor. Other templates (index, archive, home, 404, custom
templates) and types with no published entry keep the placeholders. A
page-{slug} template finds its page only among the 20 newest pages.
8. Where templates live
- Theme files: loaded by the active theme; no migration required.
- Static config:
config('artisanpack.visual-editor.site-editor.templates')— version-controlled with the app. - User overrides:
cms-framework's templates table — written by the site editor.
Picking the right home for each template:
- Theme file or static config for templates that ship with the app, evolve via deploys, and should reset cleanly.
- User record for one-off edits authors make at runtime.
See also
- Site editor — the surface that edits templates
- Renderers — render templates on the public site
- Patterns —
core/blockreferences inside templates - Global styles — theme.json driving template appearance

