Behind the Build September 13, 2026

Ten Silent Failures We Hit Building the Same WordPress Site Six Times

On this page

How we found these, and why none of them threw an error

The problem section of the design, with its time-cost breakdown table

Every failure in this article has one thing in common. None of them produced an error.

We rebuilt the same homepage six times from an identical static prototype, once in each of Divi, GeneratePress, Elementor, Astra, Kadence and the Twenty Twenty-Five block theme that ships with WordPress. Every build had to match the original to the pixel, which meant every difference had to be explained rather than tolerated.

That turns out to be an unusually good way to find bugs. When your acceptance criterion is byte-identical output, a failure that would normally pass unnoticed becomes a hard stop. Ten did.

Not one of them threw an exception, logged a warning or turned anything red.

Every one of these came out of building the same 13-section homepage six times over, which is written up in full in the main comparison. The per-platform numbers live in the individual write-ups for Elementor, GeneratePress, Kadence, Astra and the WordPress block editor which carries its own set of measurements.

1. The filter registered at priority 12, where removing it at 10 does nothing

A custom Divi module with its settings panel open in the Visual Builder

WordPress rewrites your image tags on the way out. wp_filter_content_tags injects loading, decoding and fetchpriority attributes. On a page where the markup was hand-written and already carried loading="lazy", it added a second one. Invalid HTML produced by WordPress, on markup we wrote ourselves.

The obvious fix is a remove_filter call. It does nothing, because the filter is registered at priority 12 and a removal at the default priority of 10 silently fails to match. No error. The filter is simply still there.

It had a second effect nobody looked for: it assigned fetchpriority="high" to a background tile, which then competed with the deliberately preloaded hero image for largest contentful paint.

How to detect it: diff your rendered output against your source markup. If you are not doing that, you will not find this. How to fix it: pass the real priority to remove_filter. And check what else is registered at a non-default priority before assuming any removal worked.

2. Self-hosting your fonts makes bold text render lighter

One build self-hosted its fonts for performance and watched parity break from zero to 0.18%. The obvious suspect was the subsetter dropping hinting. It was not. Swapping to Google’s unmodified original files still left 0.13%.

It stopped theorising and cropped the differing pixels. The timeline headings were rendering lighter than the original.

The cause: those headings are <b> elements, so they inherit the browser’s default bold weight of

  1. The design requests font weights up to 600 and no further. With no 700 face available, the

browser takes the heaviest it has and synthesises the rest.

Here is the part that catches people. If you self-host a single variable font declared across a weight range, the browser clamps to a real 600 and synthesises nothing. That renders differently from the discrete per-weight faces Google serves. Same font, same weights requested, different output.

How to fix it: declare your self-hosted faces as discrete weights, exactly as the font host does. That restored zero divergence and kept the performance win.

Confirmed twice, from opposite directions. One build found it by introducing the bug. Another found it by removing one it had already hit.

3. Your stylesheet resolves image paths relative to itself

The product ecosystem section of the design, laid out as four cards

A stylesheet containing url(assets/plate.webp) resolves that path against the stylesheet’s own location. The site root and the theme root have nothing to do with it.

Move the stylesheet into a subdirectory as part of a tidy theme structure and every background image 404s. One build lost the hero plate, the sky layer and the call-to-action backdrop this way, producing a 66% pixel divergence at the top of the page.

Nothing in the console suggested a CSS problem. The stylesheet loaded fine. It was the images inside it that did not.

How to fix it: mirror the original’s directory layout. If the stylesheet sat beside an assets folder, put it beside an assets folder. Do not “improve” the structure of a file you are copying verbatim.

4. The cache that serves a stale page while your database is correct

A build wrote seven menu items to the database. The page rendered five. No error appeared, the data in the database was correct and the page served was stale.

The cause was a post meta key holding the platform’s rendered HTML, separate from the data being written. A scripted write to the page data does not invalidate it. And the documented cache-clearing command clears generated CSS, leaving that meta key untouched.

The worst part is how long it hid. You don’t go looking for a cache nobody told you about. The composer script had been missing this cache for the entire build and it happened to work every time, because the set of blocks changed on each rebuild. A rebuild that changed only a setting would have served a stale page, and the composer is the last place anyone would have looked.

How to detect it: change one setting only, then rebuild and confirm the front end actually changed. Not one block, one setting. How to fix it: find your platform’s render cache and clear it inside the script that writes, so it happens on every run.

5. The custom property your theme quietly redefines

A WordPress block selected in the editor with its inspector controls open

A design token called --gap. A builder whose containers write their flex gap into a property with the same name. Section padding defined as var(--gap) collapsing to zero.

The page came out 1,785 pixels shorter than it should have. The markup was identical, there was no CSS error and there was no console warning. Nothing to search for, because nothing was wrong with any individual rule.

The same class of failure hit a second build differently. Its theme puts a class called wrap on the main element. The design used wrap for its own content width. That collision forced an entirely different architectural decision, and it is the reason that build needed a custom page template where another did not.

