OctoberCMS 4.3 - Feature Release

Release Note 42

Version 4.3 of OctoberCMS ships a broad set of features across the frontend, the backend, and developer tooling. The document form design brings the Tailor editor layout to every plugin, partials as components introduces attribute-bag patterns to CMS templates, view transitions enable native animated page navigations, and the release adds an official debugbar, a new theme translation editor, and October Boost for AI-assisted development.

Table of Contents

How to Upgrade to v4.3

There are two ways to upgrade, by clicking the Check for Updates button in the admin panel, or via console commands. For command line interface, please use the following commands:

composer update
php artisan october:migrate
php artisan october:util set build

In the event that you find some incompatibilities with your plugins due to this release, lock your composer file to the previous version (v4.2) by modifying your composer file below and then run composer update.

"require": {
    "october/all": "4.2.*",
    "october/rain": "4.2.*"
}

Document Form Design

The FormController behavior gains a new document display mode that renders a full-height document layout with a Vue-powered toolbar, outside fields header, and secondary tabs in a popover. This layout was previously implemented in the Tailor entries controller as a bespoke _edit.php view. It is now a reusable form design that any backend controller can adopt.

Enabling:

Set design.displayMode in your config_form.yaml:

# config_form.yaml
name: Blog Post
form: $/acme/blog/models/post/fields.yaml
modelClass: Acme\Blog\Models\Post

design:
    displayMode: document
    secondaryLabel: Settings   # optional - popover button label, defaults to "Options"

Then call formRenderDesign() from your create.php / update.php views:

<?= $this->formRenderDesign() ?>

This single call renders the entire form including outside fields header, primary tabs, secondary fields popover, Vue toolbar, and any drawer slot content.

Default toolbar: Save, Save & Close, and Delete buttons. The save buttons disable automatically while a request is in flight and re-enable when the change monitor reports unsaved changes.

Adaptive toolbar wiring: Any form widget with span: adaptive automatically participates in the document toolbar. The FieldProcessor sets externalToolbarBus: 'document' on adaptive fields during form configuration, so widget JS (rich editor, markdown editor, file upload, media finder, repeater) injects its own toolbar buttons into the document toolbar without any manual configuration.

fields:
    content:
        type: richeditor
        span: adaptive   # toolbar buttons appear in document toolbar automatically

Secondary fields popover: When a form has secondary tabs, the document layout renders a button in the header that opens the secondary fields in a popover. The button label is read from design.secondaryLabel (defaulting to __("Options")).

Customizing the toolbar: Document toolbars use the same _form_buttons.php override mechanism as the other form designs. Drop a _form_buttons.php partial into your controller's views directory and declare your buttons with the familiar Ui::ajaxButton and Ui::button factories - the framework adapts them into the Vue toolbar automatically. No custom JS, no VueDocumentForm extension required.

<?php // plugins/acme/blog/controllers/posts/_form_buttons.php ?>
<?= Ui::ajaxButton(
    label: __("Save"),
    handler: 'onSave',
    icon: 'icon-save-cloud',
    primary: true,
    hotkey: ['ctrl+s', 'cmd+s'],
    dataRequestData: "redirect: 0"
) ?>

<?= Ui::ajaxButton(
    label: __("Save & Close"),
    handler: 'onSave',
    icon: 'icon-keyboard-return',
    hotkey: ['ctrl+enter', 'cmd+enter'],
    dataBrowserRedirectBack: true,
    dataRequestData: "close: 1"
) ?>

<?php if (!empty($pageUrl)): ?>
    <?= Ui::button(
        label: __("Preview"),
        href: Url::to($pageUrl),
        icon: 'icon-crosshairs',
        target: '_blank'
    ) ?>
<?php endif ?>

<?= Ui::ajaxButton(
    label: __("Delete"),
    handler: 'onDelete',
    icon: 'icon-delete',
    hotkey: ['shift+option+d'],
    class: 'pull-right',
    dataRequestConfirm: __("Delete this post?")
) ?>

A <div class="toolbar-divider"></div> between buttons becomes a separator in the toolbar. Buttons with class: 'pull-right' are pinned to the right edge. Plain Ui::button calls with an href (such as a Preview link) work as link-type toolbar elements that open in a new tab when activated by hotkey.

For toolbars that need behavior beyond static buttons - bespoke Vue components, drawers, dynamic dropdowns - you can still provide a _form_mode_document.php partial in your controller's views directory and ship your own Vue control. Tailor's entries controller uses this escape hatch (see below).

Tailor adoption: The Tailor entries controller now uses this shared infrastructure. The previous _edit.php view has been removed and replaced with a _form_mode_document.php override that adds entry-specific header controls (publish button, draft notes) and the vue-entry-document control variant. Behavior in the Tailor editor is unchanged.

