In a website using ContentBlocks, sometimes it's necessary to rebuild content. However, it gets stuck sometimes on a random resource without an apparent reason (or at least none in the error logs!). Even worse, once you fix it, it might get stuck on the next resource. A real pain!
The idea is to create a script that calls the rebuild process, but if there is a problem, it will show the reason why that resource can't be processed and won't stop the execution there (VOILĂ€!). It will continue with the rest of the process until the end!
You can do this from MODX, but you need to include the .php extension in the upload_files System Setting (if you can upload it, you can create and edit it!).
Create a file in the root, at the same level as your .htaccess, core folder, or index.php. Call the file: cb_rebuild.php
<?php
// Define MODX API Mode to prevent loading frontend templates and plugins
define('MODX_API_MODE', true);
// Require the core configuration file
require_once dirname(__FILE__) . '/config.core.php';
// Require the Composer autoloader for MODX 3
require_once MODX_CORE_PATH . 'vendor/autoload.php';
// Initialize the MODX Revolution object
$modx = new \MODX\Revolution\modX();
// Initialize the manager context
$modx->initialize('mgr');
// Security check: ensure the user is logged into the MODX manager
if (!$modx->user->hasSessionContext('mgr')) {
header('HTTP/1.1 403 Forbidden');
echo json_encode(['success' => false, 'message' => 'Access denied. You must be logged into MODX.']);
exit;
}
// Set the response header to JSON
header('Content-Type: application/json');
// Retrieve the requested action from the POST request
$action = $_POST['action'] ?? '';
// Action to get all active resource IDs
if ($action === 'get_ids') {
$c = $modx->newQuery('MODX\Revolution\modResource');
$c->where(['deleted' => 0]);
$c->select('id');
$c->sortby('id','ASC');
// Execute the query and fetch the IDs
if ($c->prepare() && $c->stmt->execute()) {
$ids = $c->stmt->fetchAll(PDO::FETCH_COLUMN);
echo json_encode(['success' => true, 'ids' => $ids]);
} else {
echo json_encode(['success' => false, 'message' => 'Error querying IDs']);
}
exit;
}
// Action to rebuild a specific resource ID
if ($action === 'rebuild_id') {
$id = (int)($_POST['id'] ?? 0);
// Validate the ID
if (!$id) {
echo json_encode(['success' => false, 'message' => 'Empty ID']);
exit;
}
// Fetch the MODX resource object
$resource = $modx->getObject('MODX\Revolution\modResource', $id);
// Verify the resource exists
if (!$resource) {
echo json_encode(['success' => false, 'message' => 'Not found']);
exit;
}
// Check if the resource uses ContentBlocks
$isContentBlocks = $resource->getProperty('_isContentBlocks', 'contentblocks');
if (empty($isContentBlocks)) {
echo json_encode(['success' => true, 'message' => 'Ignored (Not ContentBlocks)']);
exit;
}
// Retrieve the ContentBlocks properties
$properties = $resource->getProperty('content', 'contentblocks');
// Decode the JSON string into an array if necessary
if (is_string($properties)) {
$properties = json_decode($properties, true);
}
// Ensure properties is an array to prevent type errors in PHP 8
if (!is_array($properties)) {
$properties = [];
}
// Define the ContentBlocks core path
$corePath = $modx->getOption('contentblocks.core_path', null, $modx->getOption('core_path') . 'components/contentblocks/');
// Load the ContentBlocks service for MODX 3 and fallback for older versions
if (isset($modx->services) && $modx->services->has('contentblocks')) {
$cb = $modx->services->get('contentblocks');
} else {
$cb = $modx->getService('contentblocks', 'ContentBlocks', $corePath . 'model/contentblocks/');
}
// Attempt to generate the HTML and save the resource
try {
// Set the resource context for ContentBlocks snippets
if (method_exists($cb, 'setResource')) {
$cb->setResource($resource);
}
// Generate the HTML from the properties array
$html = $cb->generateHtml($properties);
// Update the resource content
$resource->set('content', $html);
// Save the resource and return the response
if ($resource->save()) {
echo json_encode(['success' => true, 'message' => 'OK']);
} else {
echo json_encode(['success' => false, 'message' => 'Native save failed']);
}
} catch (Throwable $e) {
// Catch and report any fatal errors or exceptions
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
exit;
}
Now that the PHP file is in your root folder, we are going to call it using a widget.
Go to System > Dashboards, click on Widgets, and create an HTML widget with this content:
<div style="background:#fff; padding:20px; border-radius:5px; box-shadow:0 1px 3px rgba(0,0,0,0.1);">
<h3 style="margin-top:0;">ContentBlocks Safe Rebuilder</h3>
<p>Executes native rebuild. Continues automatically if an ID crashes the server.</p>
<div style="margin-bottom:15px; padding:15px; background:#f9f9f9; border:1px solid #ddd; border-radius:4px;">
<button id="cb_start_btn" class="x-btn x-btn-small primary-button">Start Rebuild</button>
<button id="cb_stop_btn" class="x-btn x-btn-small" style="margin-left:10px; background-color:#dc3545; color:white; border:none;" disabled>Stop</button>
</div>
<div style="background:#e0e0e0; height:24px; border-radius:12px; overflow:hidden; margin-bottom:15px; border:1px solid #ccc;">
<div id="cb_progress" style="background:#32ab9a; width:0%; height:100%; transition:width 0.3s; text-align:center; color:white; font-size:12px; line-height:24px; font-weight:bold;">0%</div>
</div>
<div id="cb_log" style="background:#2b2b2b; color:#10e34b; font-family:Consolas, monospace; padding:15px; height:350px; overflow-y:auto; border-radius:5px; font-size:13px; line-height:1.5;">
Waiting for command...
</div>
</div>
<script>
let stopRequested = false;
document.getElementById('cb_stop_btn').addEventListener('click', function() {
stopRequested = true;
this.disabled = true;
const logPanel = document.getElementById('cb_log');
logPanel.innerHTML += `<span style="color:#ffb84d">Stopping process gracefully after the current item...</span><br>`;
logPanel.scrollTop = logPanel.scrollHeight;
});
document.getElementById('cb_start_btn').addEventListener('click', async function() {
const btn = this;
const stopBtn = document.getElementById('cb_stop_btn');
const logPanel = document.getElementById('cb_log');
const progressBar = document.getElementById('cb_progress');
btn.disabled = true;
stopBtn.disabled = false;
stopRequested = false;
logPanel.innerHTML = 'Fetching total resource list...\n<br>';
const log = (msg, isError = false, isAlert = false) => {
let color = '#10e34b';
if (isError) color = '#ff4d4d';
if (isAlert) color = '#ffb84d';
logPanel.innerHTML += `<span style="color:${color}">${msg}</span><br>`;
logPanel.scrollTop = logPanel.scrollHeight;
};
try {
const reqIds = await fetch('/cb_rebuild.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'action=get_ids'
});
const dataIds = await reqIds.json();
if (!dataIds.success) {
log('API connection error.', true);
btn.disabled = false;
stopBtn.disabled = true;
return;
}
const ids = dataIds.ids;
const total = ids.length;
log(`Found ${total} resources in the database. Starting scan...<br><br>`);
let processedCount = 0;
for (let i = 0; i < total; i++) {
if (stopRequested) {
log('<br>=== PROCESS CANCELLED BY USER ===', false, true);
break;
}
const id = ids[i];
try {
const req = await fetch('/cb_rebuild.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'action=rebuild_id&id=' + id
});
if (!req.ok) {
throw new Error(`Server returned HTTP ${req.status}`);
}
const rawText = await req.text();
if (rawText.trim() === '') {
throw new Error("Server disconnected silently (Memory exhausted or PHP Fatal Error).");
}
let res;
try {
res = JSON.parse(rawText);
} catch (parseErr) {
throw new Error("Invalid response format.");
}
if (!res.success) {
log(`[ID ${id}] Failed: ${res.message}`, true);
} else if (res.message.includes('Ignored')) {
} else {
processedCount++;
log(`[ID ${id}] Rebuilt successfully.`);
}
} catch (e) {
log(`--------------------------------------------------`, true);
log(`[CRASH ALERT] ID ${id} halted the script: ${e.message}`, true);
log(`Continuing with the next ID automatically...`, false, true);
log(`--------------------------------------------------`, true);
}
const percent = Math.round(((i + 1) / total) * 100);
progressBar.style.width = percent + '%';
progressBar.innerText = percent + '%';
}
if (!stopRequested) {
log('<br>=== PROCESS COMPLETED ===');
}
log(`Total ContentBlocks pages rebuilt in this session: ${processedCount}`);
} catch (e) {
log('General system failure: ' + e.message, true);
}
btn.disabled = false;
stopBtn.disabled = true;
});
</script>