Blueprints and blocks
Content in Evolve is a tree of blocks on a page (or on any "blockable" record). A blueprint describes a block type: its fields, label, group and icon. A block is one instance with its field values per locale.
Blueprint definitions
A blueprint is a PHP class implementing Racerfish\Evolve\Contracts\BlueprintConfig with one static method, config(): Blueprint:
class Hero implements BlueprintConfig
{
public static function config(): Blueprint
{
return Blueprint::make('cms.hero')
->label('Hero')
->icon('heading')
->allowMedia(accept: ['image', 'video'])
->fields([
Field::string('title', 'Titel'),
Field::richtext('content', 'Inhalt'),
Field::fieldset('buttons', 'Buttons', Button::class)->repeatable(),
]);
}
}Blueprint::make() (core/src/DTO/Blueprint.php) takes the component ID (namespace.name) and accepts label, group, fields, selectable (whether the block picker offers it; the email blocks pass false so they only exist inside campaigns), icon and a media setting either as named arguments or through the fluent setters (->label(), ->group(), ->icon(), ->allowMedia(), ->fields(), ->hidden()). Field (core/src/DTO/Field.php) builds fields through static constructors named after the type (string, text, textarea, richtext, html, number, url, address, select, radioGroup, resource with a model, repeatables and more) with label, default, width and visibility options.
Discovery. Evolve::getBlueprints() scans evolve.blueprints.paths, a map from namespace to one or more directories (several paths per namespace since 2.5). The default is App\Blueprints → app/Blueprints; modules extend it (the email module adds Racerfish\Email\Blueprints) and the workbench points it at every module's app-publish/Blueprints. Projects publish module blueprints into app/Blueprints with the <module>.blueprints tags, which is why a published blueprint can be edited per project.
Legacy YAML. With evolve.blueprints.load-legacy-defintions on (default), config/components/*.yaml files are read too (name, component, icon, block, fields, titlePreview, allowNestedBlocks). The module config/components directories still ship YAML for their blocks, published with <module>.components.
Blueprint::all(), allGrouped() and for('cms.cards') are the lookups the block editor and the MCP available-block-types-resource use. evolve:blueprint scaffolds a class and tells you where the view goes; evolve:component scaffolds a component with fields; evolve:typings emits TypeScript typings.
The Block model
core/src/Models/Block.php:
propsholds the field values as JSON keyed by locale;getFieldsAttribute()hydrates them through the blueprint, resolvingresourcereferences to models (a dangling reference becomesnullsince 2.4.1) and media IDs toMedia.blockableis a morph to the owner (page, post, campaign); children are blocks whose blockable is a block, so containers (cms.cards,cms.tiles,cms.logoCloud) nest.- Editor link:
getSearchableAdminUrl()asks the blockable for its editor URL throughContracts\BlockContainer::blockEditUrl().Blockimplements it by asking its own blockable, so a nested block resolves to the root carrier. Page, Section and every module model with a block editor implement the contract; the core imports no module model for this since 2.21. Before that aswitchover six module classes decided, and it missed Shopify collections. Without the contract the link falls back toevolve.page.show. - Publishing:
is_published,publish_at,unpublish_at;scopePublished()andis_currently_publishedapply the schedule. The MCP read tools and the frontend use the published scope; the panel preview may include unpublished blocks. - Clones: a block can reference a global section's block (
is_clone), which is how global sections reuse content; saving a section forgets the cached rendering of every clone. - Regions:
getContentRegion()resolves the region whose overrides apply, see Regions. anchorgives every block anidon the site;exportContentSnippet()andimportContentSnippet()carry a locale's texts out and back in for translation.
Rendering
The theme renders a page's blocks through the block view for the type: theme/views/components/blocks/<namespace>/<name>.blade.php (cms.cards → blocks/cms/cards.blade.php). Module blocks render from the module's own views/blocks when the project has no copy (since 2.6); a block with no view at all leaves an <!-- evolve:missing-block --> comment in production.
Views receive the block with its hydrated fields. When a block needs data beyond its fields, a block composer supplies it: Evolve::registerBlockComposer('blog.posts', BlogPostsComposer::class) maps a component to a class that prepares the view data (the posts for the list, with pagination). Core ships BlockComposers\GlobalsComposer; blog, company and others register theirs in bootModule().
Livewire blocks (the posts list, the store finder, the chatbot) are Livewire components the module registers; the theme mounts them when Evolve::hasLivewireBlock($name) finds the class.
The block editor
The panel's block editor is a set of Livewire components under core/src/Http/Livewire (BlockEditor, the field components, BlockContentGenerator for the AI page generator) with views under core/views/block. Field types map to field views under core/views/components/form. The editor keys its field container by locale so values never bleed across languages (2.4.2), and takes placeholders and the "Default" option from the page region's fallback locale (2.12).
Where to look
| Question | Path |
|---|---|
| What fields does a core block have | core/app-publish/Blueprints/*.php |
| How a field type renders in the panel | core/views/components/form/*.blade.php |
| How props become fields | Block::getFieldsAttribute() and hydrateProps() |
| Which blueprints an install knows | Devtools > Block Blueprints, or Blueprint::all() |
| How a block appears on the site | theme/theme/views/components/blocks/ in the starter theme |