Partials as Components

CMS partials now support a {% props %} tag that separates parameters into props (template data) and attributes (HTML pass-through). This brings the same component pattern used by Laravel Blade Components and the backend Ui:: system to CMS theme templates.

Declaring props:

The {% props %} tag is placed at the top of a partial .htm file using Twig hash syntax. Keys become template variables with defaults, and everything else flows into an attributes variable:

{% props {title: null, icon: null, size: 'md'} %}

The attributes variable:

When {% props %} is present, the template receives an attributes variable - an instance of Laravel's ComponentAttributeBag. It supports smart class merging where the caller's classes are appended to the partial's defaults:

{# partials/ui/alert.htm #}
{% props {type: 'info', dismissible: false} %}

<div {{ attributes.merge({class: 'alert alert-' ~ type, role: 'alert'}) }}>
    <div class="alert-content">{{ body }}</div>

    {% if dismissible %}
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
    {% endif %}
</div>
{# Usage in a page #}
{% partial "ui/alert"
    type="warning"
    dismissible=true
    class="mb-3"
body %}
    <strong>Warning!</strong> Something needs your attention.
{% endpartial %}

Rendered output:

<div class="alert alert-warning mb-3" role="alert">
    <div class="alert-content">
        <strong>Warning!</strong> Something needs your attention.
    </div>
    <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>

The attribute bag supports all standard methods:

Method Description
attributes.merge({class: 'btn'}) Merge defaults - smart-appends class and style
attributes.only('class', 'id') Keep only specific attributes
attributes.except('class') Exclude specific attributes
attributes.has('id') Check if an attribute exists
attributes.get('id') Get a single attribute value

An empty {% props {} %} means all parameters go to attributes - useful for wrapper elements.

Composable partials:

The existing {% put %} / {% placeholder %} system works alongside {% props %} for partials that need multiple content sections:

{# Caller #}
{% partial "card" class="shadow" body %}
    {% put header %}
        <h2>Card Title</h2>
    {% endput %}
    <p>Body content</p>
{% endpartial %}

{# card.htm #}
{% props {} %}
<div {{ attributes.merge({class: 'card'}) }}>
    <div class="card-header">
        {% placeholder header %}
    </div>
    <div class="card-body">{{ body }}</div>
</div>

Backwards compatibility: Partials without {% props %} work exactly as before - all parameters remain plain Twig variables. Existing body, only, {% put %}, and {% placeholder %} behavior is unchanged.

File Attachment Import & Export

The ImportModel and ExportModel base classes now support file attachment relations (attachOne and attachMany) natively. File handling is implemented via two new traits - EncodesZip and DecodesZip - following the same pattern as the existing EncodesCsv/EncodesJson and DecodesCsv/DecodesJson traits.

Exporting: When an export includes file attachment columns, the output is automatically packaged as a ZIP archive containing the data file (data.json or data.csv) and a files/ directory with the attachment files using human-readable filenames. Filename collisions are handled with _2, _3 suffixes. If no file attachments are included, the export behaves as before (plain JSON or CSV).

Use encodeFileRelation($model, $attr) in your export model to encode a file relation as relative paths and register the files for ZIP packaging:

public function exportData($columns, $sessionKey = null)
{
    $records = Product::with(['images'])->get();
    $result = [];

    foreach ($records as $record) {
        $item = $record->toArray();
        $item['images'] = $this->encodeFileRelation($record, 'images');
        $result[] = $item;
    }

    return $result;
}

Importing: The import file upload field now accepts .zip files. When a ZIP is uploaded, it is extracted automatically and the data file inside is located (data.json, data.csv, or any JSON/CSV file in the root). The file_format is auto-detected from the data file extension.

Use decodeFileRelation($model, $attr, $value, $sessionKey) in your import model to resolve file paths and create attachments:

public function importData($results, $sessionKey = null)
{
    foreach ($results as $row => $data) {
        $record = MyModel::create($data);

        if ($imagesValue = array_get($data, 'images')) {
            $this->decodeFileRelation($record, 'images', $imagesValue, $sessionKey);
        }
    }
}

File path resolution follows a priority chain:

  1. ZIP paths - Values starting with files/ resolve from the extracted ZIP directory
  2. Source prefix - If setSourcePrefix() has been called (theme seed context), paths resolve relative to that prefix
  3. Media library - Paths resolve from Storage::disk('media')

Theme seeding: The setSourcePrefix() method is already called by the theme seed system, so plugin import models that extend ImportModel can resolve file paths from the theme directory or media library without any additional setup. See the theme seeding documentation for examples.

Tailor entries: The RecordImport and RecordExport models handle file attachments automatically - no additional code is needed. File attachment fields defined in blueprints are encoded/decoded during import and export.

View Transitions

The AJAX framework now supports the View Transitions API, enabling smooth animated transitions between turbo-routed page navigations. When enabled, the browser captures the old and new page states and cross-fades between them natively - no JavaScript animation libraries required.

Enabling: Add the turbo-view-transition meta tag alongside the existing turbo router tag:

<head>
    <meta name="turbo-visit-control" content="enable" />
    <meta name="turbo-view-transition" content="same-origin" />
</head>

Browsers without View Transitions API support fall back to the standard instant page swap with no errors.

Customization: Transitions are controlled entirely through CSS using the ::view-transition-old() and ::view-transition-new() pseudo-elements. You can adjust duration, easing, or replace the default cross-fade with slides, scales, or any CSS animation.

::view-transition-old(root),
::view-transition-new(root) {
    animation-duration: 0.3s;
    animation-timing-function: ease-in-out;
}

Directional animations: The turbo router sets data-turbo-visit-direction on the <html> element during navigation (forward, back, or none), allowing directional slide animations that respond to navigation direction.

html[data-turbo-visit-direction="forward"]::view-transition-old(root) {
    animation: slide-out-left 0.3s ease-in-out;
}
html[data-turbo-visit-direction="forward"]::view-transition-new(root) {
    animation: slide-in-right 0.3s ease-in-out;
}

See the View Transitions documentation for CSS examples and usage details.

Per-Site Plugin Management

Plugins can now be enabled or disabled per site or per site group. This is useful when running multiple websites from a single installation where certain plugins should only be active on specific sites - for example, a shop plugin that should only run on shop.domain.com.

Enabling: Set the feature flag in config/multisite.php:

'features' => [
    'system_plugin_sites' => true,       // Per individual site
    // or
    'system_plugin_site_groups' => true,  // Per site group
]

Clear the application cache after changing the configuration.

Usage: Navigate to Settings → Updates & Plugins → Manage Plugins. The existing enable/disable toggle becomes site-aware - it applies to whichever site is currently selected in the site picker. Switch sites to configure each one independently.

When a plugin is disabled for a site:

  • Its backend navigation items and settings are hidden
  • Its controllers return 404
  • The site picker dropdown on plugin pages only shows sites where the plugin is enabled

The two feature flags are mutually exclusive. Use system_plugin_sites for per-site control, or system_plugin_site_groups to manage plugins at the site group level where all sites in a group share the same plugin configuration.

AI Development with Boost

The october/boost Composer package extends Laravel Boost with October CMS-specific guidelines, skills, and MCP tools for AI-assisted development. It works with Claude Code, Cursor, GitHub Copilot, Codex, Gemini CLI, and Junie.

Installation:

composer require october/boost --dev
php artisan boost:install

Select october/boost when prompted during installation. The installer configures your AI agent automatically - writing guideline files, registering the MCP server, and copying skills.

Guidelines are compiled into the agent's configuration file (e.g. CLAUDE.md) and teach the AI to follow October CMS conventions: array-based model relationships, the Validation trait, YAML-configured backend behaviors, Twig themes, and the AJAX framework. The AI will not suggest Livewire, Inertia, Blade components, or other patterns that don't apply to October CMS.

Skills are on-demand knowledge modules that activate when the AI recognizes a specific domain:

Skill Activation
Plugin Development Plugin.php, migrations, version.yaml, scaffolding
Tailor Development Blueprints, content fields, entry records
Backend Controllers FormController, ListController, RelationController, YAML configs
Theme Development Pages, layouts, partials, Twig, CMS components
AJAX Framework data-request attributes, jax API, handlers, partial updates
Model Development Relationships, validation, traits, model events

MCP tools give the AI real-time access to the application:

Tool Purpose
SearchOctoberDocs Search official documentation for Laravel, October CMS, Larajax, and Meloncart from their GitHub-hosted repos
GetBlueprints List and inspect Tailor blueprint definitions and fields
GetPluginRegistration List installed plugins with components, permissions, and navigation
GetThemeStructure Inspect the active theme's pages, layouts, partials, and content files

The SearchOctoberDocs tool replaces Laravel Boost's default documentation search with a version that searches the correct doc repositories - Laravel (version-matched), October CMS, Larajax, and Meloncart - using keyword matching with local caching. Results can be filtered by package.

See the AI Development with Boost documentation for usage details.

Debugbar

The october/debugbar Composer package integrates Laravel Debugbar (fruitcake) with October CMS, replacing the legacy rainlab/debugbar-plugin.

Installation:

composer require october/debugbar --dev

The debugbar is enabled automatically when APP_DEBUG=true. Override with the DEBUGBAR_ENABLED environment variable.

October CMS collectors: In addition to the standard Laravel Debugbar collectors (queries, timeline, session, request, mail, etc.), the following October-specific collectors are included:

Collector Description
Backend Backend controller, action, parameters, and AJAX handler with file location
CMS CMS page, URL, AJAX handler, and page properties with file location
Components All components from the page and layout with their class and properties

AJAX support: AJAX requests are captured automatically and displayed in the toolbar dropdown. The package includes an InterpretsAjaxExceptions middleware that catches AJAX exceptions and embeds debugbar data in response headers, controlled by the capture_ajax setting in config/debugbar.php.

Configuration: Publish the configuration file with php artisan vendor:publish --provider="October\Debugbar\ServiceProvider" --tag=config, or create config/debugbar.php manually. The configuration extends fruitcake's default config with sensible defaults for October CMS (e.g. views and events collectors disabled by default).

See the Debugbar documentation for full usage details.

Theme Translation Editor

The CMS editor now includes a Languages section for managing theme translation files directly from the backend. Theme translations live in the lang/ directory as JSON files (e.g. en.json, fr.json) and are used by the Twig __() and trans() functions to localize frontend content.

Spreadsheet editor: Language files open in a key/value spreadsheet rather than a raw JSON editor. Each row represents a translation key and its value, making it easy to see and edit all translations at a glance. Rows can be added and removed via toolbar buttons.

Search: A toggleable search bar filters the spreadsheet in real time, highlighting matching cells. Press Enter to cycle through results.

New file pre-population: When creating a new language file, the editor automatically loads the translation keys from the theme's default language file, so translators start with the complete set of keys ready to fill in.

Toolbar: Save, rename, delete, insert/delete rows, search, and template info - all accessible from the document toolbar with keyboard shortcuts.

The Languages section appears in the CMS editor sidebar alongside Pages, Layouts, Partials, Content, and Assets. Access is controlled by the editor.cms_langs permission.

Translatable Sync for Repeaters

Tailor repeater fields now support a translatable: sync mode that keeps the repeater structure (items, order) synced across all sites while allowing individual sub-fields to have different values per site. This solves a common need in multilingual content - for example, a FAQ where the questions and answers are translated but the list of items stays consistent across languages.

Blueprint configuration:

handle: Blog\Post
type: stream
multisite: sync

fields:
    attachments:
        type: repeater
        translatable: sync
        form:
            fields:
                file_title:
                    type: text
                    translatable: true    # Different value per site
                file_desc:
                    type: textarea        # Shared across all sites
                file:
                    type: fileupload
                    mode: file
                    maxFiles: 1           # Shared file across all sites
                localized_image:
                    type: fileupload
                    mode: image
                    maxFiles: 1
                    translatable: true    # Different file per site

How it works:

The translatable property on repeater fields now accepts three values:

Value Behavior
true (default) Each site has fully independent repeater items
false All sites share the exact same repeater items (shared rows)
sync Items are replicated per site with structure synced - adding, removing, or reordering items on one site propagates to all others

When translatable: sync is set, sub-fields control their own translatability:

  • Scalar sub-fields with translatable: true have independent values per site (e.g. translated text). Sub-fields without translatable: true are shared and their values propagate across sites.
  • Relation sub-fields (file uploads, entries) with translatable: true have independent attachments per site. Non-translatable relation sub-fields share attachments across all site variants automatically.

Translatable sub-fields display a globe icon in the backend form, consistent with how translatable fields work on the parent entry.

Propagation: Structural changes propagate when the parent record is saved, using the same propagation mechanism as other multisite-synced content. The "leader" site (whichever saves) pushes its structure to all other sites. Non-translatable scalar values are copied; translatable values are left untouched on each site.

Notable Minor Changes

Twig translation functions

Theme translations now use the __() and trans() Twig functions instead of the |_ and |trans filters, bringing Twig syntax closer to the familiar PHP __() and trans() helpers.

{{ __("Hello World") }}

The function syntax takes an English string as the key and resolves translations from JSON files in the theme's lang/ directory (e.g. fr.json). The trans_choice() function is also available for pluralization. The old filter syntax ({{ "text"|_ }}, {{ "text"|trans }}, {{ "text"|transchoice }}) is deprecated.

RainLab.Blog plugin refresh

The official rainlab/blog plugin has been updated to align with v4.3 patterns: posts and categories now use the new displayMode: document form design, controller view files have been migrated from .htm to .php, toolbar markup uses the Ui:: component factories, and the blog settings model has been renamed to the singular Setting convention.

The post edit screen ships a custom _form_buttons.php partial that adds a Preview button to the document toolbar - a working reference for plugin authors adopting the new override mechanism.


This is the end of the document, you may read the announcement blog post or visit the changelog for more information.

comments powered by Disqus