## Subject
Route 66 2.4.1 — HTML entities are decoded in the final page when a performance option is on
## What happens
With any of the four performance options of Route 66 enabled, HTML entities disappear from the rendered page. Two things I noticed on Joomla 6.1.3 / PHP 8.3.30 / Route 66 2.4.1:
- a tag titled `audit & diagnostic` reaches the browser as `audit & diagnostic` — a bare ampersand in the markup, where Joomla had output `audit & diagnostic`
- a code sample published in an article, written as `<b>bold</b>` so that readers see the tag, is rendered as actual bold text instead
The second one is what put me on the trail: I write documentation articles that show HTML, and none of them display correctly.
## Steps to reproduce
1. Joomla 6.1.3, Route 66 2.4.1, any template.
2. In Route 66 options, set **lazy load images** to yes, leave the other three off.
3. Create a tag named `A & B`, assign it to an article, display tags on the article.
4. Put the literal text `<b>bold</b>` in the article body.
5. Look at the response source on the front end: the tag shows a bare `&`, and the escaped tag has become bold text.
6. Set the four performance options to no: correct output returns.
## Where it comes from
`administrator/components/com_route66/src/Helper/PerformanceHelper.php`, in `optimize()`:
```php
$buffer = $dom->saveHTML();
$buffer = str_replace('<?xml encoding="UTF-8">', '', $buffer);
$buffer = html_entity_decode($buffer, ENT_HTML5, 'UTF-8'); // <- here
$application->setBody($buffer);
```
Called from `plugins/system/route66/src/Extension/Route66.php` in `onAfterRender()`.
The decode looks intended to undo the numeric entities `DOMDocument::saveHTML()` produces for non-ASCII characters. Applied to the whole buffer, it also removes the entities that belonged to the content.
Worth noting: `prepareProtectedBlocks()` already shields `<pre>`, `<code>` and `<textarea>` from the DOM round-trip — but the decode runs after those blocks are restored, so a code sample inside `<code>` loses its entities anyway. The protection is there, the final decode undoes it.
## Trigger
`optimize()` returns early only when all four options are off, so enabling a single one is enough for the whole page to be decoded:
```php
if (!$params->get('iframe_facades') && !$params->get('iframes_lazy_load')
&& !$params->get('images_lazy_load') && !$params->get('optimize_css')) {
return;
}
```
## A possible fix
Decode only what `saveHTML()` introduced — numeric entities above codepoint 127 — and leave named entities from the content alone:
```php
$buffer = preg_replace_callback(
'/&#(?:(\d+)|[xX]([0-9a-fA-F]+));/',
static function (array $m): string {
$cp = $m[1] !== '' ? (int) $m[1] : (int) hexdec($m[2]);
return $cp > 127 ? mb_chr($cp, 'UTF-8') : $m[0];
},
$buffer
);
```
## Workaround meanwhile
Setting the four performance options to no is enough — routing and SEF are untouched, only the optimizer stops running.