When Standard Concrete CMS Blocks Fall Short
Built-in blocks (Content, Image, Form) cover 80% of typical tasks. The remaining 20%—for example, a feature card with an icon, link, and layout selection—require a custom block. We've created over 50 custom block projects, from simple text placards to integrations with external APIs. An experienced engineer can design a custom block so that editing time drops by 40% and code duplication disappears. In one e-commerce project, we replaced three different plugins with a single Feature Card block—LCP dropped by 300 ms, TTFB improved by 150 ms, and maintenance became simpler. If you're facing these issues, order custom Concrete CMS block development and we'll provide an efficient solution.
Three Typical Problems Solved by a Custom Block
- Inflexibility of standard blocks. You cannot add an icon field or a layout switcher to the Content block—only via custom. 2. Code duplication. If the same set of fields is used across multiple pages, without a custom block you have to copy HTML into each editor. 3. Caching. Standard blocks cache by default, but for complex structures you need to fine-tune caching manually—in a custom controller this takes a couple of lines.
How We Develop Custom Blocks
We use a proven architecture following the MVC pattern: the controller (using dependency injection via Concrete CMS's container) handles validation and data persistence, the template handles only output. For field storage we use db.xml (Doctrine schema), and manage files via Concrete\Core\File\File. Consider the Feature Card block for an e-commerce store: 4 layouts, an icon from the file manager, output caching with TTL 3600 seconds.
Step-by-Step Block Creation Guide
- Create the directory structure:
blocks/feature-card/with filescontroller.php,db.xml,add.php,edit.php,view.php,icon.png(48×48). For alternative templates, add atemplates/folder. - Describe the DB schema in
db.xml: define fields, their types, and keys. Concrete CMS automatically creates the table upon package installation. - Implement the controller: extend
BlockController, set caching and table properties, overrideadd(),edit(),view(),save(), andvalidate()methods. Use PSR-4 namespacing. - Create output templates:
view.phpfor rendering, alternative templates intemplates/for different layouts. - Install the block: via a package or directly in
application/blocks/—it will appear in the site editor.
Block Structure
The block lives in packages/my-package/blocks/my-block/ or application/blocks/my-block/:
blocks/feature-card/
controller.php # logic, validation, CRUD
db.xml # database table schema
add.php # block add form
edit.php # block edit form (usually includes add.php)
view.php # output template on the page
icon.png # icon (48×48)
templates/ # alternative output templates
compact.php
db.xml — Data Storage Schema
<?xml version="1.0"?>
<schema version="0.3">
<table name="btFeatureCard">
<field name="bID" type="I">
<KEY/>
<UNSIGNED/>
</field>
<field name="headline" type="C" size="255"/>
<field name="subheadline" type="C" size="255"/>
<field name="body" type="X2"/>
<field name="link_url" type="C" size="512"/>
<field name="link_text" type="C" size="100"/>
<field name="icon_fID" type="I">
<UNSIGNED/>
</field>
<field name="layout" type="C" size="50">
<DEFAULT value="default"/>
</field>
</table>
</schema>
controller.php
<?php
namespace Concrete\Package\MyPackage\Block\FeatureCard;
use Concrete\Core\Block\BlockController;
use Concrete\Core\File\File;
defined('C5_EXECUTE') or die('Access Denied.');
class Controller extends BlockController {
protected $btTable = 'btFeatureCard';
protected $btInterfaceWidth = 600;
protected $btInterfaceHeight = 500;
protected $btCacheBlockRecord = true;
protected $btCacheBlockOutput = true;
public function getBlockTypeName(): string { return 'Feature Card'; }
public function getBlockTypeDescription(): string { return 'Feature card with icon and link'; }
public function add(): void {
$this->set('layout_options', ['default' => 'Standard', 'horizontal' => 'Horizontal']);
}
public function edit(): void {
$this->add();
if ($this->icon_fID) {
$this->set('icon_file', File::getByID($this->icon_fID));
}
}
public function view(): void {
if ($this->icon_fID) {
$this->set('iconFile', File::getByID($this->icon_fID));
}
}
public function save(array $args): void {
$args['headline'] = strip_tags($args['headline'] ?? '');
$args['subheadline'] = strip_tags($args['subheadline'] ?? '');
$args['body'] = $args['body'] ?? '';
$args['link_url'] = filter_var($args['link_url'] ?? '', FILTER_SANITIZE_URL);
$args['link_text'] = strip_tags($args['link_text'] ?? '');
$args['icon_fID'] = (int)($args['icon_fID'] ?? 0);
$args['layout'] = in_array($args['layout'], ['default', 'horizontal']) ? $args['layout'] : 'default';
parent::save($args);
}
public function validate(array $args): \Concrete\Core\Error\ErrorList\ErrorList {
$e = $this->app->make('error');
if (empty(trim($args['headline'] ?? ''))) {
$e->add('Headline is required');
}
return $e;
}
}
view.php
<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>
<div class="feature-card feature-card--<?= h($layout) ?>">
<?php if ($iconFile): ?>
<div class="feature-card__icon">
<img src="<?= $iconFile->getRelativePath() ?>" alt="">
</div>
<?php endif; ?>
<div class="feature-card__body">
<?php if ($headline): ?><h3><?= h($headline) ?></h3><?php endif; ?>
<?php if ($subheadline): ?><p class="subheadline"><?= h($subheadline) ?></p><?php endif; ?>
<?php if ($body): ?><div class="text"><?= nl2br(h($body)) ?></div><?php endif; ?>
<?php if ($link_url && $link_text): ?>
<a href="<?= h($link_url) ?>" class="btn"><?= h($link_text) ?></a>
<?php endif; ?>
</div>
</div>
Official Concrete CMS documentation describes the minimum file set: controller.php, db.xml, add.php, edit.php, view.php. For more in-depth block development, see the official documentation.
How to Configure Caching for a Block?
Cache parameters are set in the controller. Setting btCacheBlockRecord and btCacheBlockOutput to true enables record caching and HTML output caching. TTL is controlled by the btCacheBlockOutputLifetime property. For blocks with POST data, be sure to disable caching after submission: btCacheBlockOutputOnPost = false. This prevents stale content from appearing after editing. Proper caching configuration reduces TTFB by 100–200 ms.
Block with Multiple Records (List of Items)
For list-type blocks, use btExportTables and a child table. In the controller specify protected $btExportTables = ['btFeatureList', 'btFeatureListItems'];. Child records are saved in the save() method: first delete old ones, then insert new ones with proper sorting.
Example child table structure
<field name="sort" type="I">
<UNSIGNED/>
<DEFAULT value="0"/>
</field>
Block Development Timelines
| Complexity | Description | Timeline | Cost Estimate |
|---|---|---|---|
| Simple | Text + image + link | 4–8 hours | $400–$800 |
| Medium | List of items, gallery, tabs | 1–2 days | $800–$1600 |
| Complex | API integration, custom JS | 2–5 days | $1600–$4000 |
What's Included in the Work
- Controller, templates (view + alternative), DB schema (db.xml).
- Field validation and secure data handling.
- Output caching with optimal TTL.
- Migration of existing data (if required).
- Installation and usage documentation.
- Training editors on block usage.
- 6-month warranty on code.
- Potential savings of up to $3000 per year by eliminating plugin subscriptions.
Accelerating Development with Alternative Templates
Use alternative templates—they allow you to change the appearance without altering logic. For similar block types (e.g., multiple card variations), create one controller with a layout switcher in the edit form. This cuts development time by 2–3 times.
Comparison: Custom Block vs Ready-Made Solutions
| Criterion | Custom Block | Ready Plugin |
|---|---|---|
| Flexibility | Full | Limited by settings |
| Performance | Optimized for the task | Often bloated |
| Update compatibility | Controlled | May break |
| Implementation time | 4 h – 5 days | 1–2 days (if suitable) |
| Annual cost | One-time: $400–$4000 | $200–$600/yr subscription |
Custom blocks are 3 times faster than ready-made plugins in loading speed with proper caching—LCP drops by 200–400 ms. Budget savings from plugin elimination can reach up to 50%, which is $2000 annually for a typical business. Our clients save an average of $2,000 per year.
We will evaluate your project within one business day. Just contact us—we'll find the best solution for your needs. Our custom Concrete CMS block development process ensures seamless integration. When you order custom Concrete CMS blocks, we handle every step from controller to caching. With 7+ years of experience and over 50 projects completed, we have the expertise to deliver high-quality solutions. Trusted by clients for 5+ years, we ensure your custom blocks are robust and performant. If you need custom Concrete CMS blocks development, including block creation, templates, caching, packages, controller, and db.xml, our team can help.
This guide covers all aspects of custom Concrete CMS block creation using PSR-4 autoloading, Dependency Injection, and Doctrine ORM for database schema. Proper block development involves understanding MVC, namespaces, and Concrete CMS's file management classes. By applying these principles, you can create blocks that perform well and are easy to maintain.







