Missing CSS when loading Bricks templates via AJAX

Hello everyone,
I am trying to solve a problem related to loading a part of a page with randomly generated content. More and more often, I receive requests from clients to display randomized data in certain sections of a page – most commonly loops of projects, references, logos, images, hints, etc. Unfortunately, most projects are currently running behind Cloudflare and/or local cache plugins. In this setup, the only realistic option is to use AJAX for the random data and generate it using custom PHP, otherwise the output gets cached.

It occurred to me to use a template in Bricks (Bricks → Templates, Type: Section) and load it via AJAX. In this template, I could use the same visuals that are already used in the system. This would save a huge amount of time, and at the same time the client could manage the template themselves in the visual editor.

Unfortunately, I ran into what seems to be a fundamental limitation: when a Bricks template is loaded this way via AJAX, Bricks completely blocks the CSS of that template. The content itself is generated correctly and transferred to the target location without any issues, but none of the visual styling is applied, including background images of elements, etc.

When I insert the template shortcode directly into the test page ([bricks_template id=“123”]) just for testing purposes, the CSS is loaded. However, it is not loaded in a configuration suitable for randomized data – for example, background images are taken from the cached, fixed shortcode output. Even if the shortcode is set to display:none, this approach is practically unusable, and the content is duplicated in the page (even if hidden). For the same reason, global styles are often unusable as well.

So the question is:
Is it possible to achieve a setup where, when a template is loaded via AJAX, the visual styling is generated together with the HTML and can be injected into the target page as well?

Of course, it is also possible that I have misunderstood the intended workflow and that I am approaching this in a completely wrong way, and that there is a much simpler and more functional solution.

Current implementation
functions.php / custom plugin:

add_action(‘rest_api_init’, function () {
register_rest_route(‘custom/v1’, ‘/bricks-template’, [
‘methods’  => ‘GET’,
‘callback’ => ‘ajax_bricks_template_raw’,
‘permission_callback’ => ‘__return_true’,
]);
});

function ajax_bricks_template_raw() {
nocache_headers();
header(‘Cache-Control: no-store, no-cache, must-revalidate, max-age=0’);
header(‘Content-Type: text/html; charset=UTF-8’);

echo do_shortcode('[bricks_template id="123"]');
exit;

}

AJAX on the target page:

<div id="ajax-bricks-template"></div>
<script>
document.addEventListener("DOMContentLoaded", () => {
  fetch("/wp-json/custom/v1/bricks-template", { cache: "no-store" })
    .then(r => r.text())
    .then(html => {
      document.getElementById("ajax-bricks-template").innerHTML = html;
    });
});
</script>

Thanks in advance for any help or for pointing me in the right direction.

Hello @Roice,

this feels to me more like a How To question, not a bug, right? I’ve moved it to the right category, but please let me know if I missed something. The report is quite long and harder to follow :sweat_smile:

I’m not sure if I understand correctly, but it seems that if you just turn the caching off for that specific page, that will work, right?

Matej

Hi,
yes, you’re probably right — I wasn’t completely sure about the correct category.
I tried to describe the issue as thoroughly as possible, since it’s not exactly a simple problem.

But no, you’re not right about the main point — because the described issue also occurs with caching disabled. In other words, the problem still happens when someone tries to load a template using the [bricks_template id=“123”] shortcode via AJAX.
You might ask — why would anyone do that? Well, there’s a clear reason: when trying to bypass both the server-side cache and Cloudflare cache, AJAX is often the only viable solution. And when it comes to displaying elements in random order, bypassing the cache is basically the only option.

From my perspective, it shouldn’t be that complicated — especially since Bricks already loads the correct CSS when the template is inserted via shortcode. So why doesn’t it do the same when the shortcode is loaded via AJAX?

If I may suggest a possible solution: you could return the template content along with inline styles as part of the AJAX response. Maybe this could be controlled via a shortcode parameter, like [bricks_template id=“123” css=“true”].

Vasek

Why not using Shortcode Element and dynamic tag to dynamically get the template id from custom field ?

But if you prefer the manual way, try this code below.

add_action( 'wp_enqueue_scripts', function() {
    wp_enqueue_script( 'ajax_bricks_template', get_stylesheet_directory_uri() . '/assets/js/ajax_bricks_template.js', array('bricks-scripts'), false, true );
    
    wp_localize_script( 'ajax_bricks_template', 'rest_data', array( 
        'rest_url'    => esc_url_raw( rest_url( 'custom/v1/bricks-template' ) ), 
        'nonce'       => wp_create_nonce( 'your-custom-nonce' ),
        'template_id' => 123 
    ));
});

