- **Offcanvas mobile navigation**: Default header now uses `header-offcanvas.html.twig` instead of `header.html.twig`. Mobile navigation slides in as an offcanvas panel from the right instead of collapsing downward.
- **User avatar in offcanvas header**: When logged in, the offcanvas header displays the user's Gravatar and display name linking to the WooCommerce My Account page (or WP admin profile as fallback). Falls back to the site name when logged out.
- **Dark mode toggle repositioned**: Moved from the offcanvas body to the offcanvas footer on mobile. Desktop toggle remains in the navbar.
### Added
- **User context data** (`inc/Template/ContextBuilder.php`): New `getUserData()` method exposing `user.logged_in`, `user.display_name`, `user.avatar`, and `user.account_url` to all Twig templates.
### Fixed
- **Admin bar overlapping offcanvas** (`functions.php`): Inline CSS via `wp_add_inline_style()` adds `padding-top` matching the admin bar height to `.offcanvas` when the admin bar is visible, preventing content overlap.
- **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.
- **Color variation CSS transient caching** (`functions.php`): `wp_bootstrap_variation_colors()` now caches the generated inline CSS in a 24-hour WordPress transient keyed by `wp_bootstrap_variation_css_` + an MD5 of the active stylesheet slug. Previously the palette iteration and CSS string building ran on every frontend page load. The transient is immediately invalidated on `switch_theme` and `save_post_wp_global_styles`, so changes made via the Design Editor are reflected instantly.
- **Twig template recompilation gated behind `WP_DEBUG`** (`inc/Twig/TwigService.php`): `auto_reload` in the Twig `Environment` constructor was hardcoded to `true`, causing Twig to stat every compiled template file on every request to check for source changes. Changed to `WP_DEBUG` so template recompilation only occurs during development. In production (`WP_DEBUG = false`) compiled Twig templates are served from cache without filesystem mtime checks.
- **Archive XSS hardening**: `ContextBuilder::getArchiveData()` now wraps `get_the_archive_title()` and `get_the_archive_description()` with `wp_kses_post()`. Term descriptions are user-editable by Editors and above; without sanitization an injected `<script>` tag would execute via the `|raw` filter in `archive.html.twig`
- **Comment author XSS hardening**: `ContextBuilder::buildCommentTree()` now applies `esc_html()` to `comment_author` and `esc_url()` to `comment_author_url` at the data source, preventing injection via user-supplied comment fields
- **Dark mode localStorage whitelist**: `getPreferredTheme()` in `dark-mode.js` now validates the stored theme value against `['dark', 'light']` before use, preventing attribute injection from a tampered localStorage value written by a third-party script
- **Twig escaping functions marked safe**: `esc_html()`, `esc_attr()`, and `esc_url()` registered in `TwigService` are now declared with `['is_safe' => ['html']]`, preventing double-encoding if Twig autoescape is ever enabled
### Changed
-`views/partials/comment-item.html.twig`: Comment author URL now output via `{{ comment.author_url|raw }}` (escaped in PHP) instead of calling `esc_url()` from the template, keeping escaping logic in one place
-`do_shortcode()` registered as a Twig function in `TwigService`, allowing shortcodes to be rendered directly from Twig templates via `{{ do_shortcode('[shortcode]') }}`
- Sidebar widgets not rendered on pages using the "Page with Sidebar" template — `ContextBuilder::build()` only populated `sidebar` context for `is_home()`, so `page-sidebar.html.twig` received no widget data
-`wp_bootstrap_should_render_template` filter in `TemplateController::render()` — allows plugins and child themes to prevent the theme from rendering a specific request, enabling clean separation of concerns when plugins handle their own page rendering
## [1.0.3] - 2026-02-11
### Fixed
- Double `<h1>` headings on pages where plugins provide their own titles — `page.html.twig` now wraps `<h1>` in `{% if post.title is not empty %}` guard so plugins can suppress it by passing empty `post.title`
- Missing HTML `<title>` tag on all pages — theme never declared `add_theme_support('title-tag')`, so WordPress's `_wp_render_title_tag()` hook was inactive during `wp_head()` output in Twig templates