This WordPress-Theme is built from scratch employing Bootstrap 5. Build modern Websites using the state-of-the-art Framework. All basic WordPress components are converted and are fully working. The theme uses Twig for rendering. It is also a good starting point as base theme for complex WordPress websites.
### Features
- [ ] All native WordPress theme files converted to Boostrap 5
- [ ] Works seemlessly on default WordPress installations
- [ ] Installable via WordPress admin
#### Frontend
- [ ] Fully responsive Design
- [ ] Supports darkmode
#### Administration
- [ ] Compatible with the Gutenberg-Editor
- [ ] Customizable with the Design-Editor
### Key Fact: 100% AI-Generated
This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase was created through AI assistance.
## Temporary Roadmap
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
- Create releases from branch `main` after merging branch `dev`
- Tags should use format `vX.X.X` (e.g., `v1.1.22`), start with v0.1.0
- Use annotated tags (`-a`) not lightweight tags
- **ALWAYS push tags to origin** - CI/CD triggers on tag push
- Commit messages should follow the established format with Claude Code attribution
-`.claude/settings.local.json` changes are typically local-only (stash before rebasing)
**CRITICAL - Release Workflow:**
On every new version, ALWAYS execute this complete workflow:
```bash
# 1. Commit changes to dev branch
git add <files>
git commit -m "Description of changes (vX.X.X)"
# 2. Merge dev to main
git checkout main
git merge dev --no-edit
# 3. Create annotated tag
git tag -a vX.X.X -m "Version X.X.X - Brief description"
# 4. Push everything to origin
git push origin dev main vX.X.X
# 5. Switch back to dev for continued development
git checkout dev
```
Never skip any of these steps. The release is not complete until all branches and the tag are pushed to origin.
---
**For AI Assistants:**
When starting a new session on this project:
1. Read this CLAUDE.md file first
2. Semantic versioning follows the `MAJOR.MINOR.BUGFIX` pattern
3. Check git log for recent changes
4. Verify you're on the `dev` branch before making changes
5. Run `git submodule update --init --recursive` if lib/ is empty (only if submodules present)
6. Run `composer install` if vendor/ is missing
7. Test changes before committing
8. Follow commit message format with Claude Code attribution
9. Update this session history section with learnings
10. Never commit backup files (`*.po~`, `*.bak`, etc.) - check `git status` before committing
11. Follow markdown linting rules (see below)
Always refer to this document when starting work on this project.
### Markdown Linting Rules
When editing CLAUDE.md or other markdown files, follow these rules to avoid linting errors:
1.**MD031 - Blank lines around fenced code blocks**: Always add a blank line before and after fenced code blocks, even when they follow list items. Example of correct format:
- **Item label**:
(blank line here)
\`\`\`php
code example
\`\`\`
(blank line here)
2.**MD056 - Table column count**: Table separators must have matching column counts and proper spacing. Use consistent dash lengths that match column header widths.
3.**MD009 - No trailing spaces**: Remove trailing whitespace from lines
4.**MD012 - No multiple consecutive blank lines**: Use only single blank lines between sections
5.**MD040 - Fenced code blocks should have a language specified**: Always add a language identifier to code blocks (e.g., `txt`, `bash`, `php`). For shortcode examples, use `txt`.
6.**MD032 - Lists should be surrounded by blank lines**: Add a blank line before AND after list blocks, including after bold labels like `**Attributes:**`.
7.**MD034 - Bare URLs**: Wrap URLs in angle brackets (e.g., `<https://example.com>`) or use markdown link syntax `[text](url)`.
8.**Author section formatting**: Use a heading (`### Name`) instead of bold (`**Name**`) for the author name to maintain consistent document structure.
- **Dark mode:** Uses Bootstrap 5.3 `data-bs-theme` attribute on `<html>`. An inline anti-flash script runs synchronously in `<head>` (via `wp_add_inline_script` with `'before'`), while the full `dark-mode.js` is deferred. Preference stored in `localStorage` key `wp-bootstrap-theme`.
- **Block styles:** Registered via `register_block_style()` with `inline_style` parameter in `functions.php`. Dark mode overrides for alert/card styles are in `src/scss/_custom.scss`.
- **Style variations:** JSON files in `styles/` directory. All 15 variations (7 light, 7 dark, plus default) use the same 10 color slug names (base, contrast, primary, secondary, success, danger, warning, info, light, dark) to ensure patterns work across all schemes.
- **Fonts:** Inter (sans-serif) and Lora (serif) variable fonts bundled as `.woff2` in `assets/fonts/`. Declared via `fontFace` in `theme.json` with `font-display: swap`.
- **Patterns:** PHP files in `patterns/` with WordPress block markup and i18n. Hidden patterns (prefixed `hidden-`) are reusable components not shown in the pattern inserter.
- **Twig frontend rendering:** `TemplateController` hooks `template_redirect` to intercept frontend requests and render Bootstrap 5 HTML via Twig, bypassing FSE block markup. FSE templates remain for the Site Editor. WordPress functions that produce output (`wp_head`, `wp_footer`, `body_class`, `language_attributes`) are captured via `ob_start()`/`ob_get_clean()` and passed to Twig as safe HTML strings.
- **Navigation menus:** `NavWalker` converts flat `wp_get_nav_menu_items()` into a nested tree for Bootstrap dropdown rendering. Falls back to listing published pages when no menu is assigned.
- **Docker development:** WordPress runs in Docker container `jobroom-wordpress`. The theme directory must be bind-mounted via `compose.override.yaml` (absolute path) for live changes to be visible.
**Completed:** Cross-theme security audit with 12 findings, all fixed. Covers WidgetRenderer regex hardening, ContextBuilder performance (O(n) comment tree, sidebar query consolidation, transient caching), and hex-to-RGB code consolidation.
**What was changed:**
- **WidgetRenderer regex fix** (`inc/Block/WidgetRenderer.php`): Combined two `preg_replace` calls into single regex matching `<h2>` with `wp-block-heading` class, preventing mismatched tags.
- **O(n) comment tree** (`inc/Template/ContextBuilder.php`): Parent-indexed lookup map replaces full-scan recursion. Each level now iterates only direct children.
- **Sidebar query consolidation** (`inc/Template/ContextBuilder.php`): Three separate sidebar detection branches merged into single boolean + one `getSidebarData()` call.
- **Transient caching** (`inc/Template/ContextBuilder.php` + `functions.php`): `getSidebarRecentPosts()` and `getSidebarTags()` cached in 1-hour transients with hook-based invalidation (`save_post`, `create/edit/delete_post_tag`).
- **Hex-to-RGB consolidation** (`functions.php`): Eliminated duplicate hex parsing. Added `ctype_xdigit()` validation and type hints to all color utility functions.
**Files modified:**
-`inc/Block/WidgetRenderer.php` — single regex for h2→h4
### Session 20 — v1.1.1 PHPUnit Test Suite (2026-02-28)
**Completed:** PHPUnit test suite with 64 unit tests and 107 assertions, CI integration, and build pipeline gating.
**What was built:**
- **PHPUnit 11 + Brain\Monkey 2.7**: Added as `require-dev` with PSR-4 `autoload-dev` mapping (`WPBootstrap\Tests\` → `tests/`).
- **WP_HTML_Tag_Processor stub** (`tests/Stubs/WpHtmlTagProcessor.php`): Functional DOMDocument-based replacement supporting `next_tag()` (by tag name or class_name), `add_class()` (idempotent), and `get_updated_html()`. Uses full HTML document wrapping for reliable body extraction.
- **WidgetRendererTest** (9 tests): Card structure, title heading, widget ID, type class extraction, generic filtering, h2→h4 regex, multiple h2 elements, empty content.
- **NavWalkerTest** (14 tests): Tree building (empty, single, flat, nested, multi-parent, orphans), node structure, classes, index reset, active detection (current-menu-item, ancestor, is_page, is_category, inactive).
- **TemplateControllerTest** (12 tests): Template resolution via ReflectionMethod for all page types (404, search, post default/full-width, page default/landing/full-width/hero/sidebar, archive, home, fallback).
- **Build pipeline**: `npm run test` and `prebuild` hook gate `npm run build` on passing tests.
- **CI workflow**: New `test` job between `lint` and `build-release` with PHP 8.3 + Composer.
- **Release exclusions**: `tests/`, `phpunit.xml.dist`, `.phpunit.cache/*` excluded from ZIP with verification step.
**Architecture decisions:**
- **Brain\Monkey over full WordPress bootstrap**: WordPress function mocking via `Functions\when()->justReturn()` and `Functions\when()->alias()` enables fast, isolated unit tests without a running WordPress installation.
- **DOMDocument stub over regex**: `WP_HTML_Tag_Processor` stub uses `DOMDocument`/`DOMXPath` for reliable HTML parsing. Full document wrapping (`<!DOCTYPE><html><body>...`) required because `LIBXML_HTML_NOIMPLIED` prevents `<body>` creation in PHP 8.4, breaking `getElementsByTagName('body')`.
- **`is_admin()=true` constructor bypass**: BlockRenderer/WidgetRenderer constructors register WordPress filters. Mocking `is_admin()` to return true causes early exit, enabling direct method testing.
- **ReflectionMethod for private methods**: `TemplateController::resolveTemplate()` is private. Testing via reflection avoids refactoring production code for testability.
- **ContextBuilder and TwigService skipped**: Too many WordPress dependencies for practical unit testing — better suited for integration tests.
**Key findings:**
-`LIBXML_HTML_NOIMPLIED` with `DOMDocument::loadHTML()` on PHP 8.4 does not create a `<body>` element, causing `getElementsByTagName('body')->item(0)` to return null. Solution: wrap input in full HTML document structure.
- Brain\Monkey's `Functions\when()->alias()` supports argument-dependent returns for functions like `is_singular()` that behave differently based on post type argument.
-`spl_object_id()` used in the stub's visited-node tracking enables sequential `next_tag()` advancement matching WordPress's forward-only API.
**Completed:** Bootstrap 5 class injection for core blocks, sidebar widget card wrappers, widget SCSS styling, and sidebar-default post template.
**What was built:**
- **BlockRenderer** (`inc/Block/BlockRenderer.php`): Per-block `render_block_{$name}` filters inject Bootstrap classes into 8 core block types using `WP_HTML_Tag_Processor`. Supports button color mapping (WP preset slugs → Bootstrap btn-{variant}), striped tables, responsive images, input-group search forms, blockquote styling, and list-group block style. Extensible via `wp_bootstrap_block_renderer_blocks` filter.
- **WidgetRenderer** (`inc/Block/WidgetRenderer.php`): `dynamic_sidebar_params` filter wraps sidebar widgets in Bootstrap `.card > .card-body` structure. `widget_block_content` filter downgrades block widget `<h2 class="wp-block-heading">` to `<h4>` via preg_replace. Widget ID and type-specific classes extracted from the already-processed `before_widget` string.
- **Widget SCSS** (`src/scss/_widgets.scss`): Comprehensive widget styling — list items with border separators, flush-to-card-edge positioning (negating card-body padding), form-control selects, input-group search forms, pill-badge tag cloud, and secondary-color post dates. Handles both legacy and block widget nesting (`.wp-block-group > ul`).
- **List Group block style** (`functions.php`): `register_block_style('core/list', ...)` for the `is-style-list-group` style option in the editor.
- **Single post sidebar template** (`views/pages/single-sidebar.html.twig`): Two-column Bootstrap layout (8/4 split) with all single post features. "More posts" section uses `row-cols-md-2` for the narrower column.
- **Post template selection** (`inc/Template/TemplateController.php`): Posts default to sidebar layout; `page-full-width` template slug maps to full-width.
- **Sidebar data always loaded for posts** (`inc/Template/ContextBuilder.php`): `getSidebarData()` called unconditionally for `is_singular('post')`.
**Architecture decisions:**
- **Per-block filters over generic `render_block`**: `render_block_{$blockName}` only fires for the targeted block type, avoiding callback overhead on every block render. More maintainable — each handler is scoped to one block type.
- **`WP_HTML_Tag_Processor` over regex**: WordPress 6.7+ class provides safe, forward-only HTML manipulation. `add_class()` is idempotent (no duplicate classes). Handles malformed HTML gracefully.
- **Card-body with card-title, not card-header**: WordPress omits `before_title`/`after_title` entirely when a widget has no title. Using `card-header` for the title would leave an empty `<div class="card-header"></div>` for titleless widgets — broken HTML. Card-body with card-title inside works correctly in both cases.
- **`dynamic_sidebar_params` + regex extraction**: WordPress runs `sprintf` on `before_widget` BEFORE the `dynamic_sidebar_params` filter fires, so placeholder strings (`%1$s`, `%2$s`) are already replaced. Widget ID comes from `$params[0]['widget_id']`, type classes extracted via regex from the processed HTML.
- **Sidebar default for posts**: Blog posts are content-centric and benefit from sidebar context (recent posts, search, tags). Full-width is opt-in via "Full Width" template assignment.
**Key findings:**
-`WP_Block $instance` type hint is too strict for manual `apply_filters()` calls — WordPress passes `null` when filters are invoked outside the block rendering pipeline. Use `?WP_Block $instance = null`.
- Block widgets nest content inside `.wp-block-group` wrappers. CSS selectors like `.card-body > ul` won't match — need `.card-body > .wp-block-group > ul` for flush list positioning.
-`widget_block_content` filter (WordPress 5.8+) fires for block-based widgets only, allowing inner HTML modification without affecting legacy widgets.
- WordPress search block uses `.wp-block-search__inside-wrapper` as the input+button container — adding `.input-group` to this element creates a proper Bootstrap input-group.
-`@extend .btn; @extend .btn-primary` in SCSS works for search submit buttons because Bootstrap is imported before the widgets partial in the SCSS cascade.
### Session 18 — v1.0.12 Admin Bar Offcanvas Fix (2026-02-28)
**Completed:** Scoped admin bar offcanvas padding to mobile viewports only.
**What was fixed:**
-`functions.php`: Added `@media (max-width: 991.98px)` wrapper to the admin bar offcanvas padding CSS so the extra padding does not appear on desktop where the offcanvas renders inline as a regular navbar.
**Completed:** Switched mobile navigation from Bootstrap collapse to offcanvas, added logged-in user context to the header, and fixed admin bar overlap.
**What was changed:**
- **Offcanvas navigation** (`views/base.html.twig`): Default header include switched from `partials/header.html.twig` (collapse) to `partials/header-offcanvas.html.twig` (offcanvas slide-in from right). The offcanvas variant already existed in the theme.
- **Offcanvas header with user avatar** (`views/partials/header-offcanvas.html.twig`): When logged in, the offcanvas header shows the user's Gravatar avatar and display name linking to the WooCommerce My Account page. Falls back to the site name when logged out.
- **Dark mode toggle repositioned**: Moved from the offcanvas body to the offcanvas footer (`d-lg-none`) on mobile. On desktop (≥lg), the toggle remains visible next to the navbar via a separate `d-none d-lg-block` wrapper.
- **User context in ContextBuilder** (`inc/Template/ContextBuilder.php`): New `getUserData()` method providing `user.logged_in`, `user.display_name`, `user.avatar` (rendered `<img>` with `rounded-circle` class), and `user.account_url` (WooCommerce My Account or WP admin profile fallback).
- **Admin bar offcanvas overlap fix** (`functions.php`): Inline CSS injected via `wp_add_inline_style()` when `is_admin_bar_showing()` is true. Adds `padding-top: var(--wp-admin--admin-bar--height, 32px)` to `.offcanvas` so the offcanvas content clears the admin bar.
**Files modified:**
-`views/base.html.twig` — header include changed to offcanvas variant
-`views/partials/header-offcanvas.html.twig` — user avatar header, dark mode toggle in footer
-`inc/Template/ContextBuilder.php` — `getUserData()` method, `user` key in context
-`functions.php` — admin bar offcanvas padding inline style
-`style.css` — version bump to 1.0.11
-`CHANGELOG.md` — v1.0.11 entry
**Key learnings:**
- Bootstrap offcanvas inside `navbar-expand-lg` uses `position: fixed; top: 0` which is covered by the WordPress admin bar (`z-index: 99999`). Since the offcanvas z-index (1045) is lower, adjusting `top` alone doesn't help visually — `padding-top` on the offcanvas content is the practical fix.
-`wp_add_inline_style()` bypasses file-level browser caching, making it more reliable for conditional CSS rules than editing the main stylesheet.
- WordPress's `--wp-admin--admin-bar--height` CSS custom property (set on `:root`) adjusts between 32px (desktop) and 46px (mobile ≤782px), making it the ideal value for admin bar offset calculations.
-`get_avatar()` accepts an `$args` array where CSS classes can be passed via the `class` key, avoiding post-processing of the HTML output.
### 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 `&#038;`, 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
**Completed:** Two targeted performance fixes for production environments.
**Changes made:**
- **Color variation CSS transient caching** (`functions.php`): `wp_bootstrap_variation_colors()` now caches the generated Bootstrap CSS variable overrides in a 24-hour transient keyed by `wp_bootstrap_variation_css_` + `md5(get_stylesheet())`. Previously the palette loop and CSS string building executed on every frontend request. Transient is invalidated on `switch_theme` and `save_post_wp_global_styles` hooks so Design Editor changes apply immediately.
- **Twig `auto_reload` gated behind `WP_DEBUG`** (`inc/Twig/TwigService.php`): Hardcoded `auto_reload => true` caused Twig to `stat()` each compiled template file on every request to detect source file changes. Changed to `auto_reload => WP_DEBUG` so stat checks only occur during development. In production, compiled templates are served from cache unconditionally.
**Files modified:**
-`functions.php` — transient caching and invalidation for variation CSS
**Completed:** Comprehensive OWASP-aligned security audit. Two parallel background agents reviewed all PHP (functions.php, ContextBuilder, NavWalker, TemplateController, TwigService, all patterns) and JavaScript/Twig templates. Four targeted security fixes applied.
**Findings and fixes:**
- **Archive term description XSS (High)**: `get_the_archive_description()` returns raw term content editable by Editor-role users. Templates rendered it with `|raw`, creating a stored XSS path. Fixed: wrapped with `wp_kses_post()` in `ContextBuilder::getArchiveData()`. Same applied to `get_the_archive_title()`.
- **Comment author injection (Low, defense-in-depth)**: `comment_author` and `comment_author_url` were passed to Twig as raw database values. Fixed: `esc_html()` applied to author name, `esc_url()` applied to author URL in `ContextBuilder::buildCommentTree()`. Template updated to output pre-escaped URL via `|raw` rather than calling `esc_url()` in Twig.
- **Dark mode localStorage whitelist (Medium)**: `getPreferredTheme()` returned any stored value without validation, allowing attribute injection if a malicious script wrote to `localStorage`. Fixed: strict equality check against `['dark', 'light']` before trusting the stored value.
- **Twig escaping functions not marked safe (Medium)**: `esc_html()`, `esc_attr()`, `esc_url()` registered in `TwigService` lacked `['is_safe' => ['html']]`, meaning any future autoescape enablement would cause double-encoding. Fixed: all three now carry the `is_safe` declaration.
**Confirmed secure (no action needed):**
- All `|raw` filter usages for widget HTML, comment content, comment reply links, and comment forms are by-design (WordPress core output)
- Pattern files: no direct-access guards needed (loaded only via `register_block_pattern()`)
- No SQL injection vectors (`$wpdb` not used directly; all data via WordPress functions)
-`TemplateController` error handling: `\Throwable` caught, logged, and gated behind `WP_DEBUG`
-`do_shortcode()` and `wp_kses_post()` Twig functions correctly marked `is_safe`
- WordPress Twig themes should not enable `autoescape => 'html'` globally: `get_the_title()` applies `wptexturize()` which returns HTML entities (`—`, `“`). Autoescape would double-encode these, corrupting post title rendering.
-`esc_url()` does more than HTML-encoding — it validates the URL scheme and strips dangerous protocols (`javascript:`, `data:`). Always use it for user-supplied URLs, even when autoescape is active.
- Registering WordPress escaping functions (`esc_url`, `esc_html`, `esc_attr`) as Twig functions without `is_safe => html` silently creates a double-encoding trap: calling `{{ esc_url(url) }}` with autoescape on would produce `&amp;` instead of `&`.
- Added `.markdownlint.json` disabling MD024 (duplicate headings, expected in changelogs) and MD013 (line length).
**Files modified:**
-`inc/Template/ContextBuilder.php` — archive data sanitization, comment field escaping
-`inc/Twig/TwigService.php` — `is_safe => html` on three escaping functions
-`views/partials/comment-item.html.twig` — use pre-escaped author URL
**Completed:** Added `wp_bootstrap_should_render_template` filter to `TemplateController::render()` for clean plugin/theme separation.
**What was added:**
- New `wp_bootstrap_should_render_template` filter at the top of `TemplateController::render()` — returns `true` by default, but plugins can return `false` to prevent the theme from rendering a request
- Enables the wp-jobroom plugin to handle its own custom post types and routes without the theme's `TemplateController` racing to render first
- Theme remains 100% standalone — the filter is a no-op when no plugin hooks into it
**Key learnings:**
- WordPress `template_redirect` hook priority ordering is the primary mechanism for plugin/theme rendering coordination: plugin Router at priority 5, theme TemplateController at default priority 10
- Adding a simple filter check (`apply_filters('wp_bootstrap_should_render_template', true)`) is the cleanest decoupling mechanism — no cross-project class detection needed
- Imported Bootstrap Icons SCSS in both `style.scss` and `editor-style.scss`
- Added `$bootstrap-icons-font-src` variable override in `_variables.scss` to point `@font-face` at `assets/fonts/`
- Added `copy:icons` npm script to copy `.woff`/`.woff2` font files from `node_modules` to `assets/fonts/`
- Updated `build` script to include `copy:icons` step
**Key learnings:**
- Bootstrap Icons SCSS uses `$bootstrap-icons-font-src` to allow overriding the `@font-face``src` declaration — set it before the import to control font file paths
- The existing `--load-path=node_modules` Sass flag resolves `@import "bootstrap-icons/font/bootstrap-icons"` without any extra configuration
- Font files (`.woff2` at 131KB, `.woff` at 176KB) are small enough to serve as web fonts without performance concern
**Completed:** Fixed dark mode rendering conflicts between WordPress global styles and Bootstrap, fixed form element styling in dark mode, bridged style variation colors to Bootstrap CSS custom properties, fixed variation detection to read from correct palette origin.
- Style variation bridge function (`wp_bootstrap_variation_colors`) that maps WordPress palette colors to Bootstrap CSS custom properties via `wp_enqueue_scripts`
- Helper functions for color manipulation: `wp_bootstrap_hex_to_rgb()`, `wp_bootstrap_build_surface_css()`, `wp_bootstrap_mix_hex()`, `wp_bootstrap_hex_to_rgb_array()`, `wp_bootstrap_relative_luminance()`
- WordPress puts style variation colors in the `theme` palette origin, NOT `custom` -- `wp_get_global_settings(['color', 'palette', 'theme'])` returns the base theme.json merged with the active variation
- The `custom` palette origin contains user manual edits from the Site Editor, but its data structure may lack expected `slug`/`color` keys
- To detect an active variation, compare `theme` origin colors against known base theme.json defaults rather than checking for slugs in `custom`
- WordPress `theme.json``styles.color` generates `body { background-color: var(--wp--preset--color--base) }` directly on `body`, which overrides inherited CSS variables from `html[data-bs-theme="dark"]` -- removing `styles.color` from theme.json is the cleanest fix
- CSS variables defined directly on `body` beat inherited values from `html` due to specificity, requiring `!important` on `html[data-bs-theme="dark"] body` to ensure Bootstrap dark mode works
- Plugin-generated form elements (e.g., `<select class="jr-search-form__filter-select">`) lack Bootstrap classes and need explicit dark mode styling via element selectors
**Completed:** Added 8 new style variations (4 light, 4 dark) to the Design Editor.
**What was built:**
- 4 new light palettes: Rose (pink/fuchsia), Sand (warm amber/beige), Lavender (soft purple), Mint (fresh teal)
- 4 new dark palettes: Slate (neutral blue-gray), Mocha (coffee/warm brown), Nebula (space teal-cyan), Obsidian (near-black with red)
- All variations follow the established 10-slug color pattern for cross-variation pattern compatibility
**Key learnings:**
- Dark style variations swap base/contrast (dark base, light contrast) and use darker shades for the `light` and `dark` slugs to maintain proper surface hierarchy
- Button and link hover states in dark palettes need explicit `color` and `text` overrides since the default theme.json assumes light base
- CSS logical properties (`border-inline-start`, `padding-inline-start`) are the modern approach to RTL support, replacing physical left/right properties
- Twig `|e('html')` filter must be applied *before* concatenation with `|raw` HTML to prevent XSS -- `search_query|e('html')` inside a `|format()` call
- Font preloading via `wp_head` at priority 1 ensures preload hints appear before render-blocking stylesheets
-`aria-live="polite"` with `role="status"` announces dynamic changes to screen readers without interrupting current reading flow
- Offcanvas navigation Twig partial with Bootstrap offcanvas component
- Twig block inheritance in `base.html.twig` for header/footer variant overrides
- Header/footer variant support via `get_theme_mod()` in `ContextBuilder`
- Custom page template routing via `get_page_template_slug()` in `TemplateController`
- Shadow presets, aspect ratios, custom layout values in `theme.json`
- Transparent header and offcanvas dark mode SCSS styles
- Updated translations (`.pot` and `de_CH.po`) with ~70 new translatable strings
**Key learnings:**
- WordPress `block_categories_all` filter (block inserter categories) and `register_block_pattern_category()` (pattern inserter categories) are separate APIs serving different parts of the editor UI
- FSE template parts (`parts/`) use pattern references (`<!-- wp:pattern {"slug":"..."} /-->`) to keep markup in PHP pattern files for i18n support
- Twig blocks (`{% block header %}`) enable page-level templates to override header/footer without modifying `base.html.twig`
-`get_page_template_slug()` returns the custom template slug assigned in the page editor, used in `TemplateController` for routing to the correct Twig template
- Bootstrap SCSS deprecation warnings (Dart Sass 3.0 migration) are upstream issues, not blocking — the build succeeds
**Completed:** Full Twig-based Bootstrap 5 frontend rendering, replacing FSE block markup on the public-facing site.
**What was built:**
-`TemplateController` class hooking `template_redirect` to render Twig templates for all page types
-`ContextBuilder` class gathering WordPress data (posts, menus, pagination, comments, sidebar, archive info) into structured arrays
-`NavWalker` class converting flat menu items to nested tree for Bootstrap dropdown menus
- 20 Twig templates: base layout, 5 page templates (index, single, page, archive, search, 404), 9 partials (header, footer, pagination, sidebar, comments, search form, dark mode toggle, meta, post navigation), 3 components (post card, grid card, post loop)
- Enhanced `TwigService` with WordPress output-buffering functions, globals, and filters
- Navigation menu locations (primary, footer) with pages fallback
- Comment form Bootstrap styling filter
- README.md project documentation
**Key learnings:**
-`template_redirect` + `exit()` cleanly bypasses FSE rendering on frontend while preserving Site Editor functionality
- WordPress functions that produce output (`wp_head`, `wp_footer`, `body_class`, `language_attributes`) must be captured via `ob_start()`/`ob_get_clean()` for use in Twig, and marked with `is_safe => html`
- With `optimize-autoloader: true` in `composer.json`, new PSR-4 classes require `composer dump-autoload` to regenerate the static classmap
- Post content is rendered via `apply_filters('the_content', get_the_content())` which processes Gutenberg blocks into standard HTML that Bootstrap CSS handles natively
- Bootstrap 5 alert colors are hardcoded (not CSS variables), so explicit dark mode overrides are needed in SCSS
- Anti-flash for dark mode requires a synchronous inline script via `wp_add_inline_script('handle', '...', 'before')` — the deferred JS alone causes a flash
- Variable fonts use `fontWeight: "100 900"` range syntax in `theme.json``fontFace` declarations
- Style variation JSON files must maintain identical color slug names across all variations for patterns to work correctly
- When re-tagging a release, delete the remote tag first (`git push origin :refs/tags/vX.X.X`) then recreate and push
### Session 1 — v0.0.1 Getting Started (2026-02-08)