ContentBox Theme Development (CFML)
Build custom themes for ContentBox CMS using CFML. Themes control the visual presentation of all public-facing content — blog entries, pages, archives, search results, and error pages.
Theme Structure
A ContentBox theme is a directory under modules_app/contentbox-custom/_themes/ (custom) or modules/contentbox/themes/ (core) containing:
MyTheme/
├── Theme.cfc ← Theme metadata, settings, lifecycle callbacks
├── screenshot.png ← Theme preview image (shown in admin)
├── layouts/
│ ├── blog.cfm ← MANDATORY: Blog entry layout
│ └── pages.cfm ← MANDATORY: Page layout
│ ├── maintenance.cfm ← Optional: Maintenance mode layout
│ └── search.cfm ← Optional: Search results layout (defaults to pages)
├── views/
│ ├── index.cfm ← MANDATORY: Home page (blog entry listing)
│ ├── entry.cfm ← MANDATORY: Single blog entry with comments
│ ├── page.cfm ← MANDATORY: Single page rendering
│ ├── archives.cfm ← MANDATORY: Blog archives view
│ ├── error.cfm ← MANDATORY: Error display
│ ├── notfound.cfm ← Optional: Entry not found view
│ └── maintenance.cfm ← Optional: Maintenance mode view
├── templates/
│ ├── entry.cfm ← Collection template for entry iterations
│ ├── category.cfm ← Collection template for category iterations
│ └── comment.cfm ← Collection template for comment iterations
├── widgets/ ← Theme-specific widget overrides
│ └── MyWidget.cfc ← Overrides core widgets of the same name
└── includes/ ← Help files, assets, etc.
Theme.cfc
The Theme.cfc defines metadata, settings, and lifecycle callbacks:
component {
// Theme Metadata
this.name = "My Custom Theme";
this.description = "A beautiful custom theme for ContentBox";
this.version = "1.0.0";
this.author = "Your Name";
this.authorURL = "https://example.com";
this.screenShotURL = "screenshot.png";
// Theme Settings — array of setting structs
this.settings = [
{
name : "siteTitle",
defaultValue : "My Site",
type : "text",
label : "Site Title:",
required : true,
group : "General"
},
{
name : "primaryColor",
defaultValue : "#3b82f6",
type : "color",
label : "Primary Color:",
group : "Colors"
},
{
name : "showSidebar",
defaultValue : true,
type : "boolean",
label : "Show Sidebar:",
group : "Layout"
},
{
name : "layoutStyle",
defaultValue : "grid",
type : "select",
label : "Entry Layout:",
options : "grid,list,masonry",
group : "Layout"
},
{
name : "footerText",
defaultValue : "",
type : "textarea",
label : "Footer Text:",
group : "General"
}
];
/**
* Called when the theme is activated
*/
function onActivation(){
// Run setup logic, create default content, etc.
}
/**
* Called when the theme is deactivated
*/
function onDeactivation(){
// Cleanup logic
}
/**
* Called when the theme is deleted
*/
function onDelete(){
// Cleanup logic
}
}
Setting Types
| Type | Description |
|---|---|
text |
Single-line text input (default) |
textarea |
Multi-line text area |
boolean |
Checkbox toggle |
select |
Dropdown select box |
color |
Color picker |
Setting Struct Keys
| Key | Required | Description |
|---|---|---|
name |
Yes | Setting name (saved as cb_themeName_settingName) |
defaultValue |
Yes | Default value |
type |
No | HTML control type (default: text) |
label |
No | HTML label (defaults to name) |
required |
No | Whether the setting is required (default: false) |
title |
No | HTML title attribute |
options |
No | For select: comma-separated list or array of values, or array of {name, value} structs |
optionsUDF |
No | UDF name (no parentheses) that returns options, e.g., getColors |
group |
No | Group name for organizing settings |
groupIntro |
No | Description text for a group |
fieldDescription |
No | Description for an individual field |
fieldHelp |
No | HTML for a modal help popup (use loadHelpFile() helper) |
Accessing Theme Settings in Views
Theme settings are available via the cb helper:
<!--- Get a theme setting --->
#cb.getThemeSetting( "siteTitle" )#
#cb.getThemeSetting( "primaryColor" )#
<!--- With fallback default --->
#cb.getThemeSetting( "showSidebar", true )#
Layout Files
blog.cfm — Blog Entry Layout
<cfoutput>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>#cb.getContent().getTitle()# — #cb.getThemeSetting( "siteTitle" )#</title>
#renderView( view = "_assets/head" )#
</head>
<body>
#renderView( view = "_assets/header" )#
<main class="container">
#renderView()#
</main>
#renderView( view = "_assets/footer" )#
</body>
</html>
</cfoutput>
pages.cfm — Page Layout
Similar structure to blog.cfm, used for rendering static pages.
View Files
index.cfm — Home Page
<cfoutput>
<!--- Render entries using collection template --->
#cb.renderCollection(
template = "entry",
collection = prc.entries,
counter = prc.start,
totalItems = prc.totalRecords
)#
<!--- Pagination --->
#cb.paginator(
totalRecords = prc.totalRecords,
maxRows = prc.maxRows,
page = prc.page,
pageLink = cb.siteURL() & "/page/{page}",
align = "center"
)#
</cfoutput>
entry.cfm — Single Blog Entry
<cfoutput>
<cfset entry = cb.getContent()>
<article class="entry">
<header>
<h1>#entry.getTitle()#</h1>
<div class="meta">
By #entry.getAuthor().getFullName()#
on #dateFormat( entry.getCreatedDate(), "mmmm d, yyyy" )#
</div>
</header>
<div class="content">
#entry.getHTMLContent()#
</div>
<!--- Categories --->
<cfif entry.getCategories().recordCount>
<div class="categories">
#cb.renderCollection(
template = "category",
collection = entry.getCategories()
)#
</div>
</cfif>
<!--- Comments --->
#cb.widget( "CommentForm" )#
</article>
</cfoutput>
page.cfm — Single Page
<cfoutput>
<cfset page = cb.getContent()>
<article class="page">
<h1>#page.getTitle()#</h1>
<div class="content">
#page.getHTMLContent()#
</div>
</article>
</cfoutput>
archives.cfm — Archives View
<cfoutput>
<h1>Archives</h1>
<!--- Monthly archives --->
<div class="archives-by-month">
#cb.renderCollection(
template = "entry",
collection = prc.entries
)#
</div>
<!--- Pagination --->
#cb.paginator(
totalRecords = prc.totalRecords,
maxRows = prc.maxRows,
page = prc.page
)#
</cfoutput>
error.cfm — Error Display
<cfoutput>
<div class="error-page">
<h1>Error</h1>
<p>#prc.errorMessage ?: "An unexpected error occurred."#</p>
<a href="#cb.siteURL()#" class="btn">Return Home</a>
</div>
</cfoutput>
Collection Templates
Templates in templates/ are used with cb.renderCollection(). Each template receives:
_counter— Current iteration index (1-based)_items— Total number of items in the collection{templateName}— The object being rendered (e.g.,entry,category,comment)
templates/entry.cfm
<cfoutput>
<article class="entry-preview">
<h2>
<a href="#cb.entryURL( entry )#">#entry.getTitle()#</a>
</h2>
<div class="meta">
#dateFormat( entry.getCreatedDate(), "mmmm d, yyyy" )#
by #entry.getAuthor().getFullName()#
</div>
<div class="excerpt">
#entry.getHTMLContentExcerpt()#
</div>
</article>
</cfoutput>
templates/category.cfm
<cfoutput>
<a href="#cb.categoryURL( category )#" class="category-tag">
#category.getCategory()#
</a>
</cfoutput>
templates/comment.cfm
<cfoutput>
<div class="comment">
<div class="comment-author">#comment.getAuthor()#</div>
<div class="comment-date">#dateFormat( comment.getCreatedDate(), "mmmm d, yyyy" )#</div>
<div class="comment-body">#comment.getComment()#</div>
</div>
</cfoutput>
Widget Overrides
Place widgets in widgets/ to override core widgets of the same name:
<!--- widgets/Menu.cfc — overrides the core Menu widget --->
component extends="contentbox.models.ui.BaseWidget" singleton {
function init(){
setName( "Menu" );
setVersion( "1.0.0" );
setDescription( "Custom menu widget override" );
}
any function renderIt( string menuName = "main" ){
// Custom menu rendering
}
}
The CB Helper
The cb helper (CBHelper@contentbox) is the primary API for theme development:
<!--- Site info --->
#cb.site()# <!--- Current site entity --->
#cb.siteURL()# <!--- Site base URL --->
#cb.siteName()# <!--- Site name --->
<!--- Content --->
#cb.getContent()# <!--- Current content (entry/page) --->
#cb.entryURL( entry )# <!--- Entry permalink --->
#cb.pageURL( page )# <!--- Page URL --->
#cb.categoryURL( category )# <!--- Category URL --->
<!--- Theme settings --->
#cb.getThemeSetting( "name" )#
<!--- Widgets --->
#cb.widget( "WidgetName", { arg1 = "value" } )#
<!--- Rendering --->
#cb.renderCollection( template = "entry", collection = query )#
#cb.renderView( view = "partial" )#
<!--- Menus --->
#cb.menu( "main" )#
<!--- RSS feeds --->
#cb.rssURL()#
#cb.rssCommentsURL()#
<!--- Search --->
#cb.searchURL()#
#cb.searchURL( "query" )#
<!--- Subscriptions --->
#cb.subscribeURL()#
#cb.unsubscribeURL()#
Theme Discovery and Registration
ContentBox discovers themes from two locations:
- Core themes:
modules/contentbox/themes/ - Custom themes:
modules_app/contentbox-custom/_themes/
The ThemeService@contentbox builds the theme registry at startup. Custom themes override core themes of the same name.
Theme Switching
Themes can be switched per-site via admin settings. The active theme is resolved at runtime:
<!--- In a handler or service --->
property name="themeService" inject="themeService@contentbox";
var activeTheme = themeService.getActiveTheme();
var themePath = themeService.getThemePath( activeTheme );
Best Practices
- Always include mandatory files:
Theme.cfc,blog.cfm,pages.cfm,index.cfm,entry.cfm,page.cfm,archives.cfm,error.cfm - Use
cbhelper for all URL generation — never hardcode paths - Use collection templates for iterating over entries, categories, comments
- Group theme settings logically using the
groupkey - Provide
screenshot.pngfor admin theme preview - Use
loadHelpFile()for field help that reads fromincludes/help/ - Keep theme-specific widgets in the theme's
widgets/folder - Test with multiple content types — entries, pages, categories, search results
- Use
prcscope for handler-passed data in views - Follow CFML compatibility — target Lucee 5+ and Adobe ColdFusion 2018+
Engine Compatibility
This skill targets CFML engines (Lucee 5+, Adobe ColdFusion 2018+). For BoxLang-specific syntax and features, see the BoxLang variant of this skill.
Key CFML considerations:
- Use
<cfoutput>for variable interpolation in CFML templates - Use
structKeyExists()for safe struct access - Use
listContains()for list operations - Use
arrayLen()for array length - Use
dateFormat()andtimeFormat()for date formatting