3 Commits
v1.1.1 ... main

Author SHA1 Message Date
6c8526d2a5 security: add |esc_url to all template URLs, register escape Twig filters (v1.1.3)
All checks were successful
Create Release Package / PHP Lint (push) Successful in 50s
Create Release Package / PHPUnit Tests (push) Successful in 44s
Create Release Package / Build Release (push) Successful in 2m17s
5th OWASP Top-10 pass: added |esc_url filter to all unescaped URL outputs
across 8 Twig template partials (headers, footers, search, comments).
Registered esc_html, esc_attr, esc_url as Twig filters with is_safe option.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:34:41 +01:00
02689f687f add escape twig functions as twig filters 2026-03-01 06:22:24 +01:00
17728e81d9 Security audit fixes: regex hardening, performance, and code quality (v1.1.2)
All checks were successful
Create Release Package / PHP Lint (push) Successful in 1m32s
Create Release Package / PHPUnit Tests (push) Successful in 2m35s
Create Release Package / Build Release (push) Successful in 2m36s
- 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>
2026-03-01 01:02:12 +01:00
15 changed files with 147 additions and 67 deletions

View File

@@ -2,6 +2,32 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [1.1.3] - 2026-03-07
### Security
- **Template output escaping**: Added `|esc_url` filter to all unescaped URL outputs across 8 Twig template partials — `header.html.twig`, `header-offcanvas.html.twig`, `header-transparent.html.twig`, `header-centered.html.twig`, `footer.html.twig`, `footer-columns.html.twig`, `search-form.html.twig`, `comment-item.html.twig`. Covers `site.url`, `item.url`, `child.url`, `user.account_url`, `comment.author_url`, and `comment.edit_url`.
### Added
- **Twig escape filters** (`TwigService.php`): Registered `esc_html`, `esc_attr`, and `esc_url` as Twig filters with `['is_safe' => ['html']]` to prevent double-encoding. Complements existing `wpautop` and `wp_kses_post` filters.
## [1.1.2] - 2026-03-01
### Security
- **WidgetRenderer regex hardening**: Combined two separate `preg_replace` calls for h2→h4 heading downgrade into a single regex that only matches `<h2>` elements with the `wp-block-heading` class. The previous approach replaced all `</h2>` tags unconditionally, risking mismatched tags if a widget contained non-block h2 elements.
### Performance
- **O(n) comment tree building** (`ContextBuilder`): Replaced O(n²) recursive scan with a parent-indexed lookup map built in a single pass. Each recursion level now iterates only direct children instead of all comments.
- **Consolidated sidebar queries** (`ContextBuilder`): Merged three separate sidebar detection branches (`is_home`, `is_page`+sidebar, `is_singular` post) into a single boolean check with one `getSidebarData()` call, eliminating up to 2 redundant calls per request.
- **Transient caching for sidebar data** (`ContextBuilder`): `getSidebarRecentPosts()` and `getSidebarTags()` results cached in WordPress transients (1 hour TTL). Invalidation hooks on `save_post` (recent posts) and `create/edit/delete_post_tag` (tags).
### Changed
- **Hex-to-RGB consolidation** (`functions.php`): `wp_bootstrap_hex_to_rgb()` now delegates to `wp_bootstrap_hex_to_rgb_array()` instead of duplicating hex parsing logic. Added `ctype_xdigit()` validation and return type hints to all color utility functions.
## [1.1.1] - 2026-02-28 ## [1.1.1] - 2026-02-28
### Added ### Added

View File

@@ -34,7 +34,7 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
**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. **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.1**. See `PLAN.md` for details. Current version is **v1.1.2**. See `PLAN.md` for details.
## Technical Stack ## Technical Stack
@@ -234,6 +234,26 @@ Build steps (in order):
## Session History ## 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 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
- `inc/Template/ContextBuilder.php` — O(n) tree, sidebar consolidation, transient caching
- `functions.php` — hex-to-RGB consolidation, type hints, transient invalidation hooks
- `style.css` — version bump to 1.1.2
- `CHANGELOG.md`, `CLAUDE.md` — documentation
### Session 20 — v1.1.1 PHPUnit Test Suite (2026-02-28) ### 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. **Completed:** PHPUnit test suite with 64 unit tests and 107 assertions, CI integration, and build pipeline gating.

