- WidgetRenderer: single regex for h2→h4 prevents mismatched tags - ContextBuilder: O(n) comment tree with parent-indexed lookup map - ContextBuilder: consolidated sidebar queries into single check - ContextBuilder: transient caching for sidebar recent posts and tags - functions.php: hex-to-RGB consolidation, type hints, ctype_xdigit validation - Transient invalidation hooks for save_post and tag CRUD operations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
46 KiB
WordPress Theme using Bootstrap 5
Author: Marco Graetsch Author URL: https://src.bundespruefstelle.ch/magdev Author Email: magdev3.0@gmail.com Repository URL: https://src.bundespruefstelle.ch/magdev/wp-bootstrap Issues URL: https://src.bundespruefstelle.ch/magdev/wp-bootstrap/issues
Project Overview
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.
Current version is v1.1.2. See PLAN.md for details.
Technical Stack
- Language: PHP 8.3.x
- PHP-Standards: PSR-4
- Framework: Latest WordPress Theme API
- Template Engine: Twig 3.0 (via Composer)
- Frontend: Bootstrap 5 Javascript & Vanilla JavaScript
- Styling: Bootstrap 5 & Custom CSS (if necessary)
- Dependency Management: Composer (PHP), npm (JS/CSS)
- Internationalization: WordPress i18n (.pot/.po/.mo files)
- Canonical Plugin Name:
wp-bootstrap
Security Best Practices
- All user inputs are sanitized (integers for quantities/prices)
- Nonce verification on form submissions
- Output escaping in templates (
esc_attr,esc_html,esc_js) - Direct file access prevention via
ABSPATHcheck - XSS-safe DOM construction in JavaScript (no
innerHTMLwith user data)
Translation Ready
All user-facing strings use:
__('Text to translate', 'wp-bootstrap')
_e('Text to translate', 'wp-bootstrap')
Text domain: wp-bootstrap
Translation Template
- Base
.potfile created:languages/wp-bootstrap.pot - Ready for translation to any locale
- All translatable strings properly marked with text domain
Available Translations
en_US- English (United States) [base language - .pot template]de_CH- German (Switzerland, formal)de_CH_informal- German (Switzerland, informal)de_DE- German (Germany, formal)de_DE_informal- German (Germany, informal)en_GB- English (United Kingdom)es_ES- Spanish (Spain)fr_CH- French (Switzerland)fr_FR- French (France)it_CH- Italian (Switzerland)it_IT- Italian (Italy)nl_NL- Dutch (Netherlands)pl_PL- Polish (Poland)pt_PT- Portuguese (Portugal)
Translation file naming convention: wp-bootstrap-{locale}.po (e.g., wp-bootstrap-de_CH.po)
Compiled .mo files are built by the Gitea CI/CD pipeline during releases. For local development:
for po in languages/wp-bootstrap-*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
Updating Translations
When new strings are added to PHP sources, use the fast JSON workflow documented in
wp-jobroom-theme/CLAUDE.md → Updating Translations (Fast JSON Workflow). That
document contains the full step-by-step process including the patch-po.py patcher script
(located in wp-jobroom-theme/languages/patch-po.py) which patches both wp-bootstrap
and wp-jobroom-theme .po files in a single pass.
Quick reference for wp-bootstrap POT regeneration:
docker exec jobroom-wordpress wp i18n make-pot \
/var/www/html/wp-content/themes/wp-bootstrap \
/var/www/html/wp-content/themes/wp-bootstrap/languages/wp-bootstrap.pot \
--allow-root
# Then merge into all .po files:
for locale in de_CH de_CH_informal de_DE de_DE_informal en_GB es_ES fr_CH fr_FR it_CH it_IT nl_NL pl_PL pt_PT; do
msgmerge --update --backup=none --no-fuzzy-matching \
languages/wp-bootstrap-${locale}.po languages/wp-bootstrap.pot
done
Create Releases
Important Git Notes:
- Default branch while development is
dev - Create releases from branch
mainafter merging branchdev - 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.jsonchanges are typically local-only (stash before rebasing)
CRITICAL - Release Workflow:
On every new version, ALWAYS execute this complete workflow:
# 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:
- Read this CLAUDE.md file first
- Semantic versioning follows the
MAJOR.MINOR.BUGFIXpattern - Check git log for recent changes
- Verify you're on the
devbranch before making changes - Run
git submodule update --init --recursiveif lib/ is empty (only if submodules present) - Run
composer installif vendor/ is missing - Test changes before committing
- Follow commit message format with Claude Code attribution
- Update this session history section with learnings
- Never commit backup files (
*.po~,*.bak, etc.) - checkgit statusbefore committing - 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:
-
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)
-
-
MD056 - Table column count: Table separators must have matching column counts and proper spacing. Use consistent dash lengths that match column header widths.
-
MD009 - No trailing spaces: Remove trailing whitespace from lines
-
MD012 - No multiple consecutive blank lines: Use only single blank lines between sections
-
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, usetxt. -
MD032 - Lists should be surrounded by blank lines: Add a blank line before AND after list blocks, including after bold labels like
**Attributes:**. -
MD034 - Bare URLs: Wrap URLs in angle brackets (e.g.,
<https://example.com>) or use markdown link syntax[text](url). -
Author section formatting: Use a heading (
### Name) instead of bold (**Name**) for the author name to maintain consistent document structure.
Build Pipeline
The build uses npm scripts defined in package.json:
# Full build (CI and local)
npm run build
# Development watch mode
npm run dev
Build steps (in order):
copy:js— Copy Bootstrap JS bundle fromnode_modulestoassets/js/copy:theme-js— Copy theme JS (e.g.,dark-mode.js) fromsrc/js/toassets/js/scss— Compile SCSS (src/scss/) to CSS (assets/css/)postcss— Autoprefixer + cssnano minification →assets/css/style.min.css
CI/CD note: The Gitea workflow uses npm install and npm run build.
Architecture Notes
- Dark mode: Uses Bootstrap 5.3
data-bs-themeattribute on<html>. An inline anti-flash script runs synchronously in<head>(viawp_add_inline_scriptwith'before'), while the fulldark-mode.jsis deferred. Preference stored inlocalStoragekeywp-bootstrap-theme. - Block styles: Registered via
register_block_style()withinline_styleparameter infunctions.php. Dark mode overrides for alert/card styles are insrc/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
.woff2inassets/fonts/. Declared viafontFaceintheme.jsonwithfont-display: swap. - Patterns: PHP files in
patterns/with WordPress block markup and i18n. Hidden patterns (prefixedhidden-) are reusable components not shown in the pattern inserter. - Twig frontend rendering:
TemplateControllerhookstemplate_redirectto 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 viaob_start()/ob_get_clean()and passed to Twig as safe HTML strings. - Navigation menus:
NavWalkerconverts flatwp_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 viacompose.override.yaml(absolute path) for live changes to be visible.
Session History
Session 21 — v1.1.2 Security Audit & Performance Fixes (2026-03-01)
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 twopreg_replacecalls into single regex matching<h2>withwp-block-headingclass, 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 + onegetSidebarData()call. - Transient caching (
inc/Template/ContextBuilder.php+functions.php):getSidebarRecentPosts()andgetSidebarTags()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. Addedctype_xdigit()validation and type hints to all color utility functions.
Files modified:
inc/Block/WidgetRenderer.php— single regex for h2→h4inc/Template/ContextBuilder.php— O(n) tree, sidebar consolidation, transient cachingfunctions.php— hex-to-RGB consolidation, type hints, transient invalidation hooksstyle.css— version bump to 1.1.2CHANGELOG.md,CLAUDE.md— documentation
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-devwith PSR-4autoload-devmapping (WPBootstrap\Tests\→tests/). - WP_HTML_Tag_Processor stub (
tests/Stubs/WpHtmlTagProcessor.php): Functional DOMDocument-based replacement supportingnext_tag()(by tag name or class_name),add_class()(idempotent), andget_updated_html(). Uses full HTML document wrapping for reliable body extraction. - BlockRendererTest (28 tests): All 8 render methods — table classes, striped tables, button variants (default/bg/outline/gradient/unknown), button group flex, img-fluid, search input-group, blockquote+cite, pullquote, list-group, empty content returns.
- 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 testandprebuildhook gatenpm run buildon passing tests. - CI workflow: New
testjob betweenlintandbuild-releasewith 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()andFunctions\when()->alias()enables fast, isolated unit tests without a running WordPress installation. - DOMDocument stub over regex:
WP_HTML_Tag_Processorstub usesDOMDocument/DOMXPathfor reliable HTML parsing. Full document wrapping (<!DOCTYPE><html><body>...) required becauseLIBXML_HTML_NOIMPLIEDprevents<body>creation in PHP 8.4, breakinggetElementsByTagName('body'). is_admin()=trueconstructor bypass: BlockRenderer/WidgetRenderer constructors register WordPress filters. Mockingis_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_NOIMPLIEDwithDOMDocument::loadHTML()on PHP 8.4 does not create a<body>element, causinggetElementsByTagName('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 likeis_singular()that behave differently based on post type argument. spl_object_id()used in the stub's visited-node tracking enables sequentialnext_tag()advancement matching WordPress's forward-only API.
Files created:
phpunit.xml.dist— PHPUnit configurationtests/bootstrap.php— Autoloader + stub loadingtests/Stubs/WpHtmlTagProcessor.php— Functional DOMDocument-based stubtests/Stubs/WpBlock.php— Empty class stubtests/Stubs/WpWidget.php— Empty class stubtests/Unit/Block/BlockRendererTest.php— 28 teststests/Unit/Block/WidgetRendererTest.php— 9 teststests/Unit/Template/NavWalkerTest.php— 14 teststests/Unit/Template/TemplateControllerTest.php— 12 tests
Files modified:
composer.json—require-dev,autoload-devpackage.json—test,prebuildscripts.gitea/workflows/release.yml— test job, exclusions, verification.gitignore—.phpunit.cache/style.css— version bump to 1.1.1CHANGELOG.md,README.md,CLAUDE.md— documentation
Session 19 — v1.1.0 Block Renderer, Widget Renderer & Sidebar Post Layout (2026-02-28)
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-blockrender_block_{$name}filters inject Bootstrap classes into 8 core block types usingWP_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 viawp_bootstrap_block_renderer_blocksfilter. - WidgetRenderer (
inc/Block/WidgetRenderer.php):dynamic_sidebar_paramsfilter wraps sidebar widgets in Bootstrap.card > .card-bodystructure.widget_block_contentfilter downgrades block widget<h2 class="wp-block-heading">to<h4>via preg_replace. Widget ID and type-specific classes extracted from the already-processedbefore_widgetstring. - 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 theis-style-list-groupstyle 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 usesrow-cols-md-2for the narrower column. - Post template selection (
inc/Template/TemplateController.php): Posts default to sidebar layout;page-full-widthtemplate slug maps to full-width. - Sidebar data always loaded for posts (
inc/Template/ContextBuilder.php):getSidebarData()called unconditionally foris_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_Processorover 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_titleentirely when a widget has no title. Usingcard-headerfor 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 runssprintfonbefore_widgetBEFORE thedynamic_sidebar_paramsfilter 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 $instancetype hint is too strict for manualapply_filters()calls — WordPress passesnullwhen filters are invoked outside the block rendering pipeline. Use?WP_Block $instance = null.- Block widgets nest content inside
.wp-block-groupwrappers. CSS selectors like.card-body > ulwon't match — need.card-body > .wp-block-group > ulfor flush list positioning. widget_block_contentfilter (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-wrapperas the input+button container — adding.input-groupto this element creates a proper Bootstrap input-group. @extend .btn; @extend .btn-primaryin SCSS works for search submit buttons because Bootstrap is imported before the widgets partial in the SCSS cascade.
Files created:
inc/Block/BlockRenderer.php— 8 block handlersinc/Block/WidgetRenderer.php— card wrapper + heading downgradesrc/scss/_widgets.scss— widget Bootstrap stylingviews/pages/single-sidebar.html.twig— two-column post template
Files modified:
functions.php— init hooks for both renderers, list-group block stylesrc/scss/style.scss— widgets importinc/Template/TemplateController.php— post template selection logicinc/Template/ContextBuilder.php— always load sidebar for postsstyle.css— version bump to 1.1.0CHANGELOG.md,README.md,CLAUDE.md— documentation
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.
Session 17 — v1.0.11 Offcanvas Navigation & User Context (2026-02-28)
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 frompartials/header.html.twig(collapse) topartials/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 separated-none d-lg-blockwrapper. - User context in ContextBuilder (
inc/Template/ContextBuilder.php): NewgetUserData()method providinguser.logged_in,user.display_name,user.avatar(rendered<img>withrounded-circleclass), anduser.account_url(WooCommerce My Account or WP admin profile fallback). - Admin bar offcanvas overlap fix (
functions.php): Inline CSS injected viawp_add_inline_style()whenis_admin_bar_showing()is true. Addspadding-top: var(--wp-admin--admin-bar--height, 32px)to.offcanvasso the offcanvas content clears the admin bar.
Files modified:
views/base.html.twig— header include changed to offcanvas variantviews/partials/header-offcanvas.html.twig— user avatar header, dark mode toggle in footerinc/Template/ContextBuilder.php—getUserData()method,userkey in contextfunctions.php— admin bar offcanvas padding inline stylestyle.css— version bump to 1.0.11CHANGELOG.md— v1.0.11 entry
Key learnings:
- Bootstrap offcanvas inside
navbar-expand-lgusesposition: fixed; top: 0which is covered by the WordPress admin bar (z-index: 99999). Since the offcanvas z-index (1045) is lower, adjustingtopalone doesn't help visually —padding-topon 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--heightCSS 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$argsarray where CSS classes can be passed via theclasskey, 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 6get_the_title()callsstyle.css— version bump to 1.0.10CHANGELOG.md— v1.0.10 entry
Session 15 — v1.0.9 Performance Optimization (2026-02-19)
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 bywp_bootstrap_variation_css_+md5(get_stylesheet()). Previously the palette loop and CSS string building executed on every frontend request. Transient is invalidated onswitch_themeandsave_post_wp_global_styleshooks so Design Editor changes apply immediately. - Twig
auto_reloadgated behindWP_DEBUG(inc/Twig/TwigService.php): Hardcodedauto_reload => truecaused Twig tostat()each compiled template file on every request to detect source file changes. Changed toauto_reload => WP_DEBUGso 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 CSSinc/Twig/TwigService.php—auto_reload => WP_DEBUGstyle.css— version bump to 1.0.9CHANGELOG.md— v1.0.9 entry
Session 14 — v1.0.8 Security Audit & Hardening (2026-02-19)
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 withwp_kses_post()inContextBuilder::getArchiveData(). Same applied toget_the_archive_title(). - Comment author injection (Low, defense-in-depth):
comment_authorandcomment_author_urlwere passed to Twig as raw database values. Fixed:esc_html()applied to author name,esc_url()applied to author URL inContextBuilder::buildCommentTree(). Template updated to output pre-escaped URL via|rawrather than callingesc_url()in Twig. - Dark mode localStorage whitelist (Medium):
getPreferredTheme()returned any stored value without validation, allowing attribute injection if a malicious script wrote tolocalStorage. 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 inTwigServicelacked['is_safe' => ['html']], meaning any future autoescape enablement would cause double-encoding. Fixed: all three now carry theis_safedeclaration.
Confirmed secure (no action needed):
- All
|rawfilter 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 (
$wpdbnot used directly; all data via WordPress functions) TemplateControllererror handling:\Throwablecaught, logged, and gated behindWP_DEBUGdo_shortcode()andwp_kses_post()Twig functions correctly markedis_safewp_head(),wp_footer(),body_class()Twig functions correctly use output buffering +is_safe
Key learnings:
- WordPress Twig themes should not enable
autoescape => 'html'globally:get_the_title()applieswptexturize()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 withoutis_safe => htmlsilently creates a double-encoding trap: calling{{ esc_url(url) }}with autoescape on would produce&amp;instead of&. - Added
.markdownlint.jsondisabling MD024 (duplicate headings, expected in changelogs) and MD013 (line length).
Files modified:
inc/Template/ContextBuilder.php— archive data sanitization, comment field escapinginc/Twig/TwigService.php—is_safe => htmlon three escaping functionsviews/partials/comment-item.html.twig— use pre-escaped author URLsrc/js/dark-mode.js— localStorage whitelistassets/js/dark-mode.js— rebuilt compiled outputstyle.css— version bump to 1.0.8CHANGELOG.md— v1.0.8 entry.markdownlint.json— created
Session 13 — v1.0.5 Translation Files (2026-02-11)
Completed: Standardized translation file naming and added 11 new locale translations.
What was done:
- Renamed all .po files to use
wp-bootstrap-prefix for WordPress text domain convention - Previously: mixed naming (some with prefix like
wp-bootstrap-en_GB.po, some without likede_CH.po) - Now: all 13 files follow
wp-bootstrap-{locale}.popattern - Compiled all 13 .po files to .mo for local development
- Added 11 new locales: de_CH_informal, de_DE, de_DE_informal, en_GB, es_ES, fr_CH, it_CH, it_IT, nl_NL, pl_PL, pt_PT
Files renamed:
de_CH.po→wp-bootstrap-de_CH.pode_CH_informal.po→wp-bootstrap-de_CH_informal.pode_DE.po→wp-bootstrap-de_DE.pode_DE_informal.po→wp-bootstrap-de_DE_informal.poes_ES.po→wp-bootstrap-es_ES.pofr_CH.po→wp-bootstrap-fr_CH.pofr_FR.po→wp-bootstrap-fr_FR.poit_CH.po→wp-bootstrap-it_CH.poit_IT.po→wp-bootstrap-it_IT.popt_PT.po→wp-bootstrap-pt_PT.po
Key learnings:
- WordPress expects translation files named
{text-domain}-{locale}.po(e.g.,wp-bootstrap-de_CH.po) load_theme_textdomain()loads files matching this pattern from thelanguages/directory- Files without the text domain prefix would not be loaded by WordPress
Session 12 — v1.0.4 Template Render Filter (2026-02-11)
Completed: Added wp_bootstrap_should_render_template filter to TemplateController::render() for clean plugin/theme separation.
What was added:
- New
wp_bootstrap_should_render_templatefilter at the top ofTemplateController::render()— returnstrueby default, but plugins can returnfalseto 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
TemplateControllerracing to render first - Theme remains 100% standalone — the filter is a no-op when no plugin hooks into it
Key learnings:
- WordPress
template_redirecthook 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
Session 11 — v1.0.3 Conditional Page Title (2026-02-11)
Completed: Made <h1> on page template conditional to prevent double headings when plugins provide their own titles.
What was fixed:
views/pages/page.html.twignow wraps<h1>{{ post.title }}</h1>in{% if post.title is not empty %}guard- When a plugin passes empty
post.titleviarender_via_theme_twig(), the theme's<h1>is skipped - Prevents duplicate headings on pages where plugin templates render their own
<h1>with richer context (icons, badges, meta)
Key learnings:
- Plugins that delegate rendering to the parent theme via
TwigServiceshould be able to opt out of the theme's<h1>by passing emptypost.title - The
is not emptyTwig test correctly handles bothnulland empty string''
Session 10 — v1.0.2 Title Tag Fix (2026-02-10)
Completed: Fixed missing HTML <title> tag on all pages rendered by the theme's Twig pipeline.
What was fixed:
- Added
add_theme_support('title-tag')towp_bootstrap_setup()infunctions.php
Root cause:
- The theme's
base.html.twigcalls{{ wp_head() }}which fires thewp_headaction - WordPress hooks
_wp_render_title_tag()towp_headat priority 1, which outputs the<title>tag - However, this hook only fires when the theme declares
add_theme_support('title-tag') - The theme never made this declaration, so
wp_head()output included styles and scripts but no<title>element - All pages rendered by
TemplateController(viabase.html.twig) were affected
Key learnings:
add_theme_support('title-tag')is required even for themes that renderwp_head()via Twig — WordPress does not output<title>without it- The absence of a
<title>tag is invisible in the rendered page but affects SEO, browser tab display, and bookmarking - This support declaration has been standard since WordPress 4.1 and should always be included in
after_setup_theme
Session 9 — v1.0.1 Bootstrap Icons (2026-02-09)
Completed: Bootstrap Icons web font integration via SCSS build pipeline.
What was built:
- Added
bootstrap-iconsnpm dependency (v1.13.1) - Imported Bootstrap Icons SCSS in both
style.scssandeditor-style.scss - Added
$bootstrap-icons-font-srcvariable override in_variables.scssto point@font-faceatassets/fonts/ - Added
copy:iconsnpm script to copy.woff/.woff2font files fromnode_modulestoassets/fonts/ - Updated
buildscript to includecopy:iconsstep
Key learnings:
- Bootstrap Icons SCSS uses
$bootstrap-icons-font-srcto allow overriding the@font-facesrcdeclaration — set it before the import to control font file paths - The existing
--load-path=node_modulesSass flag resolves@import "bootstrap-icons/font/bootstrap-icons"without any extra configuration - Font files (
.woff2at 131KB,.woffat 176KB) are small enough to serve as web fonts without performance concern
Session 8 — v1.0.0 Release (2026-02-08)
Completed: Sidebar widget area registration, Twig widget rendering with fallback, documentation refresh, v1.0.0 release.
What was built:
register_sidebar()forprimary-sidebarwidget area with Bootstrap-styled wrapper markup- Widget area rendering in
ContextBuilder::getSidebarData()viaob_start()+dynamic_sidebar()with fallback to built-in content - Twig sidebar template conditional: renders WordPress widgets when assigned, falls back to recent posts/search/tags otherwise
- Updated README.md with accurate feature counts (15 variations, 41 patterns, 3 translations, accessibility, RTL, widget area)
- Updated all translation files (.pot, de_CH.po, fr_FR.po) with widget area strings
Key learnings:
is_active_sidebar()returns true only when widgets are assigned to the area, making it the right condition for fallback logicdynamic_sidebar()outputs widget HTML directly, soob_start()/ob_get_clean()is needed to capture it for Twig- Widget area
before_widget/after_widgetmarkup should use Bootstrap utility classes (widget mb-4) for consistent spacing - Widget title markup (
before_title/after_title) should match existing sidebar heading styles (sidebar-heading h6 text-uppercase fw-semibold)
Session 7 — v0.3.2/v0.3.3 Dark Mode & Style Variation Bridge (2026-02-08)
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.
What was built:
- Style variation bridge function (
wp_bootstrap_variation_colors) that maps WordPress palette colors to Bootstrap CSS custom properties viawp_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() - Variation detection comparing
themeorigin palette against hardcoded base defaults (base, contrast, primary) - Dark mode body override in
_custom.scssusing!importantto defeat WordPress global styles specificity - Broad dark mode rules for all native form elements (
select,input,textarea) to catch plugin-generated controls - Fixed
footer-columns.html.twigto use semanticbg-body-tertiaryinstead of hardcodedbg-dark text-light
Key learnings:
- WordPress puts style variation colors in the
themepalette origin, NOTcustom--wp_get_global_settings(['color', 'palette', 'theme'])returns the base theme.json merged with the active variation - The
custompalette origin contains user manual edits from the Site Editor, but its data structure may lack expectedslug/colorkeys - To detect an active variation, compare
themeorigin colors against known base theme.json defaults rather than checking for slugs incustom - WordPress
theme.jsonstyles.colorgeneratesbody { background-color: var(--wp--preset--color--base) }directly onbody, which overrides inherited CSS variables fromhtml[data-bs-theme="dark"]-- removingstyles.colorfrom theme.json is the cleanest fix - CSS variables defined directly on
bodybeat inherited values fromhtmldue to specificity, requiring!importantonhtml[data-bs-theme="dark"] bodyto 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
Session 6 — v0.3.1 Style Variations (2026-02-08)
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
lightanddarkslugs to maintain proper surface hierarchy - Button and link hover states in dark palettes need explicit
colorandtextoverrides since the default theme.json assumes light base
Session 5 — v0.3.0 Polish (2026-02-08)
Completed: Accessibility audit and fixes, security hardening, performance optimization, RTL language support, French translation, inline style cleanup.
What was built:
- Skip-to-content link in
base.html.twigwith visually-hidden CSS class (visible on focus) - ARIA labels on all
<nav>elements across 4 header variants, 2 footer variants, and sidebar aria-current="page"on active dropdown items in all header variantsloading="lazy"on post thumbnails, card images, and comment avatars- Screen reader announcement (
aria-live="polite") indark-mode.jsfor theme toggle - Font preload
<link>tags for Inter and Lora.woff2files viawp_headpriority 1 - RTL stylesheet (
src/scss/rtl.scss) conditionally loaded whenis_rtl()is true - Logical CSS properties (
border-inline-start,padding-inline-start) replacing physical directions in block styles - French translation (
fr_FR.po) covering all ~216 translatable strings - CSS classes replacing inline styles:
.post-thumbnail,.card-thumbnail,.sidebar-heading,.hero-overlay - XSS fix in search template:
search_query|e('html')before|rawoutput - Explicit
esc_url()on comment author URLs in Twig - Updated
.potandde_CH.powith 4 new accessibility strings - RTL build step added to npm scripts
Key learnings:
- WordPress
is_rtl()detects RTL locales, allowing conditional stylesheet loading without doubling CSS bundle size - 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|rawHTML to prevent XSS --search_query|e('html')inside a|format()call - Font preloading via
wp_headat priority 1 ensures preload hints appear before render-blocking stylesheets aria-live="polite"withrole="status"announces dynamic changes to screen readers without interrupting current reading flow
Session 4 — v0.2.0 Design Editor (2026-02-08)
Completed: Full Design Editor compatibility, custom block categories, page templates, header/footer/navigation variations.
What was built:
- Enhanced editor stylesheet importing full Bootstrap SCSS for WYSIWYG fidelity
- Editor overrides SCSS (
_editor-overrides.scss) for alignment and spacing - Bootstrap JS loaded in block editor via
enqueue_block_editor_assets - 3 custom block categories (
block_categories_allfilter): Bootstrap Layout, Components, Navigation - 3 custom pattern categories: Layout, Components, Navigation
- 6 layout/component patterns: container, 2-col, 3-col, full-width section, card group, accordion
- 3 full-page patterns: about, services, contact
- 4 custom page templates (FSE + Twig): landing, full-width, hero, sidebar
- 2 header variations (FSE parts + patterns + Twig): centered, transparent
- 2 footer variations (FSE parts + patterns + Twig): minimal, multi-column
- 2 navigation patterns: dark navbar, offcanvas
- Offcanvas navigation Twig partial with Bootstrap offcanvas component
- Twig block inheritance in
base.html.twigfor header/footer variant overrides - Header/footer variant support via
get_theme_mod()inContextBuilder - Custom page template routing via
get_page_template_slug()inTemplateController - Shadow presets, aspect ratios, custom layout values in
theme.json - Transparent header and offcanvas dark mode SCSS styles
- Updated translations (
.potandde_CH.po) with ~70 new translatable strings
Key learnings:
- WordPress
block_categories_allfilter (block inserter categories) andregister_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 modifyingbase.html.twig get_page_template_slug()returns the custom template slug assigned in the page editor, used inTemplateControllerfor routing to the correct Twig template- Bootstrap SCSS deprecation warnings (Dart Sass 3.0 migration) are upstream issues, not blocking — the build succeeds
Session 3 — v0.1.1 Bootstrap Frontend Rendering (2026-02-08)
Completed: Full Twig-based Bootstrap 5 frontend rendering, replacing FSE block markup on the public-facing site.
What was built:
TemplateControllerclass hookingtemplate_redirectto render Twig templates for all page typesContextBuilderclass gathering WordPress data (posts, menus, pagination, comments, sidebar, archive info) into structured arraysNavWalkerclass 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
TwigServicewith 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 viaob_start()/ob_get_clean()for use in Twig, and marked withis_safe => html - With
optimize-autoloader: trueincomposer.json, new PSR-4 classes requirecomposer dump-autoloadto regenerate the static classmap - Docker bind mounts require absolute paths in
compose.override.yaml-- relative paths create empty directories - Post content is rendered via
apply_filters('the_content', get_the_content())which processes Gutenberg blocks into standard HTML that Bootstrap CSS handles natively
Session 2 — v0.1.0 Core Theme (2026-02-08)
Completed: Full v0.1.0 milestone implementation.
What was built:
- 16 block patterns across 7 new categories (hero, features, CTA, testimonials, pricing, contact, text)
- Dark mode toggle with SVG sun/moon icons, localStorage persistence,
prefers-color-schemedetection - 17 custom block styles mapping Bootstrap components to WordPress blocks
- 4 style variations: Ocean, Forest, Sunset, Midnight
- Sidebar template part + "Blog with Sidebar" custom template
- Inter + Lora variable web fonts with Display font size
Key learnings:
- CI uses
npm install/npm run build—yarnis not available in the CI runner - 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 intheme.jsonfontFacedeclarations - 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)
Completed: Initial theme scaffolding, Bootstrap 5 integration, SASS pipeline, Twig setup, CI/CD workflow, basic FSE templates and patterns, i18n support.