You've already forked wc-bootstrap
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 784b400c46 | |||
| 98359d4cfb | |||
| e72b4ba3c1 | |||
| bf24b121f5 | |||
| cfe089d1fe |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,3 +34,4 @@ releases/
|
||||
|
||||
# Docker runtime
|
||||
.env
|
||||
KNOWN_BUGS.md
|
||||
|
||||
63
CHANGELOG.md
63
CHANGELOG.md
@@ -2,6 +2,69 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.1.5] - 2026-03-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Empty page title on catalog pages** (`header.html.twig`): Replaced `page_title` context variable (never passed by WC) with direct `fn('woocommerce_page_title', false)` call
|
||||
- **Missing breadcrumbs on catalog pages** (`archive-product.php`): Added `woocommerce_breadcrumb()` call before shop loop header
|
||||
- **Missing product categories on catalog pages** (`content-product-cat.html.twig`): Renamed template from hyphen (`content-product-cat`) to underscore (`content-product_cat`) to match WC's `wc_get_template()` filename convention
|
||||
- **Product grid 4 columns instead of 3** (`functions.php`, `loop-start.html.twig`): Changed default columns from 4 to 3 for better card proportions with sidebar
|
||||
- **Double chevron on sort dropdown** (`wc-bootstrap.css`): Removed conflicting `appearance: auto` rule; set `appearance: none` to let Bootstrap's `form-select` class handle the dropdown arrow exclusively
|
||||
- **Variable product add-to-cart button stays disabled** (`variable.html.twig`, `variation-add-to-cart-button.html.twig`): Added missing `data-product_id` and `data-product_variations` attributes to form; replaced HTML `disabled` attribute on button with CSS classes `disabled wc-variation-selection-needed` (WC JS only toggles CSS classes, never removes the HTML attribute)
|
||||
- **Variable product select white background in dark mode** (`wc-bootstrap.css`): Increased dark mode override specificity to `(0,5,1)` to beat WC's `.woocommerce div.product form.cart .variations select` at `(0,4,3)` which uses `background` shorthand; also overrides `background-image` for Bootstrap's dark-mode-aware chevron SVG
|
||||
- **Product gallery missing main image in thumbnail strip** (`product-image.html.twig`): Prepend main image ID to gallery IDs using `[post_thumbnail_id]|merge(gallery_image_ids)` with active state on first thumbnail; added `{% if thumb_url %}` guard to skip invalid attachment IDs
|
||||
- **Related/upsells products show same product repeated** (`related.html.twig`, `up-sells.html.twig`): Added `wc_setup_product_data()` call before each product render and `wp_reset_postdata()` after loop to set global `$product` correctly for WC hooks
|
||||
- **Grouped product add-to-cart button/pricing broken** (`grouped.html.twig`): Rewrote template to compute `quantites_required` and `show_add_to_cart_button` in loop (matching WC PHP logic); moved hidden `add-to-cart` input outside conditional; added `has_options()` and `is_sold_individually()` checks
|
||||
- **Downloads page empty** (`downloads.html.twig`): Replaced fragile `fn('WC').customer.get_downloadable_products()` chain with direct `fn('wc_get_customer_available_downloads', get_current_user_id())` call
|
||||
|
||||
### Added
|
||||
|
||||
- **Product gallery JS** (`product-gallery.js`): Vanilla JS click handler for thumbnail-to-main-image swap with active state highlighting and gallery fade-in
|
||||
- **`wc_setup_product_data()` Twig function** (`WooCommerceExtension.php`): Sets `$GLOBALS['product']` and calls `setup_postdata()` for correct product context in Twig loops
|
||||
- **`wp_reset_postdata` Twig function** (`WooCommerceExtension.php`): Restores global post state after product loops
|
||||
- **`sanitize_title` Twig filter** (`WooCommerceExtension.php`): Matches WC PHP's lowercase attribute name handling for variation form data attributes
|
||||
- **Product thumbnails suppressor** (`product-thumbnails.html.twig`): Empty template override to prevent WC's default full-size gallery images rendering below custom thumbnail row
|
||||
|
||||
### Changed
|
||||
|
||||
- **Whitelisted functions** (`WooCommerceExtension.php`): Added `woocommerce_page_title` and `wc_get_customer_available_downloads` to `ALLOWED_FUNCTIONS`
|
||||
- **Removed obsolete files**: Deleted `PLAN.md` and `SETUP.md` (superseded by CLAUDE.md)
|
||||
|
||||
## [0.1.4] - 2026-03-01
|
||||
|
||||
### Security
|
||||
|
||||
- **fn() function whitelist** (`WooCommerceExtension`): The `callFunction()` method (exposed as `fn()` in Twig templates) now restricts callable functions to an explicit `ALLOWED_FUNCTIONS` whitelist. Previously any PHP function could be called, risking arbitrary code execution if template context were compromised. Only the 6 functions actually used in templates are permitted.
|
||||
- **Notice data attribute escaping**: Changed `{{ notice.data|raw }}` to `{{ notice.data|wp_kses_post }}` in success, error, and notice Twig templates. Defense-in-depth against potential XSS via data attributes.
|
||||
- **Search query escaping** (`product-searchform.html.twig`): Added `|esc_attr` filter to `get_search_query()` output in the search input value attribute.
|
||||
|
||||
### Performance
|
||||
|
||||
- **Per-request ContextBuilder caching**: New `wc_bootstrap_get_theme_context()` function with static variable caching eliminates redundant `ContextBuilder::build()` calls (10-20 DB queries each) when multiple WooCommerce render functions fire in the same request.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Shared page shell helper**: New `wc_bootstrap_render_in_page_shell()` function extracts the duplicated context-injection-and-render pattern from `wc_bootstrap_render_page()`, `wc_bootstrap_render_product_archive()`, and `wc_bootstrap_render_single_product()`.
|
||||
- **Removed unused constants**: Removed `WC_BOOTSTRAP_VERSION` and `WC_BOOTSTRAP_URL` constants that were defined but never referenced.
|
||||
|
||||
## [0.1.3] - 2026-02-28
|
||||
|
||||
### Added
|
||||
|
||||
- Theme screenshot showing dark mode product archive with Bootstrap 5 card grid
|
||||
|
||||
## [0.1.2] - 2026-02-28
|
||||
|
||||
### Fixed
|
||||
|
||||
- Dark mode: text inputs and textareas showing white background due to WooCommerce's `.woocommerce form .form-row .input-text` (specificity `0,3,1`) overriding theme's checkout form rules with `var(--wc-form-color-background, #fff)` fallback
|
||||
- Dark mode: `table-light` class on `<thead>` forcing white table headers in cart, checkout review, orders, and payment methods pages
|
||||
- Dark mode: WooCommerce notice focus ring appearing white when `focus_populate_live_region()` JS programmatically focuses alerts for screen reader accessibility
|
||||
- WooCommerce notice overrides not matching alerts rendered by Twig templates (added `.alert.woocommerce-*` compound selectors alongside `.woocommerce .woocommerce-*` descendant selectors)
|
||||
- Order details table on thank-you page not wrapped in a card like other sections
|
||||
- Thank-you page success message line-wrapping after icon due to block-level `<p>` inside inline alert context
|
||||
|
||||
## [0.1.1] - 2026-02-28
|
||||
|
||||
### Fixed
|
||||
|
||||
105
CLAUDE.md
105
CLAUDE.md
@@ -230,10 +230,31 @@ Recurring bugs and non-obvious behaviours discovered across sessions. **Read thi
|
||||
- **WooCommerce gallery JS requires modifier classes** -- `--with-images`, `--without-images`, `--columns-N` on `.woocommerce-product-gallery` + `style="opacity: 0"` for fade-in. Without these, zoom and photoswipe won't initialize.
|
||||
- **WooCommerce dark mode select backgrounds** -- `woocommerce.css` sets `select { background-color: var(--wc-form-color-background, #fff) }`. The custom property is never defined for dark mode, so it falls back to `#fff`. Override with `[data-bs-theme="dark"] .woocommerce select`.
|
||||
- **Select2/SelectWoo dark mode** -- `select2.css` hardcodes `#fff` on selection containers and dropdowns. Needs overrides for `.select2-selection`, `.select2-dropdown`, `.select2-search__field`, and highlighted options.
|
||||
- **WooCommerce notice CSS specificity** -- Uses `border-top: 3px solid`, `background-color: #f6f5f8`, and icon font `::before` at specificity `0,1,0`. Must use `.woocommerce .woocommerce-*` (specificity `0,2,0`) and explicitly reset `border-top` to override.
|
||||
- **WooCommerce notice CSS specificity** -- Uses `border-top: 3px solid`, `background-color: #f6f5f8`, and icon font `::before` at specificity `0,1,0`. Must use `.woocommerce .woocommerce-*` AND `.alert.woocommerce-*` (both specificity `0,2,0`) to cover notices both inside `.woocommerce` wrappers and rendered directly by Twig templates.
|
||||
- **WooCommerce notice focus ring** -- `woocommerce.js:focus_populate_live_region()` adds `tabindex="-1"` and calls `.focus()` on the first `.woocommerce-message[role="alert"]` after 500ms for screen reader announcement. The default browser focus ring appears white in dark mode. Suppress with `outline: 0; box-shadow: none` since the focus is non-interactive.
|
||||
- **WooCommerce form input specificity** -- `.woocommerce form .form-row .input-text` (specificity `0,3,1`) sets `background-color: var(--wc-form-color-background, #fff)`. This beats theme checkout rules at `0,2,1`. Needs `[data-bs-theme="dark"]` override at `0,4,0`.
|
||||
- **Bootstrap `table-light` breaks dark mode** -- Forces a light background on `<thead>` regardless of `data-bs-theme`. Remove it and let Bootstrap's default table styling handle theming.
|
||||
- **WooCommerce float layout fights Bootstrap grid** -- `div.product div.images/summary` have `float:left/right; width:48%` in `woocommerce-layout.css`. Override with `float: none; width: 100%`.
|
||||
- **Bootstrap `g-*` gutters add negative top margin** -- `g-4` sets both `--bs-gutter-x` and `--bs-gutter-y`; the `.row` gets `margin-top: calc(-1 * var(--bs-gutter-y))` pulling it upward. Use `gx-*` for horizontal-only gutters when vertical gap isn't desired.
|
||||
|
||||
### WooCommerce Variation JS
|
||||
|
||||
- **NEVER use HTML `disabled` attribute on the add-to-cart button.** WC's `add-to-cart-variation.js` only manages CSS classes (`disabled`, `wc-variation-selection-needed`) via jQuery `.addClass()`/`.removeClass()`. It never removes the HTML `disabled` attribute. The HTML attribute prevents click events from firing, making the button permanently unclickable.
|
||||
- Use CSS classes `disabled wc-variation-selection-needed` instead. WC's `onAddToCart` handler checks `$button.is('.disabled')` (CSS class), not the HTML property.
|
||||
- WC variation form requires `data-product_id` and `data-product_variations` on `<form class="variations_form">`. Without these, the JS doesn't initialize.
|
||||
- Attribute select `name` and `data-attribute_name` must use `sanitize_title()` on the attribute name (lowercase, hyphens) to match the keys in the variation JSON data.
|
||||
|
||||
### WooCommerce Variation Select CSS Specificity
|
||||
|
||||
- WC's `.woocommerce div.product form.cart .variations select` at specificity `(0,4,3)` uses `background` shorthand — this resets BOTH `background-color` and `background-image`. A dark mode override must exceed `(0,4,3)` AND override `background-image` with `var(--bs-form-select-bg-img)` for Bootstrap's dark-mode-aware chevron SVG.
|
||||
- Previous selector `[data-bs-theme="dark"] .variations .form-select` at `(0,3,0)` lost the specificity battle.
|
||||
|
||||
### WooCommerce Gallery Data Integrity
|
||||
|
||||
- `_product_image_gallery` post meta may contain IDs pointing to product variations or other non-attachment posts instead of image attachments. `wp_get_attachment_url()` returns empty/false for these.
|
||||
- Always guard thumbnail rendering with `{% if thumb_url %}` after calling `wp_get_attachment_url(image_id)`.
|
||||
- The main product image should be prepended to the gallery strip so users can switch back to it after viewing gallery images.
|
||||
|
||||
### Double Heading Prevention
|
||||
|
||||
- Parent theme (`wp-bootstrap`) conditionally skips its `<h1>` when `post.title` is empty.
|
||||
@@ -324,10 +345,69 @@ The child theme inherits from `wp-bootstrap` via WordPress `Template: wp-bootstr
|
||||
|
||||
## Version History
|
||||
|
||||
Current version: **v0.1.1**
|
||||
Current version: **v0.1.5**
|
||||
|
||||
## Session History
|
||||
|
||||
### 2026-03-01 — v0.1.5 Fix 10 Known Bugs
|
||||
|
||||
**Scope:** Fixed all 10 bugs from KNOWN_BUGS.md — catalog page features (title, breadcrumbs, categories, filters, sort dropdown), single product fixes (variable form, gallery, related products, grouped products), and downloads page.
|
||||
|
||||
**Files changed (17):**
|
||||
|
||||
- `inc/Twig/WooCommerceExtension.php` — Added `woocommerce_page_title` and `wc_get_customer_available_downloads` to `ALLOWED_FUNCTIONS`; added `wc_setup_product_data()` method, `wp_reset_postdata` Twig function, and `sanitize_title` Twig filter
|
||||
- `templates/single-product/add-to-cart/variable.html.twig` — Added `data-product_id` and `data-product_variations` attributes to form; applied `sanitize_title` filter on attribute names for variation matching
|
||||
- `templates/single-product/add-to-cart/variation-add-to-cart-button.html.twig` — Removed HTML `disabled` attribute, replaced with CSS classes `disabled wc-variation-selection-needed` (WC JS only manages CSS classes, never the HTML attribute)
|
||||
- `templates/single-product/product-image.html.twig` — Prepend main image to gallery thumbnail strip; added `{% if thumb_url %}` guard for invalid attachment IDs
|
||||
- `templates/single-product/product-thumbnails.html.twig` — New empty override to suppress WC's default full-size gallery images
|
||||
- `templates/single-product/related.html.twig` — Added `wc_setup_product_data()` before each product render + `wp_reset_postdata()` after loop
|
||||
- `templates/single-product/up-sells.html.twig` — Same setup_postdata fix as related.html.twig
|
||||
- `templates/single-product/add-to-cart/grouped.html.twig` — Rewrote to compute `quantites_required`/`show_add_to_cart_button` in loop (matching WC PHP); moved hidden input outside conditional; added `has_options()` and `is_sold_individually()` checks
|
||||
- `templates/loop/header.html.twig` — Replaced `page_title` context variable with direct `fn('woocommerce_page_title', false)` call
|
||||
- `woocommerce/archive-product.php` — Added `woocommerce_breadcrumb()` call before shop loop header
|
||||
- `functions.php` — Changed loop columns from 4 to 3; added product gallery JS conditional enqueue
|
||||
- `templates/loop/loop-start.html.twig` — Changed default columns from 4 to 3
|
||||
- `assets/css/wc-bootstrap.css` — Replaced duplicate ordering select rules with `appearance: none`; increased dark mode variation select specificity to `(0,5,1)` to beat WC's `(0,4,3)` background shorthand
|
||||
- `templates/content-product-cat.html.twig` — Renamed to `content-product_cat.html.twig` (WC uses underscore)
|
||||
- `assets/js/product-gallery.js` — New: thumbnail click-to-swap, gallery fade-in, active state highlighting
|
||||
- `templates/myaccount/downloads.html.twig` — Replaced `fn('WC').customer.get_downloadable_products()` with direct `fn('wc_get_customer_available_downloads', get_current_user_id())`
|
||||
- `style.css` — Version bump 0.1.4 → 0.1.5
|
||||
|
||||
**Key decisions:**
|
||||
|
||||
- **`wc_setup_product_data()` over PHP bridge:** Adding a Twig function that sets `$GLOBALS['product']` + `setup_postdata()` is simpler and more maintainable than creating PHP bridge files for related/upsells rendering
|
||||
- **`json_encode|esc_attr` over `wc_esc_json`:** Avoids adding a custom filter; `esc_attr` performs the same HTML entity encoding as `wc_esc_json`
|
||||
- **3 columns default:** With sidebar taking `col-lg-3`, 3 product columns in `col-lg-9` gives better card proportions than 4
|
||||
- **Vanilla JS gallery:** Lightweight click-to-swap handler instead of WC's built-in flexslider/photoswipe (which requires specific PHP setup for `wp_get_attachment_image_src` data attributes)
|
||||
- **CSS class `disabled` over HTML `disabled` attribute:** WC's `add-to-cart-variation.js` `onShow`/`onHide` only toggle CSS classes (`disabled`, `wc-variation-selection-needed`). The `onAddToCart` handler checks `.is('.disabled')`. Using the HTML `disabled` attribute prevents click events entirely and WC JS never removes it.
|
||||
- **CSS specificity `(0,5,1)` for dark mode selects:** WC's `.woocommerce div.product form.cart .variations select` at `(0,4,3)` uses `background` shorthand which resets `background-color` and `background-image`. Must exceed this specificity AND override `background-image` for Bootstrap's dark-mode SVG chevron.
|
||||
|
||||
**Key findings (WC variation JS):**
|
||||
|
||||
- WC's `add-to-cart-variation.js` event flow: `onChange` → `check_variations` → `findMatchingVariations` → `found_variation` → `onFoundVariation` (300ms setTimeout) → `show_variation` → `onShow` (removes CSS class only)
|
||||
- `onShow` calls `$button.removeClass('disabled wc-variation-selection-needed')` — never touches the HTML `disabled` attribute/property
|
||||
- `onAddToCart` checks `$button.is('.disabled')` — CSS class, not HTML attribute
|
||||
- Gallery `_product_image_gallery` meta may contain IDs of product variations or posts instead of image attachments — always guard with `{% if thumb_url %}` after `wp_get_attachment_url()`
|
||||
|
||||
### 2026-03-01 — v0.1.4 Security Audit & Performance Fixes
|
||||
|
||||
**Scope:** Cross-theme security audit (12 findings), all fixed. Covers fn() whitelist, notice data escaping, search query escaping, per-request context caching, shared render helper, and unused constant removal.
|
||||
|
||||
**Files changed (6):**
|
||||
|
||||
- `inc/Twig/WooCommerceExtension.php` — Added `ALLOWED_FUNCTIONS` whitelist to `callFunction()`. Only 6 functions (`WC`, `_n`, `get_pagenum_link`, `wc_review_ratings_enabled`, `wc_get_product_category_list`, `wc_get_product_tag_list`) are permitted.
|
||||
- `templates/notices/success.html.twig` — `notice.data|raw` → `notice.data|wp_kses_post`
|
||||
- `templates/notices/error.html.twig` — `notice.data|raw` → `notice.data|wp_kses_post`
|
||||
- `templates/notices/notice.html.twig` — `notice.data|raw` → `notice.data|wp_kses_post`
|
||||
- `templates/product-searchform.html.twig` — Added `|esc_attr` on `get_search_query()` value
|
||||
- `functions.php` — Removed unused `WC_BOOTSTRAP_VERSION`/`WC_BOOTSTRAP_URL` constants. Added `wc_bootstrap_get_theme_context()` with static caching and `wc_bootstrap_render_in_page_shell()` helper. Refactored 3 render functions to use shared helpers.
|
||||
|
||||
**Key decisions:**
|
||||
|
||||
- **fn() whitelist defense-in-depth**: Template files are static PHP, not user-editable, so exploitation requires file write access. Whitelist added anyway as defense-in-depth to prevent `exec()`, `system()`, etc. if template context were ever compromised.
|
||||
- **`|wp_kses_post` over `|raw` for data attributes**: WooCommerce sanitizes notice data, but belt-and-suspenders approach prevents XSS if upstream behavior changes.
|
||||
- **Static variable caching over transients**: Per-request `static $cached_context` is sufficient since WooCommerce pages build context once. No transient overhead or invalidation needed.
|
||||
|
||||
### 2026-02-28 — My Account Bootstrap 5 Polish
|
||||
|
||||
**Scope:** Redesigned 8 my-account Twig templates + CSS overrides to feel like a polished Bootstrap 5 application.
|
||||
@@ -406,3 +486,24 @@ Current version: **v0.1.1**
|
||||
- Select2/SelectWoo hardcodes `#fff` backgrounds in `select2.css` — needs comprehensive overrides for selection containers, dropdowns, search fields, and highlighted options.
|
||||
- WooCommerce notice CSS uses `border-top: 3px solid`, `background-color: #f6f5f8`, and WooCommerce icon font `::before` at specificity `0,1,0`. Single-class overrides don't win — must use `.woocommerce .woocommerce-*` (specificity `0,2,0`) and explicitly reset `border-top`.
|
||||
- Never set `background-image` without `background-repeat: no-repeat` / `background-size` / `background-position` — SVGs will tile.
|
||||
|
||||
### 2026-02-28 — v0.1.2 Dark Mode Deep Fixes (Tables, Inputs, Notices)
|
||||
|
||||
**Scope:** Fixed dark mode rendering issues across checkout, thank-you, cart, and account pages — white table headers, white form inputs, notice focus rings, and missing card wrappers.
|
||||
|
||||
**Files changed (7):**
|
||||
|
||||
- `assets/css/wc-bootstrap.css` — Added `[data-bs-theme="dark"]` override for `.input-text` and `textarea` (WooCommerce specificity `0,3,1` beats theme at `0,2,1`); added `.alert.woocommerce-*` compound selectors for notice overrides outside `.woocommerce` wrapper; added focus ring suppression for programmatically focused notices
|
||||
- `templates/order/order-details.html.twig` — Wrapped product table in `card shadow-sm` with `card-header`; removed `table-light` from `<thead>`
|
||||
- `templates/checkout/thankyou.html.twig` — Added `d-flex align-items-center` to success alerts to fix icon/text line wrap
|
||||
- `templates/checkout/review-order.html.twig` — Removed `table-light` from `<thead>`
|
||||
- `templates/cart/cart.html.twig` — Removed `table-light` from `<thead>`
|
||||
- `templates/myaccount/orders.html.twig` — Removed `table-light` from `<thead>`
|
||||
- `templates/myaccount/payment-methods.html.twig` — Removed `table-light` from `<thead>`
|
||||
|
||||
**Key findings:**
|
||||
|
||||
- WooCommerce's `.woocommerce form .form-row .input-text` at specificity `0,3,1` sets `background-color: var(--wc-form-color-background, #fff)` which beats theme rules. The `textarea` generated by `woocommerce_form_field()` has class `input-text`, so it matches this rule. Needs `[data-bs-theme="dark"]` override at `0,4,0`.
|
||||
- WooCommerce's `focus_populate_live_region()` in `woocommerce.js` adds `tabindex="-1"` and calls `.focus()` on `.woocommerce-message[role="alert"]` after 500ms for screen reader accessibility. The default browser focus ring appears white in dark mode.
|
||||
- Notice overrides using `.woocommerce .woocommerce-*` descendant selectors don't match notices rendered by Twig templates where `.alert` and `.woocommerce-message` are on the same element without a `.woocommerce` wrapper ancestor. Both `.woocommerce .woocommerce-*` and `.alert.woocommerce-*` patterns needed.
|
||||
- Bootstrap's `table-light` class forces a light background on `<thead>` regardless of dark mode. Remove it entirely — let the card-header or default table styling handle visual separation.
|
||||
|
||||
652
PLAN.md
652
PLAN.md
@@ -1,652 +0,0 @@
|
||||
# WooCommerce to Bootstrap 5 -- Template Conversion Plan
|
||||
|
||||
This document outlines the full plan for converting WooCommerce's default templates into Bootstrap 5 themed Twig overrides within the `wc-bootstrap` child theme.
|
||||
|
||||
## Scope
|
||||
|
||||
**Source:** 233 WooCommerce PHP/HTML templates in `wp-content/plugins/woocommerce/templates/`
|
||||
**Target:** Twig template overrides in `wp-content/themes/wc-bootstrap/templates/`
|
||||
**Design system:** Bootstrap 5.3 (loaded via parent theme `wp-bootstrap`)
|
||||
|
||||
The plugin is **read-only** -- we never modify WooCommerce source. All customisation happens through Twig template overrides via `TemplateOverride::prependPath()` and WordPress hooks in `functions.php`.
|
||||
|
||||
---
|
||||
|
||||
## Existing Scaffold (v0.1.0)
|
||||
|
||||
Already in place:
|
||||
|
||||
| File | Purpose |
|
||||
| ---- | ------- |
|
||||
| `templates/base.html.twig` | Conditional wrapper (`_theme_wrapped` detection) |
|
||||
| `templates/layouts/account.html.twig` | My Account layout shell |
|
||||
| `templates/layouts/archive.html.twig` | Product archive / search layout |
|
||||
| `templates/layouts/form.html.twig` | Centered form layout (card + shadow) |
|
||||
| `templates/layouts/page.html.twig` | Generic content page |
|
||||
| `templates/layouts/single.html.twig` | Detail page (8+4 grid) |
|
||||
| `templates/components/card.html.twig` | Reusable product/post card |
|
||||
| `templates/components/pagination.html.twig` | Bootstrap pagination nav |
|
||||
| `inc/TemplateOverride.php` | Twig loader hook |
|
||||
| `functions.php` | Style chain, render filter, wrapping signal |
|
||||
| `assets/css/wc-bootstrap.css` | Button/alert/dark-mode overrides |
|
||||
|
||||
---
|
||||
|
||||
## Conversion Phases
|
||||
|
||||
### Phase 1 -- Global & Notices (Foundation)
|
||||
|
||||
Templates that appear on every WooCommerce page. Must be done first because all other templates depend on them.
|
||||
|
||||
#### 1.1 Global Templates
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `global/wrapper-start.php` | `global/wrapper-start.html.twig` | `<div class="container my-4">` (or skip if `_theme_wrapped`) |
|
||||
| `global/wrapper-end.php` | `global/wrapper-end.html.twig` | Closing `</div>` |
|
||||
| `global/breadcrumb.php` | `global/breadcrumb.html.twig` | `nav[aria-label] > ol.breadcrumb > li.breadcrumb-item` |
|
||||
| `global/sidebar.php` | `global/sidebar.html.twig` | `aside.col-lg-3` with `offcanvas-lg` for mobile |
|
||||
| `global/quantity-input.php` | `global/quantity-input.html.twig` | `input-group` with `btn-outline-secondary` +/- buttons |
|
||||
| `global/form-login.php` | `global/form-login.html.twig` | Extend `layouts/form.html.twig`, `form-control`, `form-label` |
|
||||
|
||||
#### 1.2 Notice Templates
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `notices/notice.php` | `notices/notice.html.twig` | `alert alert-info alert-dismissible fade show` |
|
||||
| `notices/error.php` | `notices/error.html.twig` | `alert alert-danger alert-dismissible fade show` |
|
||||
| `notices/success.php` | `notices/success.html.twig` | `alert alert-success alert-dismissible fade show` |
|
||||
|
||||
**Notes:**
|
||||
|
||||
- Notices must support multiple messages (WooCommerce passes arrays)
|
||||
- Include `btn-close` with `data-bs-dismiss="alert"`
|
||||
- Respect dark mode via Bootstrap's adaptive colour utilities
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 -- Product Archive & Shop Loop (Storefront)
|
||||
|
||||
The shop page, category pages, and product grid. This is the main entry point for customers.
|
||||
|
||||
#### 2.1 Archive Container
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `archive-product.php` | `archive-product.html.twig` | Extend `layouts/archive.html.twig`; 3+9 grid (filter sidebar + products) |
|
||||
| `content-product.php` | `content-product.html.twig` | Extend `components/card.html.twig`; product card in grid |
|
||||
| `content-product-cat.php` | `content-product-cat.html.twig` | Category card with thumbnail |
|
||||
| `product-searchform.php` | `product-searchform.html.twig` | `input-group` with `form-control` + `btn btn-outline-primary` |
|
||||
|
||||
#### 2.2 Loop Components
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `loop/loop-start.php` | `loop/loop-start.html.twig` | `<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">` |
|
||||
| `loop/loop-end.php` | `loop/loop-end.html.twig` | `</div>` |
|
||||
| `loop/header.php` | `loop/header.html.twig` | Flex row: result-count left, orderby right |
|
||||
| `loop/result-count.php` | `loop/result-count.html.twig` | `<p class="text-body-secondary mb-0">` |
|
||||
| `loop/orderby.php` | `loop/orderby.html.twig` | `form-select form-select-sm` in flex container |
|
||||
| `loop/pagination.php` | `loop/pagination.html.twig` | Delegate to `components/pagination.html.twig` |
|
||||
| `loop/no-products-found.php` | `loop/no-products-found.html.twig` | `alert alert-info` with icon |
|
||||
| `loop/add-to-cart.php` | `loop/add-to-cart.html.twig` | `btn btn-primary btn-sm w-100` |
|
||||
| `loop/price.php` | `loop/price.html.twig` | `<span class="fs-5 fw-semibold">` (sale: `text-decoration-line-through text-body-secondary` + `text-danger`) |
|
||||
| `loop/rating.php` | `loop/rating.html.twig` | Bootstrap Icons stars (`bi-star-fill`, `bi-star`) |
|
||||
| `loop/sale-flash.php` | `loop/sale-flash.html.twig` | `badge bg-danger position-absolute top-0 start-0 m-2` |
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 -- Single Product Page
|
||||
|
||||
The product detail page, including gallery, variations, tabs, and related products.
|
||||
|
||||
#### 3.1 Product Layout
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `single-product/product-image.php` | `single-product/product-image.html.twig` | Main image + thumbnail row; `img-fluid rounded`; lightbox via PhotoSwipe or Bootstrap modal |
|
||||
| `single-product/title.php` | `single-product/title.html.twig` | `<h1 class="mb-2">` |
|
||||
| `single-product/price.php` | `single-product/price.html.twig` | `fs-3 fw-bold`; sale styling as in loop |
|
||||
| `single-product/short-description.php` | `single-product/short-description.html.twig` | `<div class="lead text-body-secondary mb-3">` |
|
||||
| `single-product/meta.php` | `single-product/meta.html.twig` | `<dl class="row mb-3">` with `col-sm-3`/`col-sm-9` pairs |
|
||||
| `single-product/rating.php` | `single-product/rating.html.twig` | Stars + link to reviews tab |
|
||||
| `single-product/stock.php` | `single-product/stock.html.twig` | `badge bg-success` (in stock) / `badge bg-danger` (out of stock) |
|
||||
| `single-product/sale-flash.php` | `single-product/sale-flash.html.twig` | `badge bg-danger fs-6` |
|
||||
| `single-product/share.php` | `single-product/share.html.twig` | `btn-group` with Bootstrap Icon buttons |
|
||||
| `single-product/product-attributes.php` | `single-product/product-attributes.html.twig` | `table table-sm table-striped` |
|
||||
| `single-product/related.php` | `single-product/related.html.twig` | Section heading + `row row-cols-2 row-cols-lg-4 g-4` of cards |
|
||||
| `single-product/up-sells.php` | `single-product/up-sells.html.twig` | Same grid as related |
|
||||
|
||||
#### 3.2 Add to Cart Forms
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `single-product/add-to-cart/simple.php` | `single-product/add-to-cart/simple.html.twig` | `input-group` (quantity) + `btn btn-primary btn-lg` |
|
||||
| `single-product/add-to-cart/variable.php` | `single-product/add-to-cart/variable.html.twig` | `form-select` per attribute + quantity + button |
|
||||
| `single-product/add-to-cart/grouped.php` | `single-product/add-to-cart/grouped.html.twig` | `table table-borderless` with quantity inputs per item |
|
||||
| `single-product/add-to-cart/external.php` | `single-product/add-to-cart/external.html.twig` | `btn btn-outline-primary btn-lg` with external link icon |
|
||||
| `single-product/add-to-cart/variation.php` | `single-product/add-to-cart/variation.html.twig` | Variation data container (hidden, JS-driven) |
|
||||
| `single-product/add-to-cart/variation-add-to-cart-button.php` | `single-product/add-to-cart/variation-add-to-cart-button.html.twig` | Same as simple but variation-aware |
|
||||
|
||||
#### 3.3 Product Tabs
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `single-product/tabs/tabs.php` | `single-product/tabs/tabs.html.twig` | `nav nav-tabs` + `tab-content` with `tab-pane fade` |
|
||||
| `single-product/tabs/description.php` | `single-product/tabs/description.html.twig` | `tab-pane` with prose content |
|
||||
| `single-product/tabs/additional-information.php` | `single-product/tabs/additional-information.html.twig` | `tab-pane` with attributes table |
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 -- Cart
|
||||
|
||||
The cart page with item management, shipping calculator, and totals.
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `cart/cart.php` | `cart/cart.html.twig` | `row`: `col-lg-8` (items table) + `col-lg-4` (totals sidebar) |
|
||||
| `cart/cart-empty.php` | `cart/cart-empty.html.twig` | Centred `alert alert-info` + `btn btn-primary` to shop |
|
||||
| `cart/cart-item-data.php` | `cart/cart-item-data.html.twig` | `<dl class="small text-body-secondary mb-0">` |
|
||||
| `cart/cart-totals.php` | `cart/cart-totals.html.twig` | `card` with `list-group list-group-flush` rows |
|
||||
| `cart/cart-shipping.php` | `cart/cart-shipping.html.twig` | `form-check` radio buttons per shipping method |
|
||||
| `cart/cross-sells.php` | `cart/cross-sells.html.twig` | Section below cart; `row row-cols-2 row-cols-md-4 g-3` of small cards |
|
||||
| `cart/mini-cart.php` | `cart/mini-cart.html.twig` | `offcanvas offcanvas-end` slide-in panel |
|
||||
| `cart/proceed-to-checkout-button.php` | `cart/proceed-to-checkout-button.html.twig` | `btn btn-primary btn-lg w-100` |
|
||||
| `cart/shipping-calculator.php` | `cart/shipping-calculator.html.twig` | Collapsible form (`collapse`) with `form-select` for country/state |
|
||||
|
||||
**Cart Table Structure:**
|
||||
|
||||
```html
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Price</th>
|
||||
<th>Quantity</th>
|
||||
<th>Subtotal</th>
|
||||
<th></th><!-- Remove -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- cart items --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 5 -- Checkout
|
||||
|
||||
The checkout flow including billing/shipping forms, order review, and payment.
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `checkout/form-checkout.php` | `checkout/form-checkout.html.twig` | `row`: `col-lg-7` (forms) + `col-lg-5` (order review, sticky) |
|
||||
| `checkout/form-billing.php` | `checkout/form-billing.html.twig` | `card mb-4` with `form-control`, `form-label`, `form-select` fields |
|
||||
| `checkout/form-shipping.php` | `checkout/form-shipping.html.twig` | Same card style; `form-check` for "ship to different address" toggle |
|
||||
| `checkout/form-coupon.php` | `checkout/form-coupon.html.twig` | `input-group` inline: `form-control` + `btn btn-outline-secondary` |
|
||||
| `checkout/form-login.php` | `checkout/form-login.html.twig` | Collapsible (`collapse`) login form above checkout |
|
||||
| `checkout/review-order.php` | `checkout/review-order.html.twig` | `card` with `table table-sm` for items + `list-group` for totals |
|
||||
| `checkout/payment.php` | `checkout/payment.html.twig` | `list-group` of `form-check` radio items per gateway; `btn btn-primary btn-lg w-100` place order |
|
||||
| `checkout/payment-method.php` | `checkout/payment-method.html.twig` | `list-group-item` with radio + label + description collapse |
|
||||
| `checkout/terms.php` | `checkout/terms.html.twig` | `form-check` checkbox with link to terms page |
|
||||
| `checkout/thankyou.php` | `checkout/thankyou.html.twig` | `alert alert-success` + order details card |
|
||||
| `checkout/order-received.php` | `checkout/order-received.html.twig` | Confirmation message with order summary |
|
||||
| `checkout/cart-errors.php` | `checkout/cart-errors.html.twig` | `alert alert-danger` list |
|
||||
|
||||
---
|
||||
|
||||
### Phase 6 -- My Account Dashboard
|
||||
|
||||
The customer account area with orders, addresses, and account management.
|
||||
|
||||
#### 6.1 Account Shell
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `myaccount/my-account.php` | `myaccount/my-account.html.twig` | Extend `layouts/account.html.twig`; 3+9 grid (nav + content) |
|
||||
| `myaccount/navigation.php` | `myaccount/navigation.html.twig` | `list-group` in sticky sidebar (desktop) / `nav nav-pills` (mobile) |
|
||||
| `myaccount/dashboard.php` | `myaccount/dashboard.html.twig` | Welcome card + quick links as `row row-cols-1 row-cols-md-3 g-3` |
|
||||
|
||||
#### 6.2 Orders
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `myaccount/orders.php` | `myaccount/orders.html.twig` | `table-responsive` > `table table-hover` with status badges |
|
||||
| `myaccount/view-order.php` | `myaccount/view-order.html.twig` | Order details card + address cards row |
|
||||
|
||||
#### 6.3 Addresses & Account
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `myaccount/my-address.php` | `myaccount/my-address.html.twig` | `row row-cols-1 row-cols-md-2 g-4`; billing + shipping cards |
|
||||
| `myaccount/form-edit-address.php` | `myaccount/form-edit-address.html.twig` | `card` form with `form-control`, `form-select` |
|
||||
| `myaccount/form-edit-account.php` | `myaccount/form-edit-account.html.twig` | `card` form for name, email, password change |
|
||||
| `myaccount/downloads.php` | `myaccount/downloads.html.twig` | `table table-striped` with download links |
|
||||
| `myaccount/payment-methods.php` | `myaccount/payment-methods.html.twig` | `list-group` of saved methods with delete button |
|
||||
| `myaccount/form-add-payment-method.php` | `myaccount/form-add-payment-method.html.twig` | `card` form |
|
||||
|
||||
#### 6.4 Authentication
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `myaccount/form-login.php` | `myaccount/form-login.html.twig` | `row row-cols-1 row-cols-md-2 g-4`: login card + register card side-by-side |
|
||||
| `myaccount/form-lost-password.php` | `myaccount/form-lost-password.html.twig` | Extend `layouts/form.html.twig`; centered card |
|
||||
| `myaccount/form-reset-password.php` | `myaccount/form-reset-password.html.twig` | Extend `layouts/form.html.twig`; centered card |
|
||||
| `myaccount/lost-password-confirmation.php` | `myaccount/lost-password-confirmation.html.twig` | `alert alert-success` |
|
||||
|
||||
---
|
||||
|
||||
### Phase 7 -- Order Details
|
||||
|
||||
Order display used on thank-you page, view-order, and emails.
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `order/order-details.php` | `order/order-details.html.twig` | `card` > `table-responsive` > `table` |
|
||||
| `order/order-details-item.php` | `order/order-details-item.html.twig` | `<tr>` with product image thumbnail, name, quantity, price |
|
||||
| `order/order-details-customer.php` | `order/order-details-customer.html.twig` | `row row-cols-1 row-cols-md-2`: billing + shipping address cards |
|
||||
| `order/form-tracking.php` | `order/form-tracking.html.twig` | Extend `layouts/form.html.twig`; order ID + email fields |
|
||||
| `order/order-again.php` | `order/order-again.html.twig` | `btn btn-outline-primary` |
|
||||
|
||||
---
|
||||
|
||||
### Phase 8 -- Email Templates
|
||||
|
||||
Email templates require special treatment: no external CSS, no Bootstrap JS, inline styles only.
|
||||
|
||||
**Strategy:** Use Bootstrap's colour palette as CSS custom properties mapped to inline styles. Prefix all classes with `wc-email-` to avoid email client conflicts (per architecture decision in CLAUDE.md).
|
||||
|
||||
#### 8.1 Email Shell
|
||||
|
||||
| WooCommerce Template | Theme Override | Purpose |
|
||||
| -------------------- | -------------- | ------- |
|
||||
| `emails/email-header.php` | `emails/email-header.html.twig` | Table-based header with logo, store name |
|
||||
| `emails/email-footer.php` | `emails/email-footer.html.twig` | Table-based footer with store info |
|
||||
| `emails/email-styles.php` | `emails/email-styles.html.twig` | Inline CSS aligned to Bootstrap colour palette |
|
||||
|
||||
#### 8.2 Email Content Components
|
||||
|
||||
| WooCommerce Template | Theme Override | Purpose |
|
||||
| -------------------- | -------------- | ------- |
|
||||
| `emails/email-order-details.php` | `emails/email-order-details.html.twig` | Order items table |
|
||||
| `emails/email-order-items.php` | `emails/email-order-items.html.twig` | Single order item row |
|
||||
| `emails/email-customer-details.php` | `emails/email-customer-details.html.twig` | Customer info section |
|
||||
| `emails/email-addresses.php` | `emails/email-addresses.html.twig` | Billing + shipping addresses |
|
||||
| `emails/email-downloads.php` | `emails/email-downloads.html.twig` | Downloads table |
|
||||
|
||||
#### 8.3 Transactional Emails (Priority Order)
|
||||
|
||||
High-traffic emails first:
|
||||
|
||||
1. `emails/customer-processing-order.php` -- Order confirmation
|
||||
2. `emails/customer-completed-order.php` -- Order shipped/complete
|
||||
3. `emails/customer-on-hold-order.php` -- Awaiting payment
|
||||
4. `emails/customer-new-account.php` -- Welcome email
|
||||
5. `emails/customer-reset-password.php` -- Password reset
|
||||
6. `emails/customer-invoice.php` -- Invoice/receipt
|
||||
7. `emails/customer-note.php` -- Order note
|
||||
8. `emails/customer-refunded-order.php` -- Refund notification
|
||||
9. `emails/customer-cancelled-order.php` -- Cancellation
|
||||
10. `emails/customer-failed-order.php` -- Payment failure
|
||||
|
||||
Admin emails:
|
||||
|
||||
11. `emails/admin-new-order.php` -- New order notification
|
||||
12. `emails/admin-cancelled-order.php` -- Cancellation alert
|
||||
13. `emails/admin-failed-order.php` -- Payment failure alert
|
||||
|
||||
#### 8.4 Plain Text Emails
|
||||
|
||||
The `emails/plain/` directory contains plain-text variants. These need minimal conversion -- mainly ensuring consistent formatting and branding text. Lower priority; can be done as a batch after HTML emails.
|
||||
|
||||
---
|
||||
|
||||
### Phase 9 -- Supplementary Templates
|
||||
|
||||
Lower-traffic pages and edge cases.
|
||||
|
||||
#### 9.1 Brands
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `brands/brand-description.php` | `brands/brand-description.html.twig` | `<div class="mb-4">` with prose |
|
||||
| `brands/taxonomy-product_brand.php` | `brands/taxonomy-product_brand.html.twig` | Extend `layouts/archive.html.twig` |
|
||||
| `brands/shortcodes/brands-a-z.php` | `brands/shortcodes/brands-a-z.html.twig` | Letter nav + `row` grid of brand cards |
|
||||
| `brands/shortcodes/single-brand.php` | `brands/shortcodes/single-brand.html.twig` | Brand card |
|
||||
|
||||
#### 9.2 Auth (OAuth)
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `auth/form-login.php` | `auth/form-login.html.twig` | Extend `layouts/form.html.twig` |
|
||||
| `auth/form-grant-access.php` | `auth/form-grant-access.html.twig` | `card` with scope list + approve/deny buttons |
|
||||
|
||||
#### 9.3 Back-in-Stock
|
||||
|
||||
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
|
||||
| -------------------- | -------------- | -------------------- |
|
||||
| `single-product/back-in-stock-form.php` | `single-product/back-in-stock-form.html.twig` | `card card-body` with email `input-group` |
|
||||
|
||||
---
|
||||
|
||||
## CSS Strategy
|
||||
|
||||
### File: `assets/css/wc-bootstrap.css`
|
||||
|
||||
Additions needed per phase:
|
||||
|
||||
| Phase | CSS Additions |
|
||||
| ----- | ------------- |
|
||||
| 1 | Quantity input-group sizing; notice icon spacing |
|
||||
| 2 | Product card hover lift effect; sale badge positioning; star rating colours |
|
||||
| 3 | Product gallery thumbnail grid; tab content transitions; variation swatches |
|
||||
| 4 | Cart item thumbnail sizing; mini-cart offcanvas width |
|
||||
| 5 | Checkout sticky sidebar offset; payment method description animation |
|
||||
| 6 | Account navigation active state; order status badge colours |
|
||||
| 8 | Email inline styles (separate `wc-email-*` classes, no Bootstrap dependency) |
|
||||
|
||||
### WooCommerce CSS Overrides
|
||||
|
||||
WooCommerce ships its own CSS that conflicts with Bootstrap. Key overrides:
|
||||
|
||||
```css
|
||||
/* Reset WooCommerce grid in favour of Bootstrap */
|
||||
.woocommerce ul.products { display: contents; }
|
||||
|
||||
/* Ensure Bootstrap form styles take precedence */
|
||||
.woocommerce .form-row input.input-text { /* inherit from .form-control */ }
|
||||
|
||||
/* Price styling consistency */
|
||||
.woocommerce .price del { @extend .text-decoration-line-through, .text-body-secondary; }
|
||||
.woocommerce .price ins { text-decoration: none; @extend .text-danger, .fw-bold; }
|
||||
```
|
||||
|
||||
### Dark Mode
|
||||
|
||||
All templates use Bootstrap's adaptive utilities (`bg-body-tertiary`, `text-body-secondary`, `border-*`) which automatically adapt via `data-bs-theme="dark"`. No manual dark-mode CSS should be needed except for:
|
||||
|
||||
- Custom sale badge colours
|
||||
- Star rating fill colour
|
||||
- Email templates (separate palette)
|
||||
|
||||
---
|
||||
|
||||
## JavaScript Strategy
|
||||
|
||||
Minimal custom JS. Bootstrap 5 JS (from parent theme) handles:
|
||||
|
||||
- Offcanvas (mini-cart, mobile sidebar)
|
||||
- Collapse (shipping calculator, coupon form, login form)
|
||||
- Tabs (product tabs)
|
||||
- Modal (image lightbox, if replacing PhotoSwipe)
|
||||
- Tooltips/popovers (optional)
|
||||
- Alert dismiss (notices)
|
||||
|
||||
Custom JS needed for:
|
||||
|
||||
| Feature | Location | Purpose |
|
||||
| ------- | -------- | ------- |
|
||||
| Quantity +/- buttons | `assets/js/quantity.js` | Increment/decrement input value, trigger `change` event for WC JS |
|
||||
| Variation swatches | `assets/js/variations.js` | Sync Bootstrap form-select with WC variation JS (if needed) |
|
||||
| Mini-cart AJAX | `assets/js/mini-cart.js` | Update offcanvas cart count after add-to-cart |
|
||||
| Sticky cart totals | Inline or CSS-only | `position: sticky; top: 1rem` on checkout/cart sidebar |
|
||||
|
||||
---
|
||||
|
||||
## Template Naming Convention
|
||||
|
||||
All theme overrides mirror the plugin's directory structure exactly:
|
||||
|
||||
```txt
|
||||
woocommerce/templates/cart/cart.php
|
||||
--> wc-bootstrap/templates/cart/cart.html.twig
|
||||
|
||||
woocommerce/templates/single-product/tabs/tabs.php
|
||||
--> wc-bootstrap/templates/single-product/tabs/tabs.html.twig
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Reuse Map
|
||||
|
||||
Common Bootstrap patterns extracted into reusable Twig components:
|
||||
|
||||
| Component | File | Used By |
|
||||
| --------- | ---- | ------- |
|
||||
| Product card | `components/card.html.twig` | Archive, related, cross-sells, up-sells |
|
||||
| Pagination | `components/pagination.html.twig` | Archive, orders, downloads |
|
||||
| Price display | `components/price.html.twig` | Loop, single product, cart, order details |
|
||||
| Star rating | `components/rating.html.twig` | Loop, single product |
|
||||
| Address card | `components/address-card.html.twig` | My Account addresses, order details, checkout |
|
||||
| Status badge | `components/status-badge.html.twig` | Orders list, view order |
|
||||
| Quantity input | `components/quantity-input.html.twig` | Cart, single product add-to-cart |
|
||||
| Form field | `components/form-field.html.twig` | Checkout, account forms, address forms |
|
||||
|
||||
---
|
||||
|
||||
## Order Status Badge Colours
|
||||
|
||||
| Status | Bootstrap Class |
|
||||
| ------ | --------------- |
|
||||
| Pending | `bg-warning text-dark` |
|
||||
| Processing | `bg-info text-dark` |
|
||||
| On Hold | `bg-secondary` |
|
||||
| Completed | `bg-success` |
|
||||
| Cancelled | `bg-danger` |
|
||||
| Refunded | `bg-dark` |
|
||||
| Failed | `bg-danger` |
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Checklist
|
||||
|
||||
Every template must meet:
|
||||
|
||||
- [ ] Semantic HTML (`<nav>`, `<main>`, `<article>`, `<section>`, `<aside>`)
|
||||
- [ ] ARIA labels on interactive elements (`aria-label`, `aria-expanded`, `aria-current`)
|
||||
- [ ] `sr-only` / `visually-hidden` for icon-only buttons
|
||||
- [ ] Focus-visible styles (Bootstrap default)
|
||||
- [ ] Keyboard navigation for custom widgets (quantity buttons, variation selects)
|
||||
- [ ] Colour contrast ratio >= 4.5:1 (Bootstrap defaults meet this)
|
||||
- [ ] Form labels associated with inputs (`for`/`id` pairs)
|
||||
- [ ] Error messages linked to fields via `aria-describedby`
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Since Docker is not yet available, testing follows a manual / staged approach:
|
||||
|
||||
### Per-Template Checklist
|
||||
|
||||
1. **Visual review** -- Load page in browser, verify Bootstrap 5 styling applied
|
||||
2. **Responsive check** -- Test at 320px, 768px, 1024px, 1440px breakpoints
|
||||
3. **Dark mode** -- Toggle `data-bs-theme="dark"`, verify all elements adapt
|
||||
4. **Functionality** -- All forms submit, AJAX works, WC JS hooks fire
|
||||
5. **Accessibility** -- Tab through page, check screen reader output
|
||||
6. **No regressions** -- Existing WooCommerce features still work (add to cart, checkout flow, etc.)
|
||||
|
||||
### Integration Test Flow
|
||||
|
||||
Once multiple phases are complete, test the full customer journey:
|
||||
|
||||
1. Browse shop archive --> filter --> view product
|
||||
2. Select variation --> add to cart --> view cart
|
||||
3. Update quantity --> apply coupon --> proceed to checkout
|
||||
4. Fill billing/shipping --> select payment --> place order
|
||||
5. View thank-you page --> check email
|
||||
6. Log in to My Account --> view order --> manage addresses
|
||||
|
||||
---
|
||||
|
||||
## Estimated Template Count by Phase
|
||||
|
||||
| Phase | Templates | Priority |
|
||||
| ----- | --------- | -------- |
|
||||
| 1 -- Global & Notices | 9 | Critical |
|
||||
| 2 -- Archive & Loop | 15 | Critical |
|
||||
| 3 -- Single Product | 21 | Critical |
|
||||
| 4 -- Cart | 9 | High |
|
||||
| 5 -- Checkout | 12 | High |
|
||||
| 6 -- My Account | 17 | Medium |
|
||||
| 7 -- Order Details | 5 | Medium |
|
||||
| 8 -- Emails | ~30 | Low |
|
||||
| 9 -- Supplementary | 7 | Low |
|
||||
| **Total** | **~125** | |
|
||||
|
||||
New reusable components to create: **8**
|
||||
|
||||
**Note:** Not all 233 WooCommerce templates need overrides. Block templates (`templates/templates/`, `parts/`), block-notice variants, and email plain-text variants are excluded or deferred. The ~125 count covers all customer-facing PHP templates that produce visible HTML.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Blockers
|
||||
|
||||
| Dependency | Status | Impact |
|
||||
| ---------- | ------ | ------ |
|
||||
| Docker environment | Not set up | Cannot test WP-CLI, i18n extraction; use local PHP server |
|
||||
| WooCommerce-to-Twig bridge | Implemented | `TemplateOverride` intercepts `wc_get_template()` via before/after hooks; `WooCommerceExtension` provides ~50 Twig functions and 7 filters |
|
||||
| Parent theme Twig functions | Available | `__()`, `esc_html()`, `esc_attr()`, etc. registered by `TwigService` |
|
||||
| Bootstrap 5 CSS/JS | Available | Loaded by parent theme `wp-bootstrap` |
|
||||
| Composer autoloader | Available | PSR-4 for `WcBootstrap\` namespace |
|
||||
|
||||
---
|
||||
|
||||
## File Checklist
|
||||
|
||||
Track completion per file. Mark with `[x]` when done.
|
||||
|
||||
### Phase 1 -- Global & Notices
|
||||
|
||||
- [x] `global/wrapper-start.html.twig`
|
||||
- [x] `global/wrapper-end.html.twig`
|
||||
- [x] `global/breadcrumb.html.twig`
|
||||
- [x] `global/sidebar.html.twig`
|
||||
- [x] `global/quantity-input.html.twig`
|
||||
- [x] `global/form-login.html.twig`
|
||||
- [x] `notices/notice.html.twig`
|
||||
- [x] `notices/error.html.twig`
|
||||
- [x] `notices/success.html.twig`
|
||||
|
||||
### Phase 2 -- Archive & Loop
|
||||
|
||||
- [x] `archive-product.html.twig`
|
||||
- [x] `content-product.html.twig`
|
||||
- [x] `content-product-cat.html.twig`
|
||||
- [x] `product-searchform.html.twig`
|
||||
- [x] `loop/loop-start.html.twig`
|
||||
- [x] `loop/loop-end.html.twig`
|
||||
- [x] `loop/header.html.twig`
|
||||
- [x] `loop/result-count.html.twig`
|
||||
- [x] `loop/orderby.html.twig`
|
||||
- [x] `loop/pagination.html.twig`
|
||||
- [x] `loop/no-products-found.html.twig`
|
||||
- [x] `loop/add-to-cart.html.twig`
|
||||
- [x] `loop/price.html.twig`
|
||||
- [x] `loop/rating.html.twig`
|
||||
- [x] `loop/sale-flash.html.twig`
|
||||
|
||||
### Phase 3 -- Single Product
|
||||
|
||||
- [x] `single-product/product-image.html.twig`
|
||||
- [x] `single-product/title.html.twig`
|
||||
- [x] `single-product/price.html.twig`
|
||||
- [x] `single-product/short-description.html.twig`
|
||||
- [x] `single-product/meta.html.twig`
|
||||
- [x] `single-product/rating.html.twig`
|
||||
- [x] `single-product/stock.html.twig`
|
||||
- [x] `single-product/sale-flash.html.twig`
|
||||
- [x] `single-product/share.html.twig`
|
||||
- [x] `single-product/product-attributes.html.twig`
|
||||
- [x] `single-product/related.html.twig`
|
||||
- [x] `single-product/up-sells.html.twig`
|
||||
- [x] `single-product/add-to-cart/simple.html.twig`
|
||||
- [x] `single-product/add-to-cart/variable.html.twig`
|
||||
- [x] `single-product/add-to-cart/grouped.html.twig`
|
||||
- [x] `single-product/add-to-cart/external.html.twig`
|
||||
- [x] `single-product/add-to-cart/variation.html.twig`
|
||||
- [x] `single-product/add-to-cart/variation-add-to-cart-button.html.twig`
|
||||
- [x] `single-product/tabs/tabs.html.twig`
|
||||
- [x] `single-product/tabs/description.html.twig`
|
||||
- [x] `single-product/tabs/additional-information.html.twig`
|
||||
|
||||
### Phase 4 -- Cart
|
||||
|
||||
- [x] `cart/cart.html.twig`
|
||||
- [x] `cart/cart-empty.html.twig`
|
||||
- [x] `cart/cart-item-data.html.twig`
|
||||
- [x] `cart/cart-totals.html.twig`
|
||||
- [x] `cart/cart-shipping.html.twig`
|
||||
- [x] `cart/cross-sells.html.twig`
|
||||
- [x] `cart/mini-cart.html.twig`
|
||||
- [x] `cart/proceed-to-checkout-button.html.twig`
|
||||
- [x] `cart/shipping-calculator.html.twig`
|
||||
|
||||
### Phase 5 -- Checkout
|
||||
|
||||
- [x] `checkout/form-checkout.html.twig`
|
||||
- [x] `checkout/form-billing.html.twig`
|
||||
- [x] `checkout/form-shipping.html.twig`
|
||||
- [x] `checkout/form-coupon.html.twig`
|
||||
- [x] `checkout/form-login.html.twig`
|
||||
- [x] `checkout/review-order.html.twig`
|
||||
- [x] `checkout/payment.html.twig`
|
||||
- [x] `checkout/payment-method.html.twig`
|
||||
- [x] `checkout/terms.html.twig`
|
||||
- [x] `checkout/thankyou.html.twig`
|
||||
- [x] `checkout/order-received.html.twig`
|
||||
- [x] `checkout/cart-errors.html.twig`
|
||||
|
||||
### Phase 6 -- My Account
|
||||
|
||||
- [x] `myaccount/my-account.html.twig`
|
||||
- [x] `myaccount/navigation.html.twig`
|
||||
- [x] `myaccount/dashboard.html.twig`
|
||||
- [x] `myaccount/orders.html.twig`
|
||||
- [x] `myaccount/view-order.html.twig`
|
||||
- [x] `myaccount/my-address.html.twig`
|
||||
- [x] `myaccount/form-edit-address.html.twig`
|
||||
- [x] `myaccount/form-edit-account.html.twig`
|
||||
- [x] `myaccount/downloads.html.twig`
|
||||
- [x] `myaccount/payment-methods.html.twig`
|
||||
- [x] `myaccount/form-add-payment-method.html.twig`
|
||||
- [x] `myaccount/form-login.html.twig`
|
||||
- [x] `myaccount/form-lost-password.html.twig`
|
||||
- [x] `myaccount/form-reset-password.html.twig`
|
||||
- [x] `myaccount/lost-password-confirmation.html.twig`
|
||||
|
||||
### Phase 7 -- Order Details
|
||||
|
||||
- [x] `order/order-details.html.twig`
|
||||
- [x] `order/order-details-item.html.twig`
|
||||
- [x] `order/order-details-customer.html.twig`
|
||||
- [x] `order/form-tracking.html.twig`
|
||||
- [x] `order/order-again.html.twig`
|
||||
|
||||
### Phase 8 -- Emails (SKIPPED)
|
||||
|
||||
Skipped: WooCommerce email templates use `wc_get_template_html()` which bypasses
|
||||
the Twig rendering pipeline. Email overrides would require traditional PHP files
|
||||
in `woocommerce/emails/`, breaking the Twig-only pattern. Default WooCommerce
|
||||
email templates are sufficient; email customization can be handled via plugins
|
||||
(e.g., Kadence WooCommerce Email Designer) or the WooCommerce block email editor.
|
||||
|
||||
### Phase 9 -- Supplementary
|
||||
|
||||
- [x] `brands/brand-description.html.twig`
|
||||
- [x] `brands/taxonomy-product_brand.html.twig`
|
||||
- [x] `brands/shortcodes/brands-a-z.html.twig`
|
||||
- [x] `brands/shortcodes/single-brand.html.twig`
|
||||
- [x] `auth/form-login.html.twig`
|
||||
- [x] `auth/form-grant-access.html.twig`
|
||||
- [x] `single-product/back-in-stock-form.html.twig`
|
||||
|
||||
### Reusable Components
|
||||
|
||||
- [x] `components/price.html.twig`
|
||||
- [x] `components/rating.html.twig`
|
||||
- [x] `components/address-card.html.twig`
|
||||
- [x] `components/status-badge.html.twig`
|
||||
- [x] `components/quantity-input.html.twig`
|
||||
- [x] `components/form-field.html.twig`
|
||||
10
README.md
10
README.md
@@ -57,7 +57,9 @@ The bridge hooks into WooCommerce's `woocommerce_before_template_part` and `wooc
|
||||
wc-bootstrap/
|
||||
├── assets/
|
||||
│ ├── css/wc-bootstrap.css # Bootstrap override styles
|
||||
│ └── js/quantity.js # Quantity +/- button handler
|
||||
│ └── js/
|
||||
│ ├── product-gallery.js # Thumbnail click-to-swap gallery handler
|
||||
│ └── quantity.js # Quantity +/- button handler
|
||||
├── inc/
|
||||
│ ├── TemplateOverride.php # WC template interception (before/after hooks)
|
||||
│ └── Twig/
|
||||
@@ -68,7 +70,7 @@ wc-bootstrap/
|
||||
│ ├── layouts/ # Page-type layouts (account, archive, form, page, single)
|
||||
│ ├── archive-product.html.twig
|
||||
│ ├── content-product.html.twig
|
||||
│ ├── content-product-cat.html.twig
|
||||
│ ├── content-product_cat.html.twig
|
||||
│ ├── product-searchform.html.twig
|
||||
│ ├── auth/
|
||||
│ ├── brands/
|
||||
@@ -141,8 +143,8 @@ for po in languages/wc-bootstrap-*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
||||
Releases are automated via Gitea Actions. Push a tag matching `vX.X.X` to trigger a release build.
|
||||
|
||||
```bash
|
||||
git tag -a v0.1.1 -m "Version 0.1.1 - Bugfix release"
|
||||
git push origin v0.1.1
|
||||
git tag -a v0.1.5 -m "Version 0.1.5 - Fix 10 known bugs"
|
||||
git push origin v0.1.5
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
182
SETUP.md
182
SETUP.md
@@ -1,182 +0,0 @@
|
||||
# WP Bootstrap Child Theme Template -- Setup Guide
|
||||
|
||||
This template creates a Bootstrap 5 child theme for the `wp-bootstrap` parent theme
|
||||
that overrides a WordPress plugin's Twig templates with Bootstrap 5 markup.
|
||||
|
||||
## Placeholders Reference
|
||||
|
||||
Search and replace these placeholders across all files when instantiating a new project:
|
||||
|
||||
### Project Identity
|
||||
|
||||
| Placeholder | Description | Example |
|
||||
| --- | --- | --- |
|
||||
| `WooCommerce Bootstrap` | Human-readable theme name | `WP JobRoom Theme` |
|
||||
| `wc-bootstrap` | WordPress theme directory name (kebab-case) | `wp-jobroom-theme` |
|
||||
| `wc-bootstrap` | WordPress text domain for i18n | `wp-jobroom-theme` |
|
||||
| `WC_BOOTSTRAP` | PHP constant prefix (UPPER_SNAKE_CASE) | `WP_JOBROOM_THEME` |
|
||||
| `wc_bootstrap` | PHP function prefix (lower_snake_case) | `wp_jobroom_theme` |
|
||||
| `WcBootstrap` | PSR-4 PHP namespace | `WPJobroomTheme` |
|
||||
| `magdev` | Composer vendor name | `magdev` |
|
||||
|
||||
### Plugin (upstream dependency)
|
||||
|
||||
| Placeholder | Description | Example |
|
||||
| --- | --- | --- |
|
||||
| `WooCommerce` | Human-readable plugin name | `WP JobRoom` |
|
||||
| `woocommerce` | Plugin directory/handle name | `wp-jobroom` |
|
||||
| `woocommerce` | Plugin text domain | `wp-jobroom` |
|
||||
| `Magdev\Woocommerce` | Plugin PHP namespace | `Magdev\WpJobroom` |
|
||||
| `https://github.com/woocommerce/woocommerce.git` | Plugin repository URL | `https://src.example.com/user/wp-myplugin` |
|
||||
| `woocommerce_render_page` | Plugin's render page filter name | `wp_jobroom_render_page` |
|
||||
| `woocommerce_is_theme_wrapped` | Plugin's is-wrapped filter name | `wp_jobroom_is_theme_wrapped` |
|
||||
|
||||
### Author & Repository
|
||||
|
||||
| Placeholder | Description | Example |
|
||||
| --- | --- | --- |
|
||||
| `Marco Grätsch` | Author's full name | `Marco Graetsch` |
|
||||
| `magdev3.0@gmail.com` | Author's email | `magdev3.0@gmail.com` |
|
||||
| `https://src.bundespruefstelle.ch/magdev` | Author's website URL | `https://src.example.com/user` |
|
||||
| `ssh://git@src.bundespruefstelle.ch:2022/magdev/wc-bootstrap.git` | Theme repository URL | `https://src.example.com/user/wp-myplugin-theme` |
|
||||
| `ssh://git@src.bundespruefstelle.ch:2022/magdev/wc-bootstrap.git/issues` | Issues tracker URL | `https://src.example.com/user/wp-myplugin-theme/issues` |
|
||||
| `https://src.bundespruefstelle.ch/magdev/wp-bootstrap` | Parent theme repository URL | `https://src.example.com/user/wp-bootstrap` |
|
||||
|
||||
### Infrastructure
|
||||
|
||||
| Placeholder | Description | Example |
|
||||
| --- | --- | --- |
|
||||
| `woocommerce-wordpress` | Docker container name for WP-CLI | `myplugin-wordpress` |
|
||||
|
||||
## Files to Customize
|
||||
|
||||
After replacing placeholders, these files need project-specific customization:
|
||||
|
||||
### Required Changes
|
||||
|
||||
1. **`inc/TemplateOverride.php`** -- Update the `use` import to match your plugin's
|
||||
actual Template class path (line: `use Magdev\Woocommerce\Frontend\Template;`)
|
||||
|
||||
2. **`functions.php`** -- Review and adapt:
|
||||
- CSS dependency array in `enqueue_styles()` -- add plugin style handles
|
||||
- Filter hooks -- match your plugin's actual filter names
|
||||
- Remove or adapt features you don't need (sticky header, register page, etc.)
|
||||
|
||||
3. **`composer.json`** -- Verify namespace mapping matches your `inc/` directory structure
|
||||
|
||||
4. **`style.css`** -- Update the header to match your theme's specifics
|
||||
|
||||
5. **`.gitea/workflows/release.yml`** -- Update theme name in release title
|
||||
|
||||
### Optional Additions
|
||||
|
||||
6. **`inc/ProfileMenu.php`** -- Create if your plugin has a navigation menu that needs
|
||||
Bootstrap 5 conversion (use wp-jobroom-theme's ProfileMenu.php as reference)
|
||||
|
||||
7. **`assets/css/theme-overrides.css`** -- Rename to `wc-bootstrap.css` and add
|
||||
plugin-specific CSS class overrides mapping to Bootstrap 5
|
||||
|
||||
8. **`templates/`** -- Add template overrides mirroring your plugin's template structure
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```txt
|
||||
wp-bootstrap (parent theme, Bootstrap 5 FSE + Twig rendering)
|
||||
+-- wc-bootstrap (child theme, overrides plugin Twig templates with Bootstrap 5)
|
||||
+-- woocommerce (plugin, provides post types, logic, base Twig templates)
|
||||
```
|
||||
|
||||
### Template Override Flow
|
||||
|
||||
1. Plugin registers Twig `FilesystemLoader` with its `templates/` directory
|
||||
2. Child theme's `TemplateOverride` hooks `init` at priority 20 (after plugin at 0)
|
||||
3. `prependPath()` adds child theme's `templates/` before plugin's
|
||||
4. Twig resolves templates: child theme first, plugin as fallback
|
||||
|
||||
### Page Rendering Flow
|
||||
|
||||
1. Plugin's Router catches a request and renders plugin content via Twig
|
||||
2. Plugin fires `woocommerce_render_page` filter with pre-rendered HTML
|
||||
3. Child theme's `render_page()` intercepts, delegates to parent theme's TwigService
|
||||
4. Parent theme wraps content in its page shell (header, footer, navigation)
|
||||
5. `_theme_wrapped` context flag tells plugin templates to suppress their own wrapper
|
||||
|
||||
### CSS Cascade
|
||||
|
||||
```txt
|
||||
1. wp-bootstrap (parent) -- Bootstrap 5 framework
|
||||
2. woocommerce -- Plugin's custom CSS
|
||||
3. wc-bootstrap-style -- Child theme style.css (metadata)
|
||||
4. wc-bootstrap-overrides -- Bootstrap 5 overrides for plugin classes
|
||||
```
|
||||
|
||||
## Plugin Requirements
|
||||
|
||||
For this template to work, the plugin must:
|
||||
|
||||
1. **Use Twig with `FilesystemLoader`** -- for template overriding via `prependPath()`
|
||||
2. **Expose a singleton Template class** -- so the child theme can access the Twig environment
|
||||
3. **Fire a render page filter** -- so the child theme can delegate rendering to the parent theme
|
||||
4. **Fire an is-wrapped filter** -- so plugin templates know to suppress their outer wrapper
|
||||
5. **Register its styles with a known handle** -- for CSS dependency chain ordering
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Clone/copy this template
|
||||
cp -r wp-theme-template/ wp-content/themes/my-new-theme/
|
||||
|
||||
# 2. Replace all placeholders (example using sed)
|
||||
cd wp-content/themes/my-new-theme/
|
||||
find . -type f \( -name "*.php" -o -name "*.md" -o -name "*.css" -o -name "*.json" -o -name "*.yml" -o -name "*.twig" \) \
|
||||
-exec sed -i 's/WooCommerce Bootstrap/My Plugin Theme/g' {} +
|
||||
# ... repeat for all placeholders
|
||||
|
||||
# 3. Rename the CSS override file
|
||||
mv assets/css/theme-overrides.css assets/css/my-plugin-theme.css
|
||||
|
||||
# 4. Install dependencies
|
||||
composer install
|
||||
|
||||
# 5. Initialize git
|
||||
git init && git checkout -b dev
|
||||
git add . && git commit -m "Initial theme scaffold"
|
||||
|
||||
# 6. Start overriding plugin templates in templates/
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Adding a New Template Override
|
||||
|
||||
1. Find the plugin's template in `plugins/woocommerce/templates/path/file.html.twig`
|
||||
2. Create the same path in `themes/wc-bootstrap/templates/path/file.html.twig`
|
||||
3. Convert HTML to Bootstrap 5 components
|
||||
4. Preserve all context variables and block names from the original
|
||||
5. Preserve plugin JS-bound CSS classes (repeater fields, interactive widgets)
|
||||
|
||||
### Layout Hierarchy
|
||||
|
||||
```txt
|
||||
base.html.twig -- Notifications, breadcrumbs, container wrapping
|
||||
+-- layouts/page.html.twig -- Standard content pages
|
||||
+-- layouts/form.html.twig -- Auth and edit forms (centered card)
|
||||
+-- layouts/single.html.twig -- Detail pages (8+4 with sidebar)
|
||||
+-- layouts/archive.html.twig -- Search/list pages (3+9 with filters)
|
||||
+-- layouts/account.html.twig -- User account pages
|
||||
```
|
||||
|
||||
### Bootstrap 5 Component Mappings
|
||||
|
||||
| Plugin Pattern | Bootstrap 5 Equivalent |
|
||||
| --- | --- |
|
||||
| Custom button classes | `btn btn-primary`, `btn-outline-*` |
|
||||
| Custom alert/notification | `alert alert-*` with `alert-dismissible` |
|
||||
| Custom grid system | `row`, `col-*`, `row-cols-*` |
|
||||
| Custom form fields | `form-control`, `form-floating`, `form-select` |
|
||||
| Custom cards | `card`, `card-body`, `card-title` |
|
||||
| Custom modal/dialog | `modal`, `modal-dialog`, `modal-content` |
|
||||
| Custom dropdown | `dropdown`, `dropdown-menu`, `data-bs-toggle` |
|
||||
| Custom tabs | `nav-tabs`, `tab-content`, `tab-pane` |
|
||||
| Custom pagination | `pagination`, `page-item`, `page-link` |
|
||||
| Custom breadcrumb | `breadcrumb`, `breadcrumb-item` |
|
||||
@@ -42,12 +42,17 @@
|
||||
when notices are rendered outside our Twig templates.
|
||||
========================================================================== */
|
||||
|
||||
/* Use .woocommerce ancestor to beat woocommerce.css specificity (0,2,0 > 0,1,0).
|
||||
WooCommerce sets border-top: 3px solid, background-color: #f6f5f8, and a
|
||||
WooCommerce icon font ::before on these classes. Override all of it. */
|
||||
/* Override woocommerce.css which sets border-top: 3px solid, background-color:
|
||||
#f6f5f8, and a WooCommerce icon font ::before on notice classes.
|
||||
Two selector patterns at specificity 0,2,0:
|
||||
- .woocommerce .woocommerce-* — notices inside a .woocommerce wrapper
|
||||
- .alert.woocommerce-* — notices rendered by our Twig templates */
|
||||
.woocommerce .woocommerce-info,
|
||||
.woocommerce .woocommerce-message,
|
||||
.woocommerce .woocommerce-error {
|
||||
.woocommerce .woocommerce-error,
|
||||
.alert.woocommerce-info,
|
||||
.alert.woocommerce-message,
|
||||
.alert.woocommerce-error {
|
||||
position: relative;
|
||||
padding: 1rem 3rem 1rem 1rem;
|
||||
margin: 0 0 1rem;
|
||||
@@ -58,19 +63,22 @@
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.woocommerce .woocommerce-info {
|
||||
.woocommerce .woocommerce-info,
|
||||
.alert.woocommerce-info {
|
||||
color: var(--bs-info-text-emphasis);
|
||||
background-color: var(--bs-info-bg-subtle);
|
||||
border-color: var(--bs-info-border-subtle);
|
||||
}
|
||||
|
||||
.woocommerce .woocommerce-message {
|
||||
.woocommerce .woocommerce-message,
|
||||
.alert.woocommerce-message {
|
||||
color: var(--bs-success-text-emphasis);
|
||||
background-color: var(--bs-success-bg-subtle);
|
||||
border-color: var(--bs-success-border-subtle);
|
||||
}
|
||||
|
||||
.woocommerce .woocommerce-error {
|
||||
.woocommerce .woocommerce-error,
|
||||
.alert.woocommerce-error {
|
||||
color: var(--bs-danger-text-emphasis);
|
||||
background-color: var(--bs-danger-bg-subtle);
|
||||
border-color: var(--bs-danger-border-subtle);
|
||||
@@ -78,10 +86,24 @@
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
/* WooCommerce JS (woocommerce.js:focus_populate_live_region) adds tabindex="-1"
|
||||
and calls .focus() on notices for screen reader accessibility. The default
|
||||
browser focus ring appears white in dark mode — suppress it since these are
|
||||
non-interactive elements (the focus is only for screen reader announcement). */
|
||||
.alert.woocommerce-info:focus,
|
||||
.alert.woocommerce-message:focus,
|
||||
.alert.woocommerce-error:focus {
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Suppress WooCommerce icon font ::before — our templates use Bootstrap Icons */
|
||||
.woocommerce .woocommerce-info::before,
|
||||
.woocommerce .woocommerce-message::before,
|
||||
.woocommerce .woocommerce-error::before {
|
||||
.woocommerce .woocommerce-error::before,
|
||||
.alert.woocommerce-info::before,
|
||||
.alert.woocommerce-message::before,
|
||||
.alert.woocommerce-error::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -206,18 +228,12 @@
|
||||
line-height: 2.5;
|
||||
}
|
||||
|
||||
.woocommerce-ordering select {
|
||||
display: inline-block;
|
||||
padding: 0.375rem 2.25rem 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: var(--bs-body-color);
|
||||
background-color: var(--bs-body-bg);
|
||||
border: var(--bs-border-width) solid var(--bs-border-color);
|
||||
border-radius: var(--bs-border-radius);
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
appearance: auto;
|
||||
/* Ordering select — Bootstrap's .form-select handles all styling.
|
||||
Remove duplicate rules that conflict with Bootstrap's dropdown arrow.
|
||||
WooCommerce's woocommerce-layout.css sets background-image on selects;
|
||||
ensure Bootstrap's chevron wins via appearance: none. */
|
||||
.woocommerce-ordering .form-select {
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -405,6 +421,16 @@
|
||||
border-color: var(--bs-border-color);
|
||||
}
|
||||
|
||||
/* Text inputs & textareas — same issue as <select>: woocommerce.css sets
|
||||
.form-row .input-text { background-color: var(--wc-form-color-background, #fff) }
|
||||
with higher specificity than the theme's checkout form rules. */
|
||||
[data-bs-theme="dark"] .woocommerce .form-row .input-text,
|
||||
[data-bs-theme="dark"] .woocommerce .form-row textarea {
|
||||
background-color: var(--bs-body-bg);
|
||||
color: var(--bs-body-color);
|
||||
border-color: var(--bs-border-color);
|
||||
}
|
||||
|
||||
/* SelectWoo / Select2 — select2.css hardcodes #fff backgrounds on selection
|
||||
containers and dropdowns. Override to use Bootstrap's dark mode variables. */
|
||||
[data-bs-theme="dark"] .select2-container--default .select2-selection--single,
|
||||
@@ -445,6 +471,19 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Variable product attribute selectors — woocommerce.css sets:
|
||||
.woocommerce div.product form.cart .variations select { background: url(...) no-repeat }
|
||||
at specificity (0,4,3). We must match or exceed that.
|
||||
Override both background-color (white fallback) and background-image (black SVG chevron)
|
||||
so Bootstrap's dark-mode form-select styling takes effect. */
|
||||
[data-bs-theme="dark"] .woocommerce div.product form.cart .variations select,
|
||||
[data-bs-theme="dark"] .woocommerce div.product .variations .form-select {
|
||||
background-color: var(--bs-body-bg);
|
||||
background-image: var(--bs-form-select-bg-img);
|
||||
color: var(--bs-body-color);
|
||||
border-color: var(--bs-border-color);
|
||||
}
|
||||
|
||||
/* Checkout form focus color for dark mode */
|
||||
[data-bs-theme="dark"] .woocommerce-checkout .form-row input.input-text:focus,
|
||||
[data-bs-theme="dark"] .woocommerce-checkout .form-row textarea:focus,
|
||||
|
||||
56
assets/js/product-gallery.js
Normal file
56
assets/js/product-gallery.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Product Gallery Thumbnail Click Handler
|
||||
*
|
||||
* Swaps the main product image when a gallery thumbnail is clicked.
|
||||
* The first thumbnail is the main product image (always included in the strip).
|
||||
* Also handles gallery fade-in (WooCommerce sets opacity: 0 by default).
|
||||
*
|
||||
* @package WcBootstrap
|
||||
* @since 0.1.5
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function initGallery() {
|
||||
var gallery = document.querySelector('.woocommerce-product-gallery');
|
||||
if (!gallery) return;
|
||||
|
||||
// Fade in the gallery (WooCommerce expects JS to set opacity: 1).
|
||||
gallery.style.opacity = '1';
|
||||
|
||||
var mainImageContainer = gallery.querySelector('.woocommerce-product-gallery__image');
|
||||
if (!mainImageContainer) return;
|
||||
|
||||
var mainImage = mainImageContainer.querySelector('img');
|
||||
if (!mainImage) return;
|
||||
|
||||
var thumbs = gallery.querySelectorAll('.wc-gallery-thumb');
|
||||
if (!thumbs.length) return;
|
||||
|
||||
thumbs.forEach(function (thumb) {
|
||||
thumb.style.cursor = 'pointer';
|
||||
thumb.addEventListener('click', function () {
|
||||
var fullSrc = this.getAttribute('data-full-src');
|
||||
if (!fullSrc) return;
|
||||
|
||||
mainImage.setAttribute('src', fullSrc);
|
||||
|
||||
// Update active state.
|
||||
thumbs.forEach(function (t) {
|
||||
t.style.opacity = '0.6';
|
||||
t.classList.remove('border-primary', 'active');
|
||||
t.classList.add('border');
|
||||
});
|
||||
this.style.opacity = '1';
|
||||
this.classList.add('border-primary', 'active');
|
||||
this.classList.remove('border');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initGallery);
|
||||
} else {
|
||||
initGallery();
|
||||
}
|
||||
})();
|
||||
122
functions.php
122
functions.php
@@ -17,15 +17,8 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
|
||||
/**
|
||||
* Define theme constants.
|
||||
*
|
||||
* CRITICAL: WordPress reads the version from TWO places:
|
||||
* 1. style.css header "Version:" — WordPress uses THIS for admin display
|
||||
* 2. This PHP constant — used internally by the theme
|
||||
* Both MUST be updated on every release.
|
||||
*/
|
||||
define( 'WC_BOOTSTRAP_VERSION', '0.1.0' );
|
||||
define( 'WC_BOOTSTRAP_PATH', get_stylesheet_directory() . '/' );
|
||||
define( 'WC_BOOTSTRAP_URL', get_stylesheet_directory_uri() . '/' );
|
||||
|
||||
/**
|
||||
* Load Composer autoloader if present.
|
||||
@@ -137,9 +130,68 @@ function wc_bootstrap_enqueue_scripts(): void {
|
||||
$theme_version,
|
||||
true
|
||||
);
|
||||
|
||||
// Product gallery thumbnail handler for single product pages.
|
||||
if ( function_exists( 'is_product' ) && is_product() ) {
|
||||
wp_enqueue_script(
|
||||
'wc-bootstrap-gallery',
|
||||
get_stylesheet_directory_uri() . '/assets/js/product-gallery.js',
|
||||
array(),
|
||||
$theme_version,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_scripts' );
|
||||
|
||||
/**
|
||||
* Build the parent theme context for a page render.
|
||||
*
|
||||
* Caches the ContextBuilder result per request to avoid redundant database
|
||||
* queries when multiple WooCommerce rendering functions need the same context.
|
||||
*
|
||||
* @since 0.1.1
|
||||
*
|
||||
* @return array Theme context array.
|
||||
*/
|
||||
function wc_bootstrap_get_theme_context(): array {
|
||||
static $cached_context = null;
|
||||
|
||||
if ( null === $cached_context ) {
|
||||
$context_builder = new \WPBootstrap\Template\ContextBuilder();
|
||||
$cached_context = $context_builder->build();
|
||||
}
|
||||
|
||||
return $cached_context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render content inside the parent theme's page shell.
|
||||
*
|
||||
* Injects the given HTML content into the parent theme's page template,
|
||||
* replacing the post content. Title and thumbnail are blanked so the
|
||||
* parent theme does not render its own headings — the content handles that.
|
||||
*
|
||||
* @since 0.1.1
|
||||
*
|
||||
* @param string $content HTML content to render inside the page shell.
|
||||
*/
|
||||
function wc_bootstrap_render_in_page_shell( string $content ): void {
|
||||
$theme_context = wc_bootstrap_get_theme_context();
|
||||
$twig = \WPBootstrap\Twig\TwigService::getInstance();
|
||||
|
||||
$theme_context['post'] = array_merge(
|
||||
$theme_context['post'] ?? [],
|
||||
[
|
||||
'content' => $content,
|
||||
'title' => '',
|
||||
'thumbnail' => '',
|
||||
]
|
||||
);
|
||||
|
||||
echo $twig->render( 'pages/page.html.twig', $theme_context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle plugin page rendering via plugin render filter.
|
||||
*
|
||||
@@ -157,26 +209,10 @@ add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_scripts' );
|
||||
function wc_bootstrap_render_page( bool $rendered, string $content, array $context ): bool {
|
||||
if ( ! class_exists( '\WPBootstrap\Twig\TwigService' )
|
||||
|| ! class_exists( '\WPBootstrap\Template\ContextBuilder' ) ) {
|
||||
return false; // Can't render, let plugin use its own fallback
|
||||
return false;
|
||||
}
|
||||
|
||||
$context_builder = new \WPBootstrap\Template\ContextBuilder();
|
||||
$theme_context = $context_builder->build();
|
||||
$twig = \WPBootstrap\Twig\TwigService::getInstance();
|
||||
|
||||
// Inject plugin content as the page post content so page.html.twig renders it
|
||||
// inside the standard content block. Title is empty so the parent theme does not
|
||||
// render its own <h1> — plugin templates handle their own headings.
|
||||
$theme_context['post'] = array_merge(
|
||||
$theme_context['post'] ?? [],
|
||||
[
|
||||
'content' => $content,
|
||||
'title' => '',
|
||||
'thumbnail' => '',
|
||||
]
|
||||
);
|
||||
|
||||
echo $twig->render( 'pages/page.html.twig', $theme_context );
|
||||
wc_bootstrap_render_in_page_shell( $content );
|
||||
return true;
|
||||
}
|
||||
add_filter( 'woocommerce_render_page', 'wc_bootstrap_render_page', 10, 3 );
|
||||
@@ -253,7 +289,7 @@ add_action( 'widgets_init', 'wc_bootstrap_register_sidebars' );
|
||||
* @since 0.1.0
|
||||
*/
|
||||
function wc_bootstrap_loop_columns(): int {
|
||||
return 4;
|
||||
return 3;
|
||||
}
|
||||
add_filter( 'loop_shop_columns', 'wc_bootstrap_loop_columns' );
|
||||
|
||||
@@ -359,26 +395,11 @@ function wc_bootstrap_render_product_archive(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture WooCommerce archive content via output buffering.
|
||||
ob_start();
|
||||
include get_stylesheet_directory() . '/woocommerce/archive-product.php';
|
||||
$content = ob_get_clean();
|
||||
|
||||
// Build parent theme context and inject archive content into page shell.
|
||||
$context_builder = new \WPBootstrap\Template\ContextBuilder();
|
||||
$theme_context = $context_builder->build();
|
||||
$twig = \WPBootstrap\Twig\TwigService::getInstance();
|
||||
|
||||
$theme_context['post'] = array_merge(
|
||||
$theme_context['post'] ?? [],
|
||||
[
|
||||
'content' => $content,
|
||||
'title' => '',
|
||||
'thumbnail' => '',
|
||||
]
|
||||
);
|
||||
|
||||
echo $twig->render( 'pages/page.html.twig', $theme_context );
|
||||
wc_bootstrap_render_in_page_shell( $content );
|
||||
exit;
|
||||
}
|
||||
add_action( 'template_redirect', 'wc_bootstrap_render_product_archive', 11 );
|
||||
@@ -406,26 +427,11 @@ function wc_bootstrap_render_single_product(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture WooCommerce single product content via output buffering.
|
||||
ob_start();
|
||||
include get_stylesheet_directory() . '/woocommerce/single-product.php';
|
||||
$content = ob_get_clean();
|
||||
|
||||
// Build parent theme context and inject product content into page shell.
|
||||
$context_builder = new \WPBootstrap\Template\ContextBuilder();
|
||||
$theme_context = $context_builder->build();
|
||||
$twig = \WPBootstrap\Twig\TwigService::getInstance();
|
||||
|
||||
$theme_context['post'] = array_merge(
|
||||
$theme_context['post'] ?? [],
|
||||
[
|
||||
'content' => $content,
|
||||
'title' => '',
|
||||
'thumbnail' => '',
|
||||
]
|
||||
);
|
||||
|
||||
echo $twig->render( 'pages/page.html.twig', $theme_context );
|
||||
wc_bootstrap_render_in_page_shell( $content );
|
||||
exit;
|
||||
}
|
||||
add_action( 'template_redirect', 'wc_bootstrap_render_single_product', 11 );
|
||||
|
||||
@@ -37,6 +37,9 @@ class WooCommerceExtension extends AbstractExtension {
|
||||
new TwigFilter( 'esc_attr', 'esc_attr', [ 'is_safe' => [ 'html' ] ] ),
|
||||
new TwigFilter( 'esc_url', 'esc_url', [ 'is_safe' => [ 'html' ] ] ),
|
||||
|
||||
// Slug/sanitize filters.
|
||||
new TwigFilter( 'sanitize_title', 'sanitize_title' ),
|
||||
|
||||
// Text processing filters.
|
||||
new TwigFilter( 'wpautop', 'wpautop', [ 'is_safe' => [ 'html' ] ] ),
|
||||
new TwigFilter( 'wp_kses_post', 'wp_kses_post', [ 'is_safe' => [ 'html' ] ] ),
|
||||
@@ -107,6 +110,10 @@ class WooCommerceExtension extends AbstractExtension {
|
||||
// Dynamic function calls.
|
||||
new TwigFunction( 'call_user_func', [ $this, 'callUserFunc' ], [ 'is_safe' => [ 'html' ] ] ),
|
||||
new TwigFunction( 'fn', [ $this, 'callFunction' ] ),
|
||||
|
||||
// Product loop helpers (set global $product for WC hooks in Twig loops).
|
||||
new TwigFunction( 'wc_setup_product_data', [ $this, 'setupProductData' ] ),
|
||||
new TwigFunction( 'wp_reset_postdata', 'wp_reset_postdata' ),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -235,24 +242,67 @@ class WooCommerceExtension extends AbstractExtension {
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a PHP function by name and return its result.
|
||||
* Allowlist of PHP functions that can be called via fn() in Twig templates.
|
||||
*
|
||||
* Prevents arbitrary function execution (e.g., exec, system) if template
|
||||
* context were ever compromised. Only functions actually used in templates
|
||||
* are permitted.
|
||||
*/
|
||||
private const ALLOWED_FUNCTIONS = [
|
||||
'WC',
|
||||
'_n',
|
||||
'get_pagenum_link',
|
||||
'wc_review_ratings_enabled',
|
||||
'wc_get_product_category_list',
|
||||
'wc_get_product_tag_list',
|
||||
'woocommerce_page_title',
|
||||
'wc_get_customer_available_downloads',
|
||||
];
|
||||
|
||||
/**
|
||||
* Call a whitelisted PHP function by name and return its result.
|
||||
*
|
||||
* Enables `fn('WC')` in templates to access the WooCommerce singleton
|
||||
* and chain method calls via Twig's property accessor.
|
||||
*
|
||||
* Only functions in the ALLOWED_FUNCTIONS list can be called. This prevents
|
||||
* arbitrary code execution if template context were ever compromised.
|
||||
*
|
||||
* @param string $name Function name.
|
||||
* @param mixed ...$args Arguments.
|
||||
* @return mixed Function return value.
|
||||
*
|
||||
* @throws \RuntimeException If function does not exist.
|
||||
* @throws \RuntimeException If function is not allowed or does not exist.
|
||||
*/
|
||||
public function callFunction( string $name, ...$args ): mixed {
|
||||
if ( ! in_array( $name, self::ALLOWED_FUNCTIONS, true ) ) {
|
||||
throw new \RuntimeException( "Function {$name} is not allowed. Add it to ALLOWED_FUNCTIONS." );
|
||||
}
|
||||
if ( ! function_exists( $name ) ) {
|
||||
throw new \RuntimeException( "Function {$name} does not exist." );
|
||||
}
|
||||
return $name( ...$args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up global product data for WC hook-based rendering in Twig loops.
|
||||
*
|
||||
* WooCommerce hooks (woocommerce_before_shop_loop_item, etc.) read from
|
||||
* the global $product. When iterating products in Twig (related, upsells),
|
||||
* the global must be updated before rendering each product card.
|
||||
*
|
||||
* @param \WC_Product $product Product object.
|
||||
* @return string Empty string (Twig requires a return value).
|
||||
*/
|
||||
public function setupProductData( \WC_Product $product ): string {
|
||||
$GLOBALS['product'] = $product;
|
||||
$post = get_post( $product->get_id() );
|
||||
if ( $post ) {
|
||||
setup_postdata( $GLOBALS['post'] = $post );
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture wc_print_notices() output.
|
||||
*
|
||||
|
||||
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 349 KiB |
@@ -7,7 +7,7 @@ Description: A Bootstrap 5 child theme for WP Bootstrap that overrides all WooCo
|
||||
Requires at least: 6.7
|
||||
Tested up to: 6.7
|
||||
Requires PHP: 8.3
|
||||
Version: 0.1.1
|
||||
Version: 0.1.5
|
||||
License: GNU General Public License v2 or later
|
||||
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
||||
Template: wp-bootstrap
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle shop_table shop_table_responsive cart woocommerce-cart-form__contents">
|
||||
<thead class="table-light">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="product-thumbnail" scope="col" style="width: 80px;">
|
||||
<span class="visually-hidden">{{ __('Thumbnail') }}</span>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="card shadow-sm woocommerce-checkout-review-order-table">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0 shop_table woocommerce-checkout-review-order-table">
|
||||
<thead class="table-light">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="product-name" scope="col">{{ __('Product') }}</th>
|
||||
<th class="product-total text-end" scope="col">{{ __('Subtotal') }}</th>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<div class="alert alert-success mb-4" role="alert">
|
||||
<div class="alert alert-success d-flex align-items-center mb-4" role="alert">
|
||||
<i class="bi bi-check-circle me-2" aria-hidden="true"></i>
|
||||
{% include 'checkout/order-received.html.twig' with { order: order } %}
|
||||
</div>
|
||||
@@ -68,7 +68,7 @@
|
||||
{{ do_action('woocommerce_thankyou_' ~ order.get_payment_method(), order.get_id()) }}
|
||||
{{ do_action('woocommerce_thankyou', order.get_id()) }}
|
||||
{% else %}
|
||||
<div class="alert alert-success mb-4" role="alert">
|
||||
<div class="alert alert-success d-flex align-items-center mb-4" role="alert">
|
||||
<i class="bi bi-check-circle me-2" aria-hidden="true"></i>
|
||||
{% include 'checkout/order-received.html.twig' %}
|
||||
</div>
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
# Product Archive Header (Bootstrap 5 Override)
|
||||
#
|
||||
# Renders the archive page title and optional description.
|
||||
#
|
||||
# Expected context:
|
||||
# show_page_title - Whether to display the title (boolean, filtered)
|
||||
# page_title - Archive page title string
|
||||
# archive_description - Optional archive description HTML
|
||||
# Calls woocommerce_page_title() directly (matching the PHP template)
|
||||
# because wc_get_template('loop/header.php') passes no context args.
|
||||
#
|
||||
# WooCommerce PHP equivalent: loop/header.php
|
||||
#
|
||||
@@ -15,9 +12,9 @@
|
||||
#}
|
||||
|
||||
<header class="woocommerce-products-header mb-4">
|
||||
{% if show_page_title is not defined or show_page_title %}
|
||||
{% if apply_filters('woocommerce_show_page_title', true) %}
|
||||
<h1 class="woocommerce-products-header__title page-title mb-2">
|
||||
{{ page_title|default('')|esc_html }}
|
||||
{{ fn('woocommerce_page_title', false)|esc_html }}
|
||||
</h1>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
# @since 0.1.0
|
||||
#}
|
||||
|
||||
{% set cols = columns|default(4) %}
|
||||
{% set cols = columns|default(3) %}
|
||||
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-{{ cols }} g-4 products">
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
# @since 0.1.0
|
||||
#}
|
||||
|
||||
{# Match PHP template: use WC()->customer->get_downloadable_products() #}
|
||||
{% set downloads = fn('WC').customer.get_downloadable_products() %}
|
||||
{% set has_downloads = downloads is not empty %}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{% if has_orders %}
|
||||
<div class="table-responsive">
|
||||
<table class="woocommerce-orders-table table table-hover align-middle mb-4">
|
||||
<thead class="table-light">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for column_id, column_name in wc_get_account_orders_columns() %}
|
||||
<th scope="col">{{ column_name|esc_html }}</th>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{% if has_methods %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-4">
|
||||
<thead class="table-light">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for column_id, column_name in wc_get_account_payment_methods_columns() %}
|
||||
<th>{{ column_name|esc_html }}</th>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{% else %}
|
||||
<ul class="mb-0 ps-3">
|
||||
{% for notice in notices %}
|
||||
<li {{ notice.data|default('')|raw }}>
|
||||
<li {{ notice.data|default('')|wp_kses_post }}>
|
||||
{{ notice.notice|raw }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{% if notices is defined and notices|length > 0 %}
|
||||
{% for notice in notices %}
|
||||
<div class="alert alert-info alert-dismissible fade show woocommerce-info"
|
||||
{{ notice.data|default('')|raw }}
|
||||
{{ notice.data|default('')|wp_kses_post }}
|
||||
role="status">
|
||||
<i class="bi bi-info-circle me-2" aria-hidden="true"></i>
|
||||
{{ notice.notice|raw }}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{% if notices is defined and notices|length > 0 %}
|
||||
{% for notice in notices %}
|
||||
<div class="alert alert-success alert-dismissible fade show woocommerce-message"
|
||||
{{ notice.data|default('')|raw }}
|
||||
{{ notice.data|default('')|wp_kses_post }}
|
||||
role="alert">
|
||||
<i class="bi bi-check-circle me-2" aria-hidden="true"></i>
|
||||
{{ notice.notice|raw }}
|
||||
|
||||
@@ -31,51 +31,54 @@
|
||||
<section class="woocommerce-order-details">
|
||||
{{ do_action('woocommerce_order_details_before_order_table', order) }}
|
||||
|
||||
<h2 class="h5 mb-3">{{ __('Order details') }}</h2>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>{{ __('Product') }}</th>
|
||||
<th class="text-end">{{ __('Total') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{ do_action('woocommerce_order_details_before_order_table_items', order) }}
|
||||
|
||||
{% for item_id, item in order_items %}
|
||||
{% set product = item.get_product() %}
|
||||
{% include 'order/order-details-item.html.twig' with {
|
||||
order: order,
|
||||
item_id: item_id,
|
||||
item: item,
|
||||
show_purchase_note: show_purchase_note,
|
||||
purchase_note: product ? product.get_purchase_note() : '',
|
||||
product: product
|
||||
} %}
|
||||
{% endfor %}
|
||||
|
||||
{{ do_action('woocommerce_order_details_after_order_table_items', order) }}
|
||||
</tbody>
|
||||
|
||||
<tfoot>
|
||||
{% for key, total in order.get_order_item_totals() %}
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="h5 mb-0">{{ __('Order details') }}</h2>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="row">{{ total.label|esc_html }}</th>
|
||||
<td class="text-end">{{ total.value|wp_kses_post }}</td>
|
||||
<th>{{ __('Product') }}</th>
|
||||
<th class="text-end">{{ __('Total') }}</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</thead>
|
||||
|
||||
{% if order.get_customer_note() %}
|
||||
<tr>
|
||||
<th>{{ __('Note:') }}</th>
|
||||
<td class="text-end">{{ order.get_customer_note()|esc_html|nl2br }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tfoot>
|
||||
</table>
|
||||
<tbody>
|
||||
{{ do_action('woocommerce_order_details_before_order_table_items', order) }}
|
||||
|
||||
{% for item_id, item in order_items %}
|
||||
{% set product = item.get_product() %}
|
||||
{% include 'order/order-details-item.html.twig' with {
|
||||
order: order,
|
||||
item_id: item_id,
|
||||
item: item,
|
||||
show_purchase_note: show_purchase_note,
|
||||
purchase_note: product ? product.get_purchase_note() : '',
|
||||
product: product
|
||||
} %}
|
||||
{% endfor %}
|
||||
|
||||
{{ do_action('woocommerce_order_details_after_order_table_items', order) }}
|
||||
</tbody>
|
||||
|
||||
<tfoot>
|
||||
{% for key, total in order.get_order_item_totals() %}
|
||||
<tr>
|
||||
<th scope="row">{{ total.label|esc_html }}</th>
|
||||
<td class="text-end">{{ total.value|wp_kses_post }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% if order.get_customer_note() %}
|
||||
<tr>
|
||||
<th>{{ __('Note:') }}</th>
|
||||
<td class="text-end">{{ order.get_customer_note()|esc_html|nl2br }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ do_action('woocommerce_order_details_after_order_table', order) }}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
id="{{ field_id }}"
|
||||
class="form-control"
|
||||
placeholder="{{ __('Search products…') }}"
|
||||
value="{{ get_search_query() }}"
|
||||
value="{{ get_search_query()|esc_attr }}"
|
||||
name="s" />
|
||||
<button type="submit" class="btn btn-outline-primary" aria-label="{{ __('Search') }}">
|
||||
<i class="bi bi-search" aria-hidden="true"></i>
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
# Add-to-cart form for grouped products: table of child products with quantities.
|
||||
#
|
||||
# Expected context:
|
||||
# product - WC_Product_Grouped object
|
||||
# grouped_products - Array of child WC_Product objects
|
||||
# grouped_product_columns - Array of column definitions
|
||||
# quantites_required - Whether quantities are required
|
||||
# show_add_to_cart_button - Whether to show the submit button
|
||||
# product - WC_Product_Grouped object (global, injected by TemplateOverride)
|
||||
# grouped_products - Array of child WC_Product objects
|
||||
#
|
||||
# Note: quantites_required and show_add_to_cart_button are computed inside the
|
||||
# loop (matching WooCommerce's PHP template behavior), not passed as context.
|
||||
#
|
||||
# WooCommerce PHP equivalent: single-product/add-to-cart/grouped.php
|
||||
#
|
||||
@@ -21,19 +21,48 @@
|
||||
<form class="cart grouped_form" action="{{ product.get_permalink()|esc_url }}" method="post" enctype="multipart/form-data">
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="woocommerce-grouped-product-list group_table table table-borderless align-middle">
|
||||
{% set quantites_required = false %}
|
||||
{% set show_add_to_cart_button = false %}
|
||||
|
||||
{{ do_action('woocommerce_grouped_product_list_before') }}
|
||||
|
||||
{% for grouped_product in grouped_products %}
|
||||
{% set child_id = grouped_product.get_id() %}
|
||||
<tr id="product-{{ child_id }}" class="{{ grouped_product.get_stock_status() }}">
|
||||
|
||||
{# Set up global product data for each child (matches PHP's setup_postdata). #}
|
||||
{{ wc_setup_product_data(grouped_product) }}
|
||||
|
||||
{% if grouped_product.is_purchasable() and not grouped_product.has_options() %}
|
||||
{% set quantites_required = true %}
|
||||
{% endif %}
|
||||
{% if grouped_product.is_in_stock() %}
|
||||
{% set show_add_to_cart_button = true %}
|
||||
{% endif %}
|
||||
|
||||
<tr id="product-{{ child_id }}" class="woocommerce-grouped-product-list-item {{ grouped_product.get_stock_status() }}">
|
||||
<td class="woocommerce-grouped-product-list-item__quantity" style="width: 140px;">
|
||||
{% if grouped_product.is_purchasable() and grouped_product.is_in_stock() %}
|
||||
{% if not grouped_product.is_purchasable() or grouped_product.has_options() or not grouped_product.is_in_stock() %}
|
||||
{# Non-purchasable, has options (variable), or out-of-stock: show view link #}
|
||||
{% if grouped_product.is_visible() %}
|
||||
<a href="{{ grouped_product.get_permalink()|esc_url }}" class="btn btn-sm btn-outline-secondary">
|
||||
{{ __('View product') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elseif grouped_product.is_sold_individually() %}
|
||||
<div class="form-check">
|
||||
<input type="checkbox"
|
||||
name="quantity[{{ child_id }}]"
|
||||
value="1"
|
||||
class="form-check-input wc-grouped-product-add-to-cart-checkbox"
|
||||
id="quantity-{{ child_id }}" />
|
||||
</div>
|
||||
{% else %}
|
||||
{% include 'global/quantity-input.html.twig' with {
|
||||
input_id: 'quantity_' ~ child_id,
|
||||
input_name: 'quantity[' ~ child_id ~ ']',
|
||||
input_value: 0,
|
||||
input_value: '',
|
||||
min_value: 0,
|
||||
max_value: grouped_product.get_max_purchase_quantity()|default(0),
|
||||
max_value: grouped_product.get_max_purchase_quantity(),
|
||||
step: 1,
|
||||
placeholder: '0',
|
||||
inputmode: 'numeric',
|
||||
@@ -46,7 +75,7 @@
|
||||
</td>
|
||||
|
||||
<td class="woocommerce-grouped-product-list-item__label">
|
||||
<label for="quantity_{{ child_id }}">
|
||||
<label for="product-{{ child_id }}">
|
||||
{% if grouped_product.is_visible() %}
|
||||
<a href="{{ grouped_product.get_permalink()|esc_url }}" class="text-decoration-none">
|
||||
{{ grouped_product.get_name()|esc_html }}
|
||||
@@ -65,14 +94,17 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{{ wp_reset_postdata() }}
|
||||
|
||||
{{ do_action('woocommerce_grouped_product_list_after') }}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if show_add_to_cart_button is not defined or show_add_to_cart_button %}
|
||||
<input type="hidden" name="add-to-cart" value="{{ product.get_id() }}" />
|
||||
|
||||
{% if quantites_required and show_add_to_cart_button %}
|
||||
{{ do_action('woocommerce_before_add_to_cart_button') }}
|
||||
|
||||
<input type="hidden" name="add-to-cart" value="{{ product.get_id() }}" />
|
||||
<button type="submit" class="btn btn-primary btn-lg single_add_to_cart_button">
|
||||
{{ product.single_add_to_cart_text() }}
|
||||
</button>
|
||||
|
||||
@@ -24,22 +24,26 @@
|
||||
action="{{ product.get_permalink()|esc_url }}"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
{{ variations_attr|default('')|raw }}>
|
||||
data-product_id="{{ product.get_id() }}"
|
||||
data-product_variations="{{ available_variations|json_encode|esc_attr }}">
|
||||
|
||||
{{ do_action('woocommerce_before_variations_form') }}
|
||||
|
||||
{% if available_variations is defined %}
|
||||
{# Variation attribute selectors #}
|
||||
{% if available_variations is not same as(false) %}
|
||||
{# Variation attribute selectors.
|
||||
WC PHP uses sanitize_title() on attribute names for name/data-attribute_name
|
||||
so they match the lowercase keys in the variation data JSON. #}
|
||||
<div class="variations mb-4">
|
||||
{% for attribute_name, options in attributes %}
|
||||
{% set sanitized_name = attribute_name|sanitize_title %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold" for="{{ attribute_name|esc_attr }}">
|
||||
<label class="form-label fw-semibold" for="{{ sanitized_name|esc_attr }}">
|
||||
{{ wc_attribute_label(attribute_name)|esc_html }}
|
||||
</label>
|
||||
<select id="{{ attribute_name|esc_attr }}"
|
||||
<select id="{{ sanitized_name|esc_attr }}"
|
||||
class="form-select"
|
||||
name="attribute_{{ attribute_name|esc_attr }}"
|
||||
data-attribute_name="attribute_{{ attribute_name|esc_attr }}">
|
||||
name="attribute_{{ sanitized_name|esc_attr }}"
|
||||
data-attribute_name="attribute_{{ sanitized_name|esc_attr }}">
|
||||
<option value="">{{ __('Choose an option') }}</option>
|
||||
{% if options is iterable %}
|
||||
{% for option in options %}
|
||||
@@ -54,25 +58,22 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Reset link — WC JS toggles visibility based on attribute selection #}
|
||||
<a class="reset_variations btn btn-link btn-sm p-0 text-decoration-none" href="#" style="visibility: hidden;">
|
||||
{{ __('Clear') }}
|
||||
</a>
|
||||
<div class="reset_variations_alert screen-reader-text" role="alert" aria-live="polite" aria-relevant="all"></div>
|
||||
|
||||
{{ do_action('woocommerce_after_variations_table') }}
|
||||
|
||||
{# Reset link #}
|
||||
<div class="reset_variations_wrapper mb-3" style="display: none;">
|
||||
<a class="reset_variations btn btn-link btn-sm p-0 text-decoration-none" href="#">
|
||||
{{ __('Clear') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Single variation display + add-to-cart button #}
|
||||
{# Single variation display + add-to-cart button.
|
||||
The woocommerce_single_variation hook outputs:
|
||||
- priority 10: empty .single_variation div (JS populates via underscore template)
|
||||
- priority 20: .variations_button div with quantity + add-to-cart button
|
||||
No manual wrapper divs — the hooks handle the full DOM structure. #}
|
||||
<div class="single_variation_wrap">
|
||||
{{ do_action('woocommerce_before_single_variation') }}
|
||||
|
||||
<div class="woocommerce-variation single_variation"></div>
|
||||
|
||||
<div class="woocommerce-variation-add-to-cart variations_button">
|
||||
{{ do_action('woocommerce_single_variation') }}
|
||||
</div>
|
||||
|
||||
{{ do_action('woocommerce_single_variation') }}
|
||||
{{ do_action('woocommerce_after_single_variation') }}
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -86,3 +87,4 @@
|
||||
</form>
|
||||
|
||||
{{ do_action('woocommerce_after_add_to_cart_form') }}
|
||||
|
||||
|
||||
@@ -37,8 +37,7 @@
|
||||
{{ do_action('woocommerce_after_add_to_cart_quantity') }}
|
||||
|
||||
<button type="submit"
|
||||
class="btn btn-primary btn-lg single_add_to_cart_button"
|
||||
disabled>
|
||||
class="btn btn-primary btn-lg single_add_to_cart_button disabled wc-variation-selection-needed">
|
||||
{{ product.single_add_to_cart_text() }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
# @since 0.1.0
|
||||
#}
|
||||
|
||||
{% set cols = columns|default(4) %}
|
||||
{% set has_images = post_thumbnail_id is defined and post_thumbnail_id %}
|
||||
{# Compute image data from the product object (PHP template does this locally). #}
|
||||
{% set post_thumbnail_id = product.get_image_id() %}
|
||||
{% set gallery_image_ids = product.get_gallery_image_ids() %}
|
||||
{% set cols = apply_filters('woocommerce_product_thumbnails_columns', 4) %}
|
||||
{% set has_images = post_thumbnail_id %}
|
||||
{% set gallery_classes = 'woocommerce-product-gallery woocommerce-product-gallery--columns-' ~ cols %}
|
||||
{% if has_images %}
|
||||
{% set gallery_classes = gallery_classes ~ ' woocommerce-product-gallery--with-images' %}
|
||||
@@ -28,34 +31,37 @@
|
||||
<div class="{{ gallery_classes }}" data-columns="{{ cols }}" style="opacity: 0; transition: opacity .25s ease-in-out;">
|
||||
<div class="woocommerce-product-gallery__wrapper">
|
||||
{# Main product image #}
|
||||
{% if main_image_html is defined and main_image_html %}
|
||||
<div class="woocommerce-product-gallery__image mb-3">
|
||||
{{ main_image_html|raw }}
|
||||
</div>
|
||||
{% elseif post_thumbnail_id is defined and post_thumbnail_id %}
|
||||
{% if post_thumbnail_id %}
|
||||
<div class="woocommerce-product-gallery__image mb-3">
|
||||
<img src="{{ wp_get_attachment_url(post_thumbnail_id)|esc_url }}"
|
||||
class="img-fluid rounded"
|
||||
class="img-fluid rounded wp-post-image"
|
||||
alt="{{ product.get_name()|esc_attr }}" />
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="woocommerce-product-gallery__image mb-3">
|
||||
<div class="woocommerce-product-gallery__image woocommerce-product-gallery__image--placeholder mb-3">
|
||||
<img src="{{ wc_placeholder_img_src()|esc_url }}"
|
||||
class="img-fluid rounded"
|
||||
alt="{{ __('Placeholder')|esc_attr }}" />
|
||||
class="img-fluid rounded wp-post-image"
|
||||
alt="{{ __('Awaiting product image')|esc_attr }}" />
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Thumbnail gallery strip #}
|
||||
{% if gallery_image_ids is defined and gallery_image_ids|length > 0 %}
|
||||
<div class="row row-cols-{{ columns|default(4) }} g-2">
|
||||
{% for image_id in gallery_image_ids %}
|
||||
<div class="col">
|
||||
<img src="{{ wp_get_attachment_url(image_id)|esc_url }}"
|
||||
class="img-fluid rounded border cursor-pointer wc-gallery-thumb"
|
||||
alt="{{ product.get_name()|esc_attr }}"
|
||||
data-full-src="{{ wp_get_attachment_url(image_id)|esc_url }}" />
|
||||
</div>
|
||||
{# Thumbnail gallery strip — includes main image so user can switch back.
|
||||
Build a combined list: main image first, then gallery images.
|
||||
Skip any IDs that don't resolve to a valid attachment URL. #}
|
||||
{% if gallery_image_ids|length > 0 and post_thumbnail_id %}
|
||||
{% set all_thumb_ids = [post_thumbnail_id]|merge(gallery_image_ids) %}
|
||||
<div class="row row-cols-{{ cols }} g-2 mt-2">
|
||||
{% for image_id in all_thumb_ids %}
|
||||
{% set thumb_url = wp_get_attachment_url(image_id) %}
|
||||
{% if thumb_url %}
|
||||
<div class="col">
|
||||
<img src="{{ thumb_url|esc_url }}"
|
||||
class="img-fluid rounded border wc-gallery-thumb{% if loop.first %} border-primary active{% endif %}"
|
||||
alt="{{ product.get_name()|esc_attr }}"
|
||||
data-full-src="{{ thumb_url|esc_url }}"
|
||||
style="{% if loop.first %}opacity: 1{% else %}opacity: 0.6{% endif %}" />
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
13
templates/single-product/product-thumbnails.html.twig
Normal file
13
templates/single-product/product-thumbnails.html.twig
Normal file
@@ -0,0 +1,13 @@
|
||||
{#
|
||||
# Product Thumbnails (Bootstrap 5 Override)
|
||||
#
|
||||
# Intentionally empty — the product-image.html.twig template renders its own
|
||||
# thumbnail gallery strip using Bootstrap grid. This override suppresses the
|
||||
# default WC output from woocommerce_show_product_thumbnails() which would
|
||||
# render additional full-size gallery images below the thumbnail row.
|
||||
#
|
||||
# WooCommerce PHP equivalent: single-product/product-thumbnails.php
|
||||
#
|
||||
# @package WcBootstrap
|
||||
# @since 0.1.5
|
||||
#}
|
||||
@@ -23,9 +23,11 @@
|
||||
|
||||
{{ woocommerce_product_loop_start() }}
|
||||
|
||||
{% for product in related_products %}
|
||||
{% include 'content-product.html.twig' with { product: product } %}
|
||||
{% for related_product in related_products %}
|
||||
{{ wc_setup_product_data(related_product) }}
|
||||
{% include 'content-product.html.twig' %}
|
||||
{% endfor %}
|
||||
{{ wp_reset_postdata() }}
|
||||
|
||||
{{ woocommerce_product_loop_end() }}
|
||||
</section>
|
||||
|
||||
@@ -23,9 +23,11 @@
|
||||
|
||||
{{ woocommerce_product_loop_start() }}
|
||||
|
||||
{% for product in upsells %}
|
||||
{% include 'content-product.html.twig' with { product: product } %}
|
||||
{% for upsell in upsells %}
|
||||
{{ wc_setup_product_data(upsell) }}
|
||||
{% include 'content-product.html.twig' %}
|
||||
{% endfor %}
|
||||
{{ wp_reset_postdata() }}
|
||||
|
||||
{{ woocommerce_product_loop_end() }}
|
||||
</section>
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
// Render breadcrumbs. The woocommerce_before_main_content hook is not fired here
|
||||
// (to avoid the content wrapper), so breadcrumbs must be called directly.
|
||||
// TemplateOverride will intercept global/breadcrumb.php and render the Bootstrap
|
||||
// breadcrumb Twig template.
|
||||
woocommerce_breadcrumb();
|
||||
|
||||
// Fire structured data hook (normally on woocommerce_before_main_content at priority 30).
|
||||
do_action( 'woocommerce_shop_loop_header' );
|
||||
|
||||
|
||||
Reference in New Issue
Block a user