View File

@@ -320,6 +320,24 @@ add_action( 'save_post_wp_global_styles', function () {
delete_transient( 'wp_bootstrap_variation_css_' . md5( get_stylesheet() ) ); delete_transient( 'wp_bootstrap_variation_css_' . md5( get_stylesheet() ) );
} ); } );
/**
* Invalidate sidebar transient caches when content changes.
*
* @since 1.2.0
*/
add_action( 'save_post', function () {
delete_transient( 'wp_bootstrap_sidebar_recent_4' );
} );
add_action( 'create_post_tag', function () {
delete_transient( 'wp_bootstrap_sidebar_tags_15' );
} );
add_action( 'edit_post_tag', function () {
delete_transient( 'wp_bootstrap_sidebar_tags_15' );
} );
add_action( 'delete_post_tag', function () {
delete_transient( 'wp_bootstrap_sidebar_tags_15' );
} );
/** /**
* Build Bootstrap surface CSS variables for a given background/foreground pair. * Build Bootstrap surface CSS variables for a given background/foreground pair.
* *
@@ -378,17 +396,12 @@ endif;
* @return string RGB triplet (e.g. "13,110,253") or empty string on failure. * @return string RGB triplet (e.g. "13,110,253") or empty string on failure.
*/ */
if ( ! function_exists( 'wp_bootstrap_hex_to_rgb' ) ) : if ( ! function_exists( 'wp_bootstrap_hex_to_rgb' ) ) :
function wp_bootstrap_hex_to_rgb( $hex ) { function wp_bootstrap_hex_to_rgb( string $hex ): string {
$hex = ltrim( $hex, '#' ); $rgb = wp_bootstrap_hex_to_rgb_array( $hex );
if ( strlen( $hex ) === 3 ) { if ( ! $rgb ) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
if ( strlen( $hex ) !== 6 ) {
return ''; return '';
} }
return hexdec( substr( $hex, 0, 2 ) ) . ',' return implode( ',', $rgb );
. hexdec( substr( $hex, 2, 2 ) ) . ','
. hexdec( substr( $hex, 4, 2 ) );
} }
endif; endif;
@@ -403,7 +416,7 @@ endif;
* @return string Resulting hex color. * @return string Resulting hex color.
*/ */
if ( ! function_exists( 'wp_bootstrap_mix_hex' ) ) : if ( ! function_exists( 'wp_bootstrap_mix_hex' ) ) :
function wp_bootstrap_mix_hex( $color1, $color2, $weight ) { function wp_bootstrap_mix_hex( string $color1, string $color2, float $weight ): string {
$c1 = wp_bootstrap_hex_to_rgb_array( $color1 ); $c1 = wp_bootstrap_hex_to_rgb_array( $color1 );
$c2 = wp_bootstrap_hex_to_rgb_array( $color2 ); $c2 = wp_bootstrap_hex_to_rgb_array( $color2 );
if ( ! $c1 || ! $c2 ) { if ( ! $c1 || ! $c2 ) {
@@ -425,12 +438,12 @@ endif;
* @return array|false Array of [r, g, b] or false on failure. * @return array|false Array of [r, g, b] or false on failure.
*/ */
if ( ! function_exists( 'wp_bootstrap_hex_to_rgb_array' ) ) : if ( ! function_exists( 'wp_bootstrap_hex_to_rgb_array' ) ) :
function wp_bootstrap_hex_to_rgb_array( $hex ) { function wp_bootstrap_hex_to_rgb_array( string $hex ): array|false {
$hex = ltrim( $hex, '#' ); $hex = ltrim( $hex, '#' );
if ( strlen( $hex ) === 3 ) { if ( strlen( $hex ) === 3 ) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
} }
if ( strlen( $hex ) !== 6 ) { if ( strlen( $hex ) !== 6 || ! ctype_xdigit( $hex ) ) {
return false; return false;
} }
return array( return array(
@@ -450,7 +463,7 @@ endif;
* @return float Relative luminance. * @return float Relative luminance.
*/ */
if ( ! function_exists( 'wp_bootstrap_relative_luminance' ) ) : if ( ! function_exists( 'wp_bootstrap_relative_luminance' ) ) :
function wp_bootstrap_relative_luminance( $hex ) { function wp_bootstrap_relative_luminance( string $hex ): float {
$rgb = wp_bootstrap_hex_to_rgb_array( $hex ); $rgb = wp_bootstrap_hex_to_rgb_array( $hex );
if ( ! $rgb ) { if ( ! $rgb ) {
return 0.0; return 0.0;

View File

@@ -98,15 +98,12 @@ class WidgetRenderer
return $content; return $content;
} }
// Replace <h2 with <h4 and </h2> with </h4> for widget headings. // Replace <h2 ... wp-block-heading ...>...</h2> with <h4> pairs.
// Single regex ensures only headings with wp-block-heading class are
// downgraded, preventing mismatched tags if a widget contains other h2s.
$content = preg_replace( $content = preg_replace(
'/<h2(\s+class="[^"]*wp-block-heading[^"]*")/', '/<h2(\s+class="[^"]*wp-block-heading[^"]*"[^>]*)>(.*?)<\/h2>/s',
'<h4$1', '<h4$1>$2</h4>',
$content
);
$content = preg_replace(
'/<\/h2>/',
'</h4>',
$content $content
); );

View File

@@ -58,7 +58,9 @@ class ContextBuilder
$context['search_query'] = get_search_query(); $context['search_query'] = get_search_query();
} }
// Sidebar layout detection. // Sidebar: determine once whether the current page needs sidebar data.
$needsSidebar = false;
if (is_home()) { if (is_home()) {
$pageId = (int) get_option('page_for_posts'); $pageId = (int) get_option('page_for_posts');
if ($pageId) { if ($pageId) {
@@ -67,19 +69,14 @@ class ContextBuilder
$context['layout'] = 'sidebar'; $context['layout'] = 'sidebar';
} }
} }
$context['sidebar'] = $this->getSidebarData(); $needsSidebar = true;
} elseif (is_singular('post')) {
$needsSidebar = true;
} elseif (is_page() && get_page_template_slug() === 'page-sidebar') {
$needsSidebar = true;
} }
// Sidebar data for pages using the "Page with Sidebar" template. if ($needsSidebar) {
if (is_page()) {
$slug = get_page_template_slug();
if ($slug === 'page-sidebar') {
$context['sidebar'] = $this->getSidebarData();
}
}
// Posts always get sidebar data (sidebar is the default layout).
if (is_singular('post')) {
$context['sidebar'] = $this->getSidebarData(); $context['sidebar'] = $this->getSidebarData();
} }
@@ -309,21 +306,26 @@ class ContextBuilder
/** /**
* Build a nested comment tree from flat comments. * Build a nested comment tree from flat comments.
*
* Uses a parent-indexed lookup map for O(n) performance instead of
* scanning all comments at each recursion level (O(n^2)).
*/ */
private function buildCommentTree(array $comments, int $parentId = 0): array private function buildCommentTree(array $comments, int $parentId = 0, ?array $index = null): array
{ {
if ($index === null) {
$index = [];
foreach ($comments as $comment) {
$parent = (int) $comment->comment_parent;
$index[$parent][] = $comment;
}
}
$tree = []; $tree = [];
foreach ($comments as $comment) { foreach ($index[$parentId] ?? [] as $comment) {
if ((int) $comment->comment_parent !== $parentId) {
continue;
}
$tree[] = [ $tree[] = [
'id' => (int) $comment->comment_ID, 'id' => (int) $comment->comment_ID,
// Escape at source — comment_author is user-supplied, store as safe text.
'author' => esc_html($comment->comment_author), 'author' => esc_html($comment->comment_author),
// esc_url() strips dangerous schemes (javascript:, data:) and encodes for HTML.
'author_url' => esc_url($comment->comment_author_url), 'author_url' => esc_url($comment->comment_author_url),
'avatar_url' => get_avatar_url($comment, ['size' => 40]), 'avatar_url' => get_avatar_url($comment, ['size' => 40]),
'date' => get_comment_date('', $comment), 'date' => get_comment_date('', $comment),
@@ -336,7 +338,7 @@ class ContextBuilder
'depth' => 1, 'depth' => 1,
'max_depth' => get_option('thread_comments_depth', 5), 'max_depth' => get_option('thread_comments_depth', 5),
], $comment), ], $comment),
'children' => $this->buildCommentTree($comments, (int) $comment->comment_ID), 'children' => $this->buildCommentTree($comments, (int) $comment->comment_ID, $index),
]; ];
} }
@@ -452,9 +454,17 @@ class ContextBuilder
/** /**
* Get recent posts for sidebar. * Get recent posts for sidebar.
*
* Cached via transient for 1 hour; invalidated on save_post.
*/ */
private function getSidebarRecentPosts(int $count = 4): array private function getSidebarRecentPosts(int $count = 4): array
{ {
$transient_key = 'wp_bootstrap_sidebar_recent_' . $count;
$cached = get_transient($transient_key);
if (false !== $cached) {
return $cached;
}
$query = new \WP_Query([ $query = new \WP_Query([
'posts_per_page' => $count, 'posts_per_page' => $count,
'orderby' => 'date', 'orderby' => 'date',
@@ -474,14 +484,23 @@ class ContextBuilder
wp_reset_postdata(); wp_reset_postdata();
} }
set_transient($transient_key, $posts, HOUR_IN_SECONDS);
return $posts; return $posts;
} }
/** /**
* Get tags for sidebar tag cloud. * Get tags for sidebar tag cloud.
*
* Cached via transient for 1 hour; invalidated on tag changes.
*/ */
private function getSidebarTags(int $count = 15): array private function getSidebarTags(int $count = 15): array
{ {
$transient_key = 'wp_bootstrap_sidebar_tags_' . $count;
$cached = get_transient($transient_key);
if (false !== $cached) {
return $cached;
}
$tags = get_tags([ $tags = get_tags([
'number' => $count, 'number' => $count,
'orderby' => 'count', 'orderby' => 'count',
@@ -501,6 +520,7 @@ class ContextBuilder
]; ];
} }
set_transient($transient_key, $items, HOUR_IN_SECONDS);
return $items; return $items;
} }

View File

@@ -165,5 +165,10 @@ class TwigService
{ {
$this->twig->addFilter(new TwigFilter('wpautop', 'wpautop', ['is_safe' => ['html']])); $this->twig->addFilter(new TwigFilter('wpautop', 'wpautop', ['is_safe' => ['html']]));
$this->twig->addFilter(new TwigFilter('wp_kses_post', 'wp_kses_post', ['is_safe' => ['html']])); $this->twig->addFilter(new TwigFilter('wp_kses_post', 'wp_kses_post', ['is_safe' => ['html']]));
// Escaping filters — same functions registered above, but as filters for |esc_html syntax.
$this->twig->addFilter(new TwigFilter('esc_html', 'esc_html', ['is_safe' => ['html']]));
$this->twig->addFilter(new TwigFilter('esc_attr', 'esc_attr', ['is_safe' => ['html']]));
$this->twig->addFilter(new TwigFilter('esc_url', 'esc_url', ['is_safe' => ['html']]));
} }
} }

View File

@@ -7,7 +7,7 @@ Description: A modern WordPress Block Theme built from scratch with Bootstrap 5.
Requires at least: 6.7 Requires at least: 6.7
Tested up to: 6.7 Tested up to: 6.7
Requires PHP: 8.3 Requires PHP: 8.3
Version: 1.1.1 Version: 1.1.3
License: GNU General Public License v2 or later License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: wp-bootstrap Text Domain: wp-bootstrap

View File

@@ -7,8 +7,7 @@
<div class="d-flex align-items-center gap-2 mb-1"> <div class="d-flex align-items-center gap-2 mb-1">
<strong class="small"> <strong class="small">
{% if comment.author_url %} {% if comment.author_url %}
{# author_url is pre-escaped with esc_url() in ContextBuilder #} <a href="{{ comment.author_url|esc_url }}" class="text-decoration-none text-body" rel="nofollow">
<a href="{{ comment.author_url|raw }}" class="text-decoration-none text-body" rel="nofollow">
{{ comment.author }} {{ comment.author }}
</a> </a>
{% else %} {% else %}
@@ -19,7 +18,7 @@
{{ comment.date }} {{ comment.date }}
</time> </time>
{% if comment.edit_url %} {% if comment.edit_url %}
<a href="{{ comment.edit_url }}" class="text-body-secondary small">{{ __('Edit') }}</a> <a href="{{ comment.edit_url|esc_url }}" class="text-body-secondary small">{{ __('Edit') }}</a>
{% endif %} {% endif %}
</div> </div>
<div class="comment-content small"> <div class="comment-content small">

View File

@@ -13,7 +13,7 @@
<ul class="list-unstyled"> <ul class="list-unstyled">
{% for item in footer_menu %} {% for item in footer_menu %}
<li class="mb-1"> <li class="mb-1">
<a href="{{ item.url }}" class="text-body-secondary text-decoration-none"> <a href="{{ item.url|esc_url }}" class="text-body-secondary text-decoration-none">
{{ item.title }} {{ item.title }}
</a> </a>
</li> </li>

View File

@@ -11,7 +11,7 @@
<ul class="list-unstyled"> <ul class="list-unstyled">
{% for item in footer_menu %} {% for item in footer_menu %}
<li> <li>
<a href="{{ item.url }}" class="text-body-secondary text-decoration-none"> <a href="{{ item.url|esc_url }}" class="text-body-secondary text-decoration-none">
{{ item.title }} {{ item.title }}
</a> </a>
</li> </li>

View File

@@ -1,7 +1,7 @@
<header> <header>
<nav class="navbar navbar-expand-lg bg-body-tertiary" aria-label="{{ __('Primary navigation') }}"> <nav class="navbar navbar-expand-lg bg-body-tertiary" aria-label="{{ __('Primary navigation') }}">
<div class="container flex-column"> <div class="container flex-column">
<a class="navbar-brand fw-bold mb-2" href="{{ site.url }}"> <a class="navbar-brand fw-bold mb-2" href="{{ site.url|esc_url }}">
{{ site.name }} {{ site.name }}
</a> </a>
{% if site.description %} {% if site.description %}
@@ -21,7 +21,7 @@
{% if item.children|length > 0 %} {% if item.children|length > 0 %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle{{ item.active ? ' active' : '' }}" <a class="nav-link dropdown-toggle{{ item.active ? ' active' : '' }}"
href="{{ item.url }}" role="button" href="{{ item.url|esc_url }}" role="button"
data-bs-toggle="dropdown" aria-expanded="false"> data-bs-toggle="dropdown" aria-expanded="false">
{{ item.title }} {{ item.title }}
</a> </a>
@@ -29,7 +29,7 @@
{% for child in item.children %} {% for child in item.children %}
<li> <li>
<a class="dropdown-item{{ child.active ? ' active' : '' }}" <a class="dropdown-item{{ child.active ? ' active' : '' }}"
href="{{ child.url }}" href="{{ child.url|esc_url }}"
{% if child.active %}aria-current="page"{% endif %} {% if child.active %}aria-current="page"{% endif %}
{% if child.target %}target="{{ child.target }}"{% endif %}> {% if child.target %}target="{{ child.target }}"{% endif %}>
{{ child.title }} {{ child.title }}
@@ -41,7 +41,7 @@
{% else %} {% else %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{{ item.active ? ' active' : '' }}" <a class="nav-link{{ item.active ? ' active' : '' }}"
href="{{ item.url }}" href="{{ item.url|esc_url }}"
{% if item.active %}aria-current="page"{% endif %} {% if item.active %}aria-current="page"{% endif %}
{% if item.target %}target="{{ item.target }}"{% endif %}> {% if item.target %}target="{{ item.target }}"{% endif %}>
{{ item.title }} {{ item.title }}

View File

@@ -1,7 +1,7 @@
<header> <header>
<nav class="navbar navbar-expand-lg bg-body-tertiary" aria-label="{{ __('Primary navigation') }}"> <nav class="navbar navbar-expand-lg bg-body-tertiary" aria-label="{{ __('Primary navigation') }}">
<div class="container"> <div class="container">
<a class="navbar-brand fw-bold" href="{{ site.url }}"> <a class="navbar-brand fw-bold" href="{{ site.url|esc_url }}">
{{ site.name }} {{ site.name }}
</a> </a>
@@ -16,7 +16,7 @@
aria-labelledby="navbarOffcanvasLabel"> aria-labelledby="navbarOffcanvasLabel">
<div class="offcanvas-header"> <div class="offcanvas-header">
{% if user.logged_in %} {% if user.logged_in %}
<a href="{{ user.account_url }}" class="d-flex align-items-center text-decoration-none"> <a href="{{ user.account_url|esc_url }}" class="d-flex align-items-center text-decoration-none">
{{ user.avatar|raw }} {{ user.avatar|raw }}
<span class="ms-2 fw-semibold">{{ user.display_name|esc_html }}</span> <span class="ms-2 fw-semibold">{{ user.display_name|esc_html }}</span>
</a> </a>
@@ -32,7 +32,7 @@
{% if item.children|length > 0 %} {% if item.children|length > 0 %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle{{ item.active ? ' active' : '' }}" <a class="nav-link dropdown-toggle{{ item.active ? ' active' : '' }}"
href="{{ item.url }}" role="button" href="{{ item.url|esc_url }}" role="button"
data-bs-toggle="dropdown" aria-expanded="false"> data-bs-toggle="dropdown" aria-expanded="false">
{{ item.title }} {{ item.title }}
</a> </a>
@@ -40,7 +40,7 @@
{% for child in item.children %} {% for child in item.children %}
<li> <li>
<a class="dropdown-item{{ child.active ? ' active' : '' }}" <a class="dropdown-item{{ child.active ? ' active' : '' }}"
href="{{ child.url }}" href="{{ child.url|esc_url }}"
{% if child.active %}aria-current="page"{% endif %} {% if child.active %}aria-current="page"{% endif %}
{% if child.target %}target="{{ child.target }}"{% endif %}> {% if child.target %}target="{{ child.target }}"{% endif %}>
{{ child.title }} {{ child.title }}
@@ -52,7 +52,7 @@
{% else %} {% else %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{{ item.active ? ' active' : '' }}" <a class="nav-link{{ item.active ? ' active' : '' }}"
href="{{ item.url }}" href="{{ item.url|esc_url }}"
{% if item.active %}aria-current="page"{% endif %} {% if item.active %}aria-current="page"{% endif %}
{% if item.target %}target="{{ item.target }}"{% endif %}> {% if item.target %}target="{{ item.target }}"{% endif %}>
{{ item.title }} {{ item.title }}

View File

@@ -1,7 +1,7 @@
<header class="position-absolute w-100" style="z-index: 1030;"> <header class="position-absolute w-100" style="z-index: 1030;">
<nav class="navbar navbar-expand-lg navbar-dark" aria-label="{{ __('Primary navigation') }}"> <nav class="navbar navbar-expand-lg navbar-dark" aria-label="{{ __('Primary navigation') }}">
<div class="container"> <div class="container">
<a class="navbar-brand fw-bold" href="{{ site.url }}"> <a class="navbar-brand fw-bold" href="{{ site.url|esc_url }}">
{{ site.name }} {{ site.name }}
</a> </a>
@@ -18,7 +18,7 @@
{% if item.children|length > 0 %} {% if item.children|length > 0 %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle{{ item.active ? ' active' : '' }}" <a class="nav-link dropdown-toggle{{ item.active ? ' active' : '' }}"
href="{{ item.url }}" role="button" href="{{ item.url|esc_url }}" role="button"
data-bs-toggle="dropdown" aria-expanded="false"> data-bs-toggle="dropdown" aria-expanded="false">
{{ item.title }} {{ item.title }}
</a> </a>
@@ -26,7 +26,7 @@
{% for child in item.children %} {% for child in item.children %}
<li> <li>
<a class="dropdown-item{{ child.active ? ' active' : '' }}" <a class="dropdown-item{{ child.active ? ' active' : '' }}"
href="{{ child.url }}" href="{{ child.url|esc_url }}"
{% if child.active %}aria-current="page"{% endif %} {% if child.active %}aria-current="page"{% endif %}
{% if child.target %}target="{{ child.target }}"{% endif %}> {% if child.target %}target="{{ child.target }}"{% endif %}>
{{ child.title }} {{ child.title }}
@@ -38,7 +38,7 @@
{% else %} {% else %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{{ item.active ? ' active' : '' }}" <a class="nav-link{{ item.active ? ' active' : '' }}"
href="{{ item.url }}" href="{{ item.url|esc_url }}"
{% if item.active %}aria-current="page"{% endif %} {% if item.active %}aria-current="page"{% endif %}
{% if item.target %}target="{{ item.target }}"{% endif %}> {% if item.target %}target="{{ item.target }}"{% endif %}>
{{ item.title }} {{ item.title }}

View File

@@ -1,7 +1,7 @@
<header> <header>
<nav class="navbar navbar-expand-lg bg-body-tertiary" aria-label="{{ __('Primary navigation') }}"> <nav class="navbar navbar-expand-lg bg-body-tertiary" aria-label="{{ __('Primary navigation') }}">
<div class="container"> <div class="container">
<a class="navbar-brand fw-bold" href="{{ site.url }}"> <a class="navbar-brand fw-bold" href="{{ site.url|esc_url }}">
{{ site.name }} {{ site.name }}
</a> </a>
@@ -18,7 +18,7 @@
{% if item.children|length > 0 %} {% if item.children|length > 0 %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle{{ item.active ? ' active' : '' }}" <a class="nav-link dropdown-toggle{{ item.active ? ' active' : '' }}"
href="{{ item.url }}" role="button" href="{{ item.url|esc_url }}" role="button"
data-bs-toggle="dropdown" aria-expanded="false"> data-bs-toggle="dropdown" aria-expanded="false">
{{ item.title }} {{ item.title }}
</a> </a>
@@ -26,7 +26,7 @@
{% for child in item.children %} {% for child in item.children %}
<li> <li>
<a class="dropdown-item{{ child.active ? ' active' : '' }}" <a class="dropdown-item{{ child.active ? ' active' : '' }}"
href="{{ child.url }}" href="{{ child.url|esc_url }}"
{% if child.active %}aria-current="page"{% endif %} {% if child.active %}aria-current="page"{% endif %}
{% if child.target %}target="{{ child.target }}"{% endif %}> {% if child.target %}target="{{ child.target }}"{% endif %}>
{{ child.title }} {{ child.title }}
@@ -38,7 +38,7 @@
{% else %} {% else %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{{ item.active ? ' active' : '' }}" <a class="nav-link{{ item.active ? ' active' : '' }}"
href="{{ item.url }}" href="{{ item.url|esc_url }}"
{% if item.active %}aria-current="page"{% endif %} {% if item.active %}aria-current="page"{% endif %}
{% if item.target %}target="{{ item.target }}"{% endif %}> {% if item.target %}target="{{ item.target }}"{% endif %}>
{{ item.title }} {{ item.title }}

View File

@@ -1,4 +1,4 @@
<form role="search" method="get" action="{{ site.url }}" class="mb-4"> <form role="search" method="get" action="{{ site.url|esc_url }}" class="mb-4">
<div class="input-group"> <div class="input-group">
<input type="search" class="form-control" name="s" <input type="search" class="form-control" name="s"
placeholder="{{ __('Search...') }}" placeholder="{{ __('Search...') }}"