Skip to content

2.7 regression: frontend CSS/JS assets not output when a theme template renders the page (buffer-and-replace mechanism removed) #1306

@adammichaelroach

Description

@adammichaelroach

Breakdance version: 2.7.0
Last known working version: 2.6.1

Theme: Blocksy 2.1.38 (but this affects any theme that uses the standard WordPress template lifecycle)
Host: Pantheon (not host-specific)
"Disable Theme" setting: Off

Summary

In Breakdance 2.7, when the "Disable Theme" option is off and a non-Breakdance theme template handles the request (i.e. maybe_override_the_theme_with_a_breakdance_template() returns the original $file_to_include), Breakdance's frontend asset and <script> tags are not output on the page. The generated CSS files exist on disk and are directly accessible, but no tags are emitted into the HTML, so the frontend renders unstyled.
Setting "Disable Theme" to on, or switching the page template to "Breakdance (no header/footer)", works around the issue — any path where Breakdance's template-simulator renders the page directly is fine.

Steps to reproduce

Install Breakdance 2.7.0 on a site using a standard WordPress theme (reproduced with Blocksy; expected to reproduce with any theme that renders via get_header(); while (have_posts()) { the_post(); the_content(); } get_footer(); — i.e. nearly all themes).

Ensure "Disable Theme" in Breakdance → Settings → Theme is off.

Build a page with Breakdance. Confirm the generated CSS exists at wp-content/uploads/breakdance/css/post-{ID}.css and loads fine when accessed directly by URL.
Visit the page on the frontend.
View source.

Expected

tags for the global Breakdance CSS (variables.css, global-settings.css, selectors.css, elements.css, etc.) and per-post CSS (post-{ID}.css, post-{ID}-defaults.css) are present in . Breakdance JS dependencies (Swiper, etc.) are present before .

Actual

No Breakdance asset tags are emitted. Only selectors.css and global-settings.css appear in some configurations; the rest (including per-post CSS and all JS dependencies) are missing entirely. The post-{ID} filename does not appear anywhere in the page source.

Root cause

In 2.6.1, plugin/render/capture-wordpress-output.php used a placeholder-and-replace mechanism:

registerAssetPlaceholdersInHeaderAndFooter() hooked wp_head / wp_footer to emit literal placeholder strings (%%BREAKDANCE_HEADER_DEPENDENCIES%% / %%BREAKDANCE_FOOTER_DEPENDENCIES%%).
getWordPressHtmlOutputWithHeaderAndFooterDependenciesAddedAndDisplayIt() buffered the entire template output with ob_start(), then ran renderHtmlFromScriptAndStyleHolder() after the_content had fired and populated ScriptAndStyleHolder, then str_replace()'d the placeholders with the rendered asset HTML.

In 2.7.0, this mechanism was removed. loadAssetsInHeaderAndFooter() now hooks wp_head / wp_footer directly and outputs $assets['headerHtml'] / $assets['footerHtml'] at hook time. But ScriptAndStyleHolder is populated inside \Breakdance\Render\getRenderedPost(), which is reached via replace_the_content_with_breakdance_content() in plugin/actions_filters/the_content.php — i.e. through the the_content filter. In the standard WordPress template lifecycle, wp_head fires before the_content, so at the moment 2.7's wp_head callback runs, the holder is empty and renderHtmlFromScriptAndStyleHolder() returns effectively empty asset HTML.

The 2.6 buffer-and-replace worked around this timing by deferring the replace to after the full render. 2.7 no longer does.

Why template-simulator paths still work

Paths like breakdance-no-template.php, breakdance-blank-canvas.php, and breakdance-template.php call \Breakdance\Render\render($post->ID) explicitly before wp_head() runs (see e.g. the position in plugin/themeless/util.php). So on those paths the holder is populated in time and 2.7's direct-output approach works. The regression only manifests when a theme's standard get_header(); the_content(); get_footer(); flow is used.

Relevant files / diff points

plugin/render/capture-wordpress-output.php — diff between 2.6.1 and 2.7.0 shows removal of BREAKDANCE_HEADER_ASSETS_PLACEHOLDER / BREAKDANCE_FOOTER_ASSETS_PLACEHOLDER constants, removal of getWordPressHtmlOutputWithHeaderAndFooterDependenciesAddedAndDisplayIt(), and rename of registerAssetPlaceholdersInHeaderAndFooter() → loadAssetsInHeaderAndFooter() with direct output.
plugin/actions_filters/template_include.php — loadAssetsInHeaderAndFooter() is now called but no longer wraps the template output.
plugin/actions_filters/the_content.php — replace_the_content_with_breakdance_content at priority -2147483648 is the only thing populating ScriptAndStyleHolder when a theme handles rendering, and it fires after wp_head.

Suggested fix

Either:

Restore the 2.6 buffer-and-replace mechanism when a theme template (rather than a Breakdance template-simulator) is handling rendering.

Populate ScriptAndStyleHolder earlier — e.g. trigger post CSS registration from template_redirect or wp rather than waiting for the_content — so the direct wp_head output in 2.7 actually has content to emit.

Workaround

Reinstating the 2.6 mechanism in a child theme / mu-plugin fixes the issue:

add_action('template_redirect', function () {
    if (is_admin() || is_feed() || is_robots() || is_trackback()) return;
    if (! function_exists('Breakdance\\Render\\renderHtmlFromScriptAndStyleHolder')) return;

    $h = '%%BD_FIX_HEADER_ASSETS%%';
    $f = '%%BD_FIX_FOOTER_ASSETS%%';

    add_action('wp_head',   function () use ($h) { echo $h; }, 1000001);
    add_action('wp_footer', function () use ($f) { echo $f; }, 1000001);

    ob_start(function ($buffer) use ($h, $f) {
        if (! class_exists('\Breakdance\Render\ScriptAndStyleHolder')) return $buffer;
        $assets = \Breakdance\Render\renderHtmlFromScriptAndStyleHolder(
            \Breakdance\Render\ScriptAndStyleHolder::getInstance()
        );
        return str_replace([$h, $f], [$assets['headerHtml'], $assets['footerHtml']], $buffer);
    });
}, 0);

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions