D
P
0

WordPress & PHP

strtoupper Turned a WordPress Category Chip Into 'BANKING & NEOBANKS'

June 16, 2026·3 min read
strtoupper Turned a WordPress Category Chip Into 'BANKING & NEOBANKS'

On a WordPress site I maintain, a category chip was supposed to read "BANKING & NEOBANKS". What actually rendered on screen was this:

BANKING & NEOBANKS

The literal text &, sitting right in the middle of the label. Not a rendered & character — five actual characters: ampersand, A, M, P, semicolon. It looks like random encoding corruption, but it's perfectly logical once you know the cause.

The markup was simple — a chip that uppercases the term name:

<span class="chip">
    <?php echo esc_html( strtoupper( $term->name ) ); ?>
</span>

The category name is "Banking & Neobanks". So why does & come out as &AMP;?

Root cause: term names are already encoded in display context

This is the part that trapped me. When WordPress hands you a term field in 'display' context (which is what happens through get_the_terms, wp_get_post_terms, and whenever a term is accessed in a normal loop), it runs that field through sanitize_term_field. And in 'display' context, that sanitizer encodes & into &amp;.

That means $term->name for "Banking & Neobanks" is not the string you think it is. The actual value in memory is:

Banking &amp; Neobanks

Now follow the chain. If you just esc_html and print it, everything's fine — the browser receives &amp; and decodes it back to &. That's exactly why naive-looking code so often "happens to be correct."

But the moment you slip in strtoupper, it uppercases the whole string — entity included:

Banking &amp; Neobanks
   strtoupper ↓
BANKING &AMP; NEOBANKS

And &AMP; is not a valid HTML entity. Entities are case-sensitive; the valid one is &amp; (lowercase). Because &AMP; is unrecognized, the browser doesn't decode it — it prints it verbatim. That's why the literal text shows up in the chip.

Why CSS text-transform is safe but PHP/JS isn't

I briefly considered just uppercasing in CSS, and that does dodge this bug — for a reason worth understanding:

.chip { text-transform: uppercase; } /* safe */

text-transform: uppercase operates on already-rendered text. By the time CSS runs, the browser has already decoded &amp; into &, so what gets uppercased is the & character — not the entity. The effect is purely visual, and correct.

But strtoupper (PHP) and toUpperCase() (JS) operate on the raw string before the browser decodes anything. They see &amp; and dutifully uppercase it into &AMP;. That's the trap: a case function at the code layer breaks the entity, a case function at the presentation layer doesn't.

The fix: decode entities BEFORE transforming case

The fix is to decode the entity back to its raw character first, then uppercase:

<span class="chip">
    <?php echo esc_html(
        strtoupper( wp_specialchars_decode( $term->name, ENT_QUOTES ) )
    ); ?>
</span>

Now the chain is correct:

  1. wp_specialchars_decode( $term->name, ENT_QUOTES ) turns Banking &amp; NeobanksBanking & Neobanks
  2. strtoupper( ... ) turns it → BANKING & NEOBANKS
  3. esc_html( ... ) re-encodes &&amp;, safe for output
  4. The browser decodes &amp;& at render time

Final result on screen: BANKING & NEOBANKS. Exactly what you wanted.

The same trap applies to strtolower, ucfirst, and mb_strtoupper. Any PHP case function — or toUpperCase/toLowerCase in JavaScript — will mangle the entity if you run it over the raw term string.

Lesson

WordPress term names come HTML-entity-encoded in 'display' context. $term->name for "Banking & Neobanks" is actually "Banking & Neobanks". esc_html alone is fine because the browser decodes the entity back — but the instant you run strtoupper/strtolower over it, &amp; becomes an invalid &AMP; and leaks out as literal text.

The rule: never run a PHP/JS case function over a term name without wp_specialchars_decode( ..., ENT_QUOTES ) first. If all you need is an uppercase look, text-transform: uppercase in CSS is the safest route — it works on already-decoded text and never touches the entity.