diff --git a/CHANGELOG.md b/CHANGELOG.md index e3c5af8..235e970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [1.0.10] - 2026-02-25 + +### Fixed + +- **Title double-encoding in Twig templates** (`inc/Template/ContextBuilder.php`): WordPress's `get_the_title()` pre-encodes `&` as `&`. When passed to Twig with autoescape enabled, the `&` in `&` was escaped again to `&`, rendering as literal `&` in the browser (e.g. "Bewerbungen & Nachrichten" instead of "Bewerbungen & Nachrichten"). Fixed by wrapping all 6 `get_the_title()` calls with `wp_specialchars_decode()` to decode WordPress entities before Twig. Twig autoescape then properly re-encodes `&` → `&`. This is XSS-safe because Twig still escapes all output. + ## [1.0.9] - 2026-02-19 ### Performance diff --git a/CLAUDE.md b/CLAUDE.md index 6b61a9c..4737631 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -234,6 +234,20 @@ Build steps (in order): ## Session History +### Session 16 — v1.0.10 Title Double-Encoding Fix (2026-02-25) + +**Completed:** Fixed double-encoding of HTML entities in page titles rendered through Twig. + +**Root cause:** WordPress's `get_the_title()` returns titles with HTML entities pre-encoded (e.g. `&` → `&`). `ContextBuilder` passed these pre-encoded strings to Twig as template variables. Twig's autoescape then re-encoded the `&` in `&` to `&`, which browsers rendered as the literal text `&` instead of `&`. Affected all pages with `&` in their title (e.g. help pages "Bewerbungen & Nachrichten", "Konto & Sicherheit", "Abonnements & Abrechnung"). + +**Fix:** Wrapped all 6 `get_the_title()` calls in `ContextBuilder.php` with `wp_specialchars_decode()`. This decodes WordPress entities back to raw characters before Twig, allowing Twig autoescape to properly encode them once. XSS-safe because Twig still escapes all output. + +**Files modified:** + +- `inc/Template/ContextBuilder.php` — `wp_specialchars_decode()` on all 6 `get_the_title()` calls +- `style.css` — version bump to 1.0.10 +- `CHANGELOG.md` — v1.0.10 entry + ### Session 15 — v1.0.9 Performance Optimization (2026-02-19) **Completed:** Two targeted performance fixes for production environments. diff --git a/inc/Template/ContextBuilder.php b/inc/Template/ContextBuilder.php index c023c36..b0120dc 100644 --- a/inc/Template/ContextBuilder.php +++ b/inc/Template/ContextBuilder.php @@ -153,7 +153,7 @@ class ContextBuilder return [ 'id' => $post->ID, - 'title' => get_the_title(), + 'title' => wp_specialchars_decode( get_the_title() ), 'url' => get_permalink(), 'content' => apply_filters('the_content', get_the_content()), 'excerpt' => get_the_excerpt(), @@ -184,7 +184,7 @@ class ContextBuilder $wp_query->the_post(); $posts[] = [ 'id' => get_the_ID(), - 'title' => get_the_title(), + 'title' => wp_specialchars_decode( get_the_title() ), 'url' => get_permalink(), 'excerpt' => get_the_excerpt(), 'date' => get_the_date(), @@ -349,14 +349,14 @@ class ContextBuilder if ($prev) { $navigation['previous'] = [ - 'title' => get_the_title($prev), + 'title' => wp_specialchars_decode( get_the_title($prev) ), 'url' => get_permalink($prev), ]; } if ($next) { $navigation['next'] = [ - 'title' => get_the_title($next), + 'title' => wp_specialchars_decode( get_the_title($next) ), 'url' => get_permalink($next), ]; } @@ -384,7 +384,7 @@ class ContextBuilder $query->the_post(); $posts[] = [ 'id' => get_the_ID(), - 'title' => get_the_title(), + 'title' => wp_specialchars_decode( get_the_title() ), 'url' => get_permalink(), 'date' => get_the_date(), 'date_iso' => get_the_date('c'), @@ -438,7 +438,7 @@ class ContextBuilder while ($query->have_posts()) { $query->the_post(); $posts[] = [ - 'title' => get_the_title(), + 'title' => wp_specialchars_decode( get_the_title() ), 'url' => get_permalink(), 'date' => get_the_date(), ]; diff --git a/style.css b/style.css index 721fa1e..86f9431 100644 --- a/style.css +++ b/style.css @@ -7,7 +7,7 @@ Description: A modern WordPress Block Theme built from scratch with Bootstrap 5. Requires at least: 6.7 Tested up to: 6.7 Requires PHP: 8.3 -Version: 1.0.9 +Version: 1.0.10 License: GNU General Public License v2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html Text Domain: wp-bootstrap