add_action('rest_api_init', function () {
    register_rest_route('custom/v1', '/bricks-template', [
        'methods'  => 'POST',
        'callback' => 'ajax_bricks_template_raw',
        'permission_callback' => 'ajax_bricks_template_raw_permissions_check',
    ]);
});

function ajax_bricks_template_raw( $request ) {
	$request_data 	= $request->get_json_params();
	$template_id 	= absint( $request_data['template_id'] );
	
	if ( get_post_status( $template_id ) !== 'publish' ) {
		return rest_ensure_response(
			[
				'html'   => '',
				'styles' => '',
				'error'  => 'Invalid template status',
			]
		);
	}
	
	$elements = \Bricks\Database::get_data( $template_id, 'content' ); // get_post_meta( $template_id, BRICKS_DB_PAGE_CONTENT, true );
	
	if ( empty( $elements ) || ! is_array( $elements ) ) {
		return rest_ensure_response(
			[
				'html'   => '',
				'styles' => '',
				'error'  => 'Selected template is empty.',
			]
		);
	}
	
	$template_inline_css 	= \Bricks\Templates::generate_inline_css( $template_id, $elements );
	$template_content 		= \Bricks\Frontend::render_data( $elements );
	$template_inline_css	.= \Bricks\Assets::$inline_css_dynamic_data;
	$template_inline_css 	= \Bricks\Assets::minify_css( $template_inline_css );
	$styles 				= ! empty( $template_inline_css ) ? "<style>{$template_inline_css}</style>\n" : '';
	
	return rest_ensure_response(
		[
			'html'   => $template_content,
			'styles' => $styles
		]
	);
}


function ajax_bricks_template_raw_permissions_check( $request ) {
	$data = $request->get_json_params();
	
	if ( empty( $data['template_id'] ) || empty( $data['nonce'] ) ) {
		return new \WP_Error( 'ajax_bricks_template_api_missing', __( 'Missing parameters' ), [ 'status' => 400 ] );
	}
	
	$result = wp_verify_nonce( $data['nonce'], 'your-custom-nonce' );
	
	if ( $result === false ) {
		return new \WP_Error( 'rest_invalid_nonce', __( 'Nonce check failed' ), [ 'status' => 403 ] );
	}

	return true;
}
document.addEventListener("DOMContentLoaded", () => {
    const container = document.getElementById("ajax-bricks-template");
    if (!container) return;

    async function getBricksTemplate(templateId, nonce) {
        // 3. Use the localized URL from PHP
        const endpoint = rest_data.rest_url;

        try {
            const response = await fetch(endpoint, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    template_id: templateId,
                    nonce: nonce
                })
            });

            const data = await response.json();

            if (data.error) {
                console.error('Bricks AJAX Error:', data.error);
                return;
            }

            // Inject styles and HTML
            container.innerHTML = data.styles + data.html;

            // 4. IMPORTANT: Re-init Bricks scripts
            setTimeout(() => {
                bricksRunAllFunctions();
            }, 200);

        } catch (error) {
            console.error('Fetch error:', error);
        }
    }

    // Call using localized data
    getBricksTemplate(rest_data.template_id, rest_data.nonce);
});

Thanks for the reply, but I think we’re talking about slightly different things.

AJAX is not optional here – it’s required due to Cloudflare and page caching. Server-side dynamic data still ends up cached.

The issue is not that Bricks generates CSS server-side (that’s expected), but that there is currently no way to reuse or attach the template CSS when a template is rendered via AJAX, even though the CSS clearly exists when the same template is rendered during page load.

Inserting the template shortcode hidden on the page proves that Bricks can generate the correct CSS, but there is no supported way to access or enqueue that CSS for AJAX-rendered output.

The simplified code above already does what you need: it generates the section template HTML and CSS (you may also need to generate additional CSS such as global classes, theme styles, etc.). You only need to adjust the logic for returning the correct template ID and then localize it.

If you look at the render_shortcode() method in Templates.php, you’ll see why it works on page load but not during REST or AJAX calls.

Hi,
thanks a lot for sharing your code – it’s impressive.

That said, I have to admit that using a hack on this level, which relies so heavily on internal Bricks behavior and lifecycle simulation, is something I would be very hesitant to deploy on a client’s production site. Any Bricks update, change in initialization order, or internal refactor could easily break this approach, and that’s a risk I can’t really take responsibility for.

Still, thank you very much for the effort and for taking the time to share this – it’s definitely an interesting and clever solution. I was quietly hoping there might be a simpler, more systematic or officially supported way to solve this. It looks like that’s not the case (at least for now).