A client's Etch builder canvas looked nothing like the frontend. The page was usable, but all AutomaticCSS styles were unavailable, custom properties, tokens, utility classes, gone.
The frontend rendered correctly; the builder was working from a completely different stylesheet baseline. Etch support called it a plugin conflict with Yabe Webfont and suggested a manual workaround. That’s not a diagnosis. That’s an exit.
So I read the source.
What actually happens
There are two bugs here — not one plugin conflicting with another.
Bug one is in AutomaticCSS. It registers the same filter callback, Etch::enqueue_preview_assets(), on etch/canvas/additional_stylesheets twice — once via ServiceProvider at plugin load time, once via IntegrationsManager during setup_theme. Both registrations are live simultaneously. When the filter fires, ACSS appends its three stylesheets twice: indices 0–2 on the first pass, 3–5 on the second.
Bug two is in Etch. Its normalize_assets_queue() method deduplicates the array with array_unique(), which is correct. What it doesn’t do is call array_values() afterwards. PHP’s array_unique() preserves original keys. So after removing the duplicate indices 3–5, the array has keys {0, 1, 2, 6} — non-sequential, because Yabe Webfont registered at priority 1000001 and its entry landed after the gap.
Here’s where PHP bites: json_encode() serializes sequential integer arrays as JSON arrays [], and non-sequential integer arrays as JSON objects {}. The result is that etchGlobal.iframe.additionalStylesheets arrives in the browser as a plain object. Etch’s canvas JavaScript iterates it as an array, finds nothing, injects zero <link> tags. No error. No warning. Just silence.
Yabe doesn’t break anything. Its late priority merely exposes a latent defect in Etch’s normalization logic that exists independently.
Execution trace
The full pipeline runs on wp_enqueue_scripts. The relevant source files are etch/classes/Hooks.php and etch/classes/Helpers/EtchGlobal.php on the Etch side, and automaticcss-plugin/classes/Framework/Integrations/Etch.php on the ACSS side.
wp_enqueue_scripts
└─ priority 10: Hooks::enqueue_etch_global_hook_data() [Hooks.php:76]
├─ apply_filters('etch/canvas/additional_stylesheets')
│ ACSS instance 1 (priority 10) → appends indices 0=tokens, 1=core, 2=custom
│ ACSS instance 2 (priority 10) → appends indices 3=tokens, 4=core, 5=custom
│ Yabe (priority 1000001) → appends index 6=yabe-webfont-cache-css
│ returns [0,1,2,3,4,5,6] → 7 entries
│
├─ normalize_assets_queue() [Hooks.php:140]
│ array_unique() removes indices 3,4,5 (duplicates of 0,1,2)
│ array_values() NOT called → keys {0,1,2,6} remain non-sequential
│ result: {0:tokens, 1:core, 2:custom, 6:yabe}
│
└─ wp_cache_set('etch_global_data', [...], 'etch')
stores the non-sequential array in the object cache
└─ priority 15: [this MU-plugin]
reads cache, calls array_values(), writes back
result: {0:tokens, 1:core, 2:custom, 3:yabe} ← sequential
└─ priority 20: EtchGlobal::enqueue_scripts() [EtchGlobal.php:65]
reads wp_cache_get('etch_global_data', 'etch')
json_encodes → etchGlobal.iframe.additionalStylesheets
sequential keys → JSON array [] ← canvas JS can iterate
DevTools evidence
On a ?etch=magic canvas page with Yabe active and no fix in place, run this in the browser console:
console.log(JSON.stringify(window.etchGlobal?.iframe?.additionalStylesheets, null, 2));
Output without the fix — a JSON object, not an array:
{
"0": { "id": "automaticcss-tokens", "url": ".../automatic-tokens.css" },
"1": { "id": "automaticcss-core", "url": ".../automatic.css" },
"2": { "id": "automaticcss-custom", "url": ".../automatic-custom-css.css" },
"6": { "id": "yabe-webfont-cache-css","url": ".../fonts.css" }
}
The key gap between 2 and 6 is the entire bug. All four stylesheets are present in the PHP output — the pipeline is intact up to that point. The failure is purely in serialization.
The fix
The timing constraint is itself a diagnostic finding. A filter on etch/canvas/additional_stylesheets at PHP_INT_MAX looks like it should work — but normalize_assets_queue() runs inside the filter execution and writes its result to the object cache immediately after. The fix has to land in the narrow window between that cache write (priority 10) and the cache read by EtchGlobal that feeds json_encode (priority 20). That’s why this runs as a wp_enqueue_scripts action at priority 15.
<?php
/**
* MU-Plugin: Fix Etch + Yabe Webfont array index gap.
*
* normalize_assets_queue() in Hooks.php uses array_unique() + array_filter()
* without array_values(), leaving non-sequential integer keys when Yabe's entry
* lands at index 6. PHP json_encode() then emits a JSON object {} instead of a
* JSON array [], which Etch's canvas JS cannot iterate to inject <link> tags.
*
* Runs at priority 15 — after Hooks writes to the object cache (priority 10)
* but before EtchGlobal reads it and json_encodes it (priority 20).
*
* File: wp-content/mu-plugins/yabe-etch-compat.php
*/
add_action( 'wp_enqueue_scripts', function () {
$data = wp_cache_get( 'etch_global_data', 'etch' );
if ( ! is_array( $data ) ) {
return;
}
if (
isset( $data['iframe']['additionalStylesheets'] ) &&
is_array( $data['iframe']['additionalStylesheets'] )
) {
$data['iframe']['additionalStylesheets'] = array_values(
$data['iframe']['additionalStylesheets']
);
wp_cache_set( 'etch_global_data', $data, 'etch' );
}
}, 15 );
Drop that file into wp-content/mu-plugins/. No other changes needed.
The proper fix in Etch’s source would be adding array_values() after the dedup loop in normalize_assets_queue(). The proper fix in ACSS would be ensuring the filter callback is only registered once.
Verify
After dropping the MU-plugin in place, reload the builder canvas and run the same DevTools check from above:
console.log(JSON.stringify(window.etchGlobal?.iframe?.additionalStylesheets, null, 2));
The output should now be a proper JSON array [...] with sequential indices 0–3. Confirm with:
Array.isArray(window.etchGlobal?.iframe?.additionalStylesheets)
Without the fix: false. With it: true. ACSS styles will load in the builder and match the frontend.
Strategic Opposition principle: the instinct to name a culprit is the enemy of diagnosis. “Plugin conflict” is a conclusion, not an investigation. The root cause here wasn’t between two plugins — it was between an untested normalization edge case and a PHP specification that has been documented for twenty years. Read the source before you assign blame.