modules/cms/README.md
The CMS module is October CMS's frontend template engine. It renders pages using file-based themes with Twig templating, provides a component system for reusable page logic, handles routing from URLs to pages, and integrates with the Editor module for visual template editing. Unlike frameworks that couple you to a specific frontend stack, October CMS themes are pure HTML and Twig -- no build step required, no JavaScript framework mandated. Developers keep full control of their markup while getting the productivity of a component system and a well-defined page lifecycle.
The CMS processes incoming requests through this pipeline:
All templates are file-based and stored in themes under the themes/ directory.
| Service | Class | Description |
|---|---|---|
cms.helper | Cms\Helpers\Cms | URL generation and CMS utilities |
cms.components | Cms\Classes\ComponentManager | Component registry |
cms.snippets | Cms\Classes\SnippetManager | Snippet management |
cms.themes | Cms\Classes\ThemeManager | Theme operations |
themes/mytheme/
├── theme.yaml # Theme configuration (name, description, author)
├── pages/ # Routable pages
├── layouts/ # Page structure templates
├── partials/ # Reusable template fragments
├── content/ # Static content files (HTML, Markdown, text)
├── assets/ # CSS, JS, images
└── lang/ # Translation strings
CMS templates use a multi-section file format that keeps configuration, logic, and markup together in a single file -- no separate route definitions, no controller classes, no Blade directives to learn:
title = "Blog Post"
url = "/blog/:slug"
layout = "default"
[blogPost]
slug = "{{ :slug }}"
==
<?php
function onStart()
{
$this['activeMenu'] = 'blog';
}
?>
==
<h1>{{ blogPost.title }}</h1>
{{ blogPost.content_html|raw }}
The three sections separated by == are:
Pages are routable templates with URL patterns. URLs support parameters:
/blog/:slug - required parameter/blog/:slug? - optional parameter/blog/:post_id|^[0-9]+$ - parameter with regex validationAccess parameters in code with $this->param('slug') or in Twig with {{ :slug }}.
Layouts wrap pages and define the common structure (HTML head, navigation, footer). A layout uses {% page %} to render the page content:
<html>
<body>
{% partial 'header' %}
{% page %}
{% partial 'footer' %}
</body>
</html>
Partials are reusable template fragments rendered with {% partial 'name' %}. They can accept variables:
{% partial 'card' title="Hello" body=post.content %}
Static content files in HTML, Markdown (.md), or plain text (.txt) format, rendered with:
{% content 'welcome.md' %}
onInit() - layout and page components initializeonStart() - layout code sectiononStart() - page code sectiononRun() - each component executesonEnd() - page code sectiononEnd() - layout code sectionComponents are reusable PHP classes that attach to pages, layouts, or partials. They encapsulate server-side logic, inject template variables, provide AJAX handlers, and render default markup. This is how October CMS avoids the complexity of separate API endpoints and frontend state management -- components bring data directly to the template where it's needed.
<?php namespace Acme\Blog\Components;
use Cms\Classes\ComponentBase;
class BlogPosts extends ComponentBase
{
public function componentDetails()
{
return [
'name' => 'Blog Posts',
'description' => 'Displays a list of blog posts',
];
}
public function defineProperties()
{
return [
'postsPerPage' => [
'title' => 'Posts per page',
'type' => 'string',
'default' => '10',
'validationPattern' => '^[0-9]+$',
],
];
}
public function onRun()
{
$this->page['posts'] = $this->loadPosts();
}
public function onLoadMore()
{
// AJAX handler
return ['#posts' => $this->renderPartial('@more-posts')];
}
protected function loadPosts()
{
return Post::paginate($this->property('postsPerPage'));
}
}
In your plugin's Plugin.php:
public function registerComponents()
{
return [
\Acme\Blog\Components\BlogPosts::class => 'blogPosts',
];
}
In a page or layout INI section:
[blogPosts]
postsPerPage = 5
In Twig markup:
{% component 'blogPosts' %}
| Component | Description |
|---|---|
ViewBag | Arbitrary page variables (used by Tailor and other modules) |
Resources | Register additional CSS/JS assets on a page |
SitePicker | Multisite language/site selector |
partial(), content(), component(), page(), placeholder(), ajaxHandler(), flash(), response(), redirect(), abort()
page (generate page URL), theme (theme asset URL), content (render content file)
{% page %} {# Render page inside layout #}
{% partial 'name' %} {# Render a partial #}
{% component 'name' %} {# Render component default markup #}
{% content 'file' %} {# Render content file #}
{% placeholder name %} {# Define a placeholder #}
{% put name %} {# Push content to a placeholder #}
{% scripts %} {# Render registered scripts #}
{% styles %} {# Render registered styles #}
{% meta %} {# Render registered meta tags #}
{% framework %} {# Include the AJAX framework #}
{% flash %} {# Render flash messages #}
{% cache %} {# Cache a template block #}
The Cms\Classes\Router matches incoming URLs against page URL patterns. Pages are matched in specificity order - static segments take priority over parameters.
Parameters are defined in page URLs with a colon prefix:
url = "/blog/:category/:slug"
Access in PHP:
$slug = $this->param('slug');
Access in Twig:
{{ :slug }}
| Class | Description |
|---|---|
Controller | Primary frontend controller; handles the full page lifecycle |
CmsController | Laravel route entry point; detects site and delegates to Controller |
CmsCompoundObject | Base for multi-section template files (Page, Layout, Partial) |
CmsObject | File-based model base using Halcyon datasource |
Page | Represents a CMS page file |
Layout | Represents a CMS layout file |
Partial | Represents a CMS partial file |
Content | Represents a CMS content file |
Asset | Represents a CMS asset file |
ComponentBase | Base class for all CMS components |
ComponentManager | Registry for all available components |
Router | URL-to-page matching |
Theme | Represents a theme directory |
ThemeManager | Theme installation, activation, deletion |
PageManager | URL resolution and content processing |
CodeParser | Parses PHP code sections in templates |
SnippetManager | Manages partial snippets for the rich editor |
| Event | Description |
|---|---|
cms.page.beforeDisplay | Override the rendered page before display |
cms.page.display | After page is rendered |
cms.router.beforeRoute | Override routing logic |
cms.component.beforeRunAjaxHandler | Before component AJAX handler |
cms.component.runAjaxHandler | After component AJAX handler |
cms.extendTwig | Register custom Twig extensions |
cms.theme.getActiveTheme | Override the active theme |
cms.content.postProcessMarkup | Post-process rendered content |
cms.pageLookup.listTypes | Register page link types |
cms.pageLookup.getTypeInfo | Provide page link type info |
cms.pageLookup.resolveItem | Resolve a page link item to a URL |
\Cms\Classes\Controller::extend(function ($controller) {
// Add middleware, modify behavior
});
In your plugin's Plugin.php:
public function registerMarkupTags()
{
return [
'functions' => [
'myFunction' => function ($arg) { return strtoupper($arg); },
],
'filters' => [
'myFilter' => function ($value) { return str_slug($value); },
],
];
}
The CMS module registers an Editor extension (Cms\Classes\EditorExtension) that provides an IDE experience for editing templates. It supports these document types:
cms-page - Pagescms-layout - Layoutscms-partial - Partialscms-content - Content filescms-asset - Asset filescms-lang - Language filesCMS templates use the Halcyon datasource (from the October Rain library) rather than database tables. Templates can be stored on the filesystem or in the database, with a caching layer for performance. Each theme is isolated in its own directory.