How to detect it: dump your builder’s computed custom properties and diff them against your design’s token list. Before you debug anything else. Generic names are the risk. wrap, container, inner, content, card, gap: these are what everyone calls things, which is exactly why two independent codebases reach for the same one.

6. Lighthouse scores contrast as pass or fail, so partial fixes score zero

A page had nineteen failing contrast nodes, in two groups needing two different fixes. Two were a colour token used at full opacity. Seventeen were elements that start dim and animate in, which Lighthouse samples at load.

The arithmetic is the trap:

fix the colour token only   19 → 17 failing   score unchanged
fix the animation only      19 →  2 failing   score unchanged
fix both                    19 →  0 failing   score improves

The audit is binary. One failing node scores it zero. The category is a weighted average of binary audits, so nineteen failures and two failures score identically.

A locally correct fix ships, the score does not move, and it looks like the fix did not work.

How to fix it: enumerate every failing node before you start, and fix the whole set or accept that you are fixing it for the users rather than the score. Both are legitimate. Confusing them is not.

7. Measuring after the change and comparing it to a report also taken after the change

This one cost us two full rounds and it is embarrassingly easy to do.

A colour token was changed to improve contrast. The score did not move. Everyone concluded the change had achieved nothing, and it nearly got reverted.

Then somebody restored the old value and actually scored it. The change had removed thirteen genuine accessibility failures on small text. The reason nobody saw it: every report being compared had been generated after the change. The timestamps proved it. We had been comparing a number to itself.

The rule: if you are evaluating a change, score the before state explicitly. A report generated after the edit cannot tell you what the edit did, however carefully you read it.

8. The absence audit that proves nothing because the command failed

A build audited its own code to prove it contained no shortcuts. Zero occurrences of every pattern it searched for, which reads as a clean result.

The command had failed. A flag the shell rejected meant nothing ran, and a failed search returns zero matches that look exactly like a clean audit. The failure was visible in the output and was read straight past.

The rule, and it is stronger than “double-check”: an absence audit with no passing positive control is not weak evidence, it is no evidence. Before searching for what should not be there, search for something you know is there. If that returns nothing, your command is broken.

There is a second-order version. One build searched for a construct and got a hit from a code comment mentioning it. The pattern has to match the construct itself. A comment mentioning it satisfies a search for the topic and proves nothing.

9. The browser difference three separate builds each blamed on themselves

The company timeline section of the design, with seven dated milestones

At one specific width, a five-pixel-tall band differed by a maximum of 13 out of 255. Just under 2,319 pixels, in a minority of runs.

Three builds, three different themes, three sessions not comparing notes, reported the identical number, the identical maximum delta and the identical bounding box. Each of them spent time trying to work out what their build had done wrong. One theorised a fractional pixel boundary on a header edge. Another reported it as intermittent and then corrected its own frequency estimate twice.

A fourth build ran the experiment none of the others had: it compared the reference against itself, at sample size. The reference disagrees with itself at the same rate.

It is browser rendering nondeterminism, present in the original. There was never a build-versus- original difference to explain. Three careful teams each measured something real, each attributed it to their own work, and none of them could have found the truth from their own data.

The rule: when a measurement surprises you, measure the reference against itself before you measure anything else.

10. The block that fails to register, with no error at all

The homepage design: the hero section as rendered at desktop width

Build a WordPress block without a compiler and you have to write the asset manifest file yourself. Skip it, and the editor script loads with no dependencies declared. A core function it relies on is undefined at run time. The block does not register.

There is no console error, no admin notice and no entry in a log. The block is simply not in the inserter, and the page it should render shows nothing where it should be.

How to detect it: assert that every block you expect is actually registered, in code, as part of your build. Do not confirm it by looking at the editor, because a missing block looks like a block you forgot to add.

Who this is for, if you are hiring rather than debugging

If you are reading this because you are about to hand a WordPress build to someone, the useful takeaway is that none of these failures produced an error message. Ask whoever builds your site how they verified the result, rather than whether it looked right. That is most of what Zealous Sites does differently.

The thing all ten have in common

None of them produced an error, and most of them produced something that looked like success. A clean audit. A passing score. A page that rendered.

That is the actual lesson from rebuilding one page six times. The failures that cost the most were not the ones that broke something. They were the ones that quietly did nothing while appearing to work.

The only reliable defence we found is cheap and unglamorous: run the control. Score the before state. Search for something you know is present before concluding something is absent. Compare the reference against itself before comparing it against your work.

Every one of these ten was caught by a control. Not one was caught by reading the code more carefully.

Would you rather just hand this to someone?

That is the job. Zealous Sites has been building websites since 2010, which is more than sixteen years of handing finished sites to people who then have to live in them. We will work in whichever theme you want to use, and Divi is what we reach for first.

New sites start at $2,400, and you get a fixed price to approve before anything starts, so there’s no hourly meter running while we work out the scope. You can scope your own project on the pricing page, see what we have built, or tell us what you have in mind.

Want results like these?

Let's scope your project and put a real number on it.