ContentBlocks Rebuild Process Stuck

A solution to the problem of ContentBlocks Rebuild Content getting stuck when you need it most!

The Problem

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 Cause
  1. Fatal errors or memory exhaustion on specific pages.
  2. Sometimes everything looks fine but it still causes problems.
  3. I gave up and built my own rebuilder!

The Solution

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!

Step 1You'll need access to the root folder

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;
        }
        
Step 2Create a Widget for your Dashboard

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>
        
Add your Widget to your Dashboard and enjoy it!

Did this save you time and a headache?

Consider leaving a tip!

Thanks!