Recent Activities
This page shows what are we working on.
-
head
-
Done with search (#2) d8e0f3
-
Enhance minisearch functionality with slideout behavior and styling adjustments without Ajaxsearch (#2) 8c4a9d
-
Minisearch styles when ajaxsearch disabled... in progress (#2) 259fac
-
Remove unused styles from viewcart action and update empty subtitle styles in minicart (#2) dba7a5
-
Minicart complete (#2) 56efd2
-
Search on desktop (#2) 9c587e
-
BCB homepage: added videos section 7ee864
-
Minicart inprogress (only buttons to adjust)... (#2) c1be91
-
Minicart update in progress.... c3d18b
-
Minicart update in progress... (#2) 55e7b9
-
INstaller. Create dummy CMS block header_panel_info to allow user add custom content (#2) f70d58
-
Adjust minicart counter (#2) 03e843
-
BCB homepage: Added brand logos section b905f1
-
BCB homepage: Added testimonials section 2ab2cf
-
Update newsletter styles: adjust input border and add consent positioning (#2) 02d37d
-
Header. Update search behaviou and look of the result dropown. 993432
-
BCB homepage: Added FAQ a0bfde
-
Add focused search styles. d80f88
-
BCB homepage: added new sections 875068
-
BCB homepage: added ECI and full-width banner d40cfa
-
Top navigation added. 8fa9a3
-
Remove color from menu links dec081
-
Clone currency and store switchers in header slideout menu for improved functionality de36e3
-
Enhance header and newsletter styles for improved layout and responsiveness in header slideout menu. b63dd0
-
Installer. Update header slideout menu styles. 825795
-
Update footer top content HTML to enhance newsletter section styling.
Don't set font familiy for header in Page Builder ba0b81 -
BCB homepage WIP b228af
-
Basic navpro slideout styles 75ac0e
-
Refactor header slideout menu and newsletter styles for improved layout and consistency 3b42ff
-
Header slideout menu... d4f174
-
Listing styles for the list mode and old price update. cbff61
-
Update README.md to clarify usage of `with-bottom-divider` CSS class and provide detailed instructions 1b9ea5
-
Pagination and make with-bottom-divider general. 37ea17
-
Added list banner 5 config; updated installer, moved it to json a43985
-
Add new layout configuration and remove easybanner references; update footer content and styles 87165b
-
Refactor easybanner configuration and installer for product listing banners 8b1d0d
-
Installer. Listing grid banner added. 914c01
-
Add swatches variables and layout styles d59581
-
Update positioning for filter title and content in layered navigation 8cbd21
-
Add image dimensions for product listings and related widgets in view.xml 6985cd
-
layout and styles for category view, product toolbar, shop by button 08fd27
-
Add layered navigation styles and update imports 944ff1
-
Keep working on prodcut listing and category page. 411088
-
Keep working on product listing toolbar. ebcfac
-
Working on product listing and toolbar look. 1ab39d
-
Add Cooper* font 132906
-
Add listing item styles and variables e725b5
-
Product lisnting in progress - grey bg and border radius. 4e7c35
-
Add new variable files for colors, typography, header, footer, icons, navigation, popover, minisearch, and gradient ba122c
-
Refactor footer and newsletter styles for improved consistency and responsiveness d831e4
-
Footer top newsletter - some updates. 452ca5
-
Add footer top content block and styles for newsletter integration (WIP) 128131
-
Footer bottom. 8d58ea
-
Add footer content block and styles for enhanced footer layout 4d6b1b
-
Header. Hide menu on medium screens. Update search look on monile. e0146d
-
Installer. Add navigation slideout menu a89fd5
-
Add EasySlide functionality for header panel with custom slides and styles e55b0b
-
-
3.3.1
-
3.3.0
-
1.4.0
-
chore(release): 1.4.0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 0bcf86 -
Merge pull request #89 from breezefront/claude/relaxed-shirley-d523bc
feat(backup): restore values overwritten by an AI run (#8) 1593f6 -
test(backup): cover the selection path and grid columns; honest run date (#8) 551936
-
Merge pull request #90 from breezefront/fix/composer-constraints
fix(composer): tighten dependency constraints (breezefront/breeze-ai#1) 976098 -
fix(openai): stop advising an upgrade PHP 8.1 cannot install
The TypeError branch told the user to update openai-php/client to ^0.19.
Only 0.10.x reaches it, and 0.10.x is also the only branch PHP 8.1 can
install: 0.11 and up require PHP 8.2. On the stores that actually hit
this, the advice was impossible to follow.
Name the PHP requirement instead of a client version.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> e3f75f -
fix(composer): tighten dependency constraints (breezefront/breeze-ai#1)
- php: "^7.4|^8.0|^8.1|^8.2" collapsed to ">=7.4 <9" anyway, since ^8.0
already covers 8.1-8.4. It also claimed support for PHP 7.4/8.0, which
cannot work: openai-php/client 0.10.x requires PHP ^8.1 and 0.19.x
requires ^8.2, so no version resolves below 8.1. Now "^8.1", matching
swissup/module-breeze.
- magento/framework: "*" guarded nothing. Dropped, as in module-breeze
and module-breeze-integrations.
swissup/module-core stays "*" — it is pulled in transitively and no
specific core version is required.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> b725ac -
fix(backup): declare restore isolation, strict capture, scope-true wording (#8) ad050c
-
fix(backup): re-check selections, capture and write together (#8) d54dbe
-
fix(backup): row action bypasses the mass-action filter, keep uncaptured video columns (#8)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 12ba05 -
fix(backup): keep restores retriable, recorded and correctly scoped (#8)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 0fb862 -
fix(backup): one capture per field, per-commit counts, gated mass action (#8)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 7ed888 -
feat(backup): restore values overwritten by an AI run (#8)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 3ec08b -
Merge pull request #88 from breezefront/claude/beautiful-liskov-50c01d
feat(usage): log tokens, cost and duration per AI call (#86) 6aba08 -
fix(usage): say when the ledger stops recording, distrust stored rates (#86) 784abb
-
fix(usage): escape separators so a value cannot forge a field (#86) 085672
-
fix(usage): price the model that answered, keep one line per call (#86) 4ac0c0
-
fix(usage): count reasoning tokens, keep sub-cent charges (#86) 7549fb
-
fix(usage): never infer a rate the ledger cannot stand behind (#86) a856a8
-
test(usage): cover the log wiring, not just the writer (#86) 0117c1
-
feat(usage): log tokens, cost and duration per AI call (#86) 128d10
-
-
1.3.0
-
Version 1.3.0 050f99
-
Merge pull request #83 from breezefront/fix/ai-buttons-overlap-name-82
fix(admin): stop AI buttons overlapping long field values (#82) 3348b6 -
fix(admin): lay AI actions beside field, not over it (#82) 3eb55f
-
fix(admin): stop AI buttons overlapping long field values (#82) 1a4854
-
Merge pull request #81 from breezefront/feat/editor-chat-panel
feat(content-builder): chat panel in the editor for building and refining a page (#64) 0932fb -
fix(content-builder): ground every request in the live working copy
pageContext() only supplies the page's title and id, so a first chat
message had no representation of the page's actual content, and manual
edits made between AI replies were silently overwritten by the last
frozen AI document. Every request now seeds or refreshes its document
turn from contentBuilder.getComponents() instead. Also drops historical
image attachments from the request — maxBytes is sized for one
screenshot under post_max_size, and replaying every prior attachment on
each follow-up can push the aggregate body past that limit. 9ca7e3 -
fix(content-builder): give the message textarea an accessible name b48efb
-
refactor(content-builder): share the chat loop between the modal and the editor panel (#64)
Extracts turn storage, chat-bubble rendering, and copy-to-clipboard into
chat-core.js, used by both chat.js's field-targeted modal and
bcb-chat-panel.js's docked panel. The request/response cycle stays local
to each caller — they talk to different skills with different payload
and response shapes, and forcing that through one function would trade
duplication for a worse, branchier abstraction.
Fixes two bugs the duplication had introduced in bcb-chat-panel.js: the
Copy button was never bound (only chat.js's copy of the handler existed)
and a stale "type a message" error could survive a valid follow-up
submission. 1ab342 -
fix(content-builder): drop a stale AI response after a page switch (#64) 371c91
-
fix(content-builder): keep the generated document in follow-up history, guard double-submit a48469
-
feat(content-builder): chat panel in the editor for building and refining a page (#64)
Docks a conversational panel into the Content Builder editor's tree panel
via a mixin on bcb-panel.js. Every reply is applied straight into the
working copy through content-builder.js's applyAiComponents(), and every
request is sent persist:false so nothing reaches the draft until the
admin uses the editor's own Save/Publish. 9e66ae -
feat(content-builder): add non-persisting build endpoint and editor chat config (#64)
Adds options.persist=false to Controller/Adminhtml/Page/Build so a request
can return generated components without writing the Content Builder draft.
Extracts vision-model selection out of BuildFromScreenshot into a shared
VisionModelProvider, and adds Block/Adminhtml/Editor/AiChatConfig plus its
layout/template to seed the editor route with the same config. 6eca5e -
Merge pull request #80 from breezefront/claude/focused-payne-b60751
fix(content-builder): reproduce every band the screenshot shows (#74) a84559 -
fix(content-builder): word the band warnings for the request that was sent (#74)
Four defects found reviewing the review fixes.
The count-mismatch warnings hardcoded "The reference design was read as ...",
but the mismatch branch runs whether or not a screenshot was attached — the
prompt asks for a band count on every build, not only on the image path. A
text-only brief that came back over- or under-segmented therefore told the
merchant about a reference design they never sent, on the build note and in
the Content Builder editor. The flag that decides whether to warn now decides
what the warning calls the thing the page was counted against.
The band-wrapper warnings borrowed ComponentValidator's "root[N]" label while
numbering a different list. The validator indexes the raw model output, where
a dropped node still consumes its index; the audit sees the list after
validation. One dropped band was enough to put two warnings saying "root[0]"
in one list, pointing at two different bands. They are numbered as bands now.
readScreenshot() read and base64-encoded the file before ImageValidator got to
apply MAX_IMAGE_BYTES, so an oversized design export cost about 2.3x its size
in memory to reach a verdict available from filesize(). The admin path is
unaffected: the browser posts base64 and no file is read.
FiveBandMock::drawProductRow() took $bottom and never used it, so that band
alone would not follow a change to the band split in draw() — in the one file
whose docblock claims the band boundaries are readable in source. 685502 -
fix(content-builder): keep the band count when the reply carries prose (#74)
Three defects found reviewing the band-audit change.
The prompt says "no prose" and models add it anyway, which is why
jsonCandidates() exists at all. It tries the widest "[...]" run before
"{...}", correct while a bare list was the asked-for shape. With an envelope
whose "unsupported" is omitted and "components" last, that run is exactly the
components array: it parses, wins, and the band count is thrown away. The
audit then reported "the AI did not report how many bands it saw" for a reply
that did report it. A list is now held as a fallback and the remaining
candidates are still tried for an envelope, so a reply that really is a bare
list still resolves to the first list.
spreadStackedSlots() decided a band was stacked from the count of the first
slot alone. "children" written as a flat list of nodes rather than a list of
slots lands there with the node itself as the first slot, and its own keys are
counted as sibling blocks — {"type": ..., "props": ...} counts 2 in a
two-column band and was shredded across the slots, replacing the accurate
short-slot warning with a repair that never happened. Every entry now has to
look like a node.
render.php ignored the file_put_contents result and printed "(0 bytes)" with
status 0 on an unwritable path, sending whoever reproduces a measurement to
look at the build command instead. ee671e -
fix(content-builder): reproduce every band the screenshot shows (#74)
Shown a five-band reference design, gpt-4o-mini returned a document whose
columns bands were fakes: the container had the right type and the right
column count, but every card sat in its first slot, so the band rendered as
one column with the rest empty. Measured over ten runs of the committed mock,
that happened in nine of them.
Three changes, in order of how much they hold:
- ComponentValidator spreads a container whose whole band was written into
its first slot, when the arithmetic leaves no other reading. Prompting
against this only half worked; the repair is unambiguous.
- The prompt asks for {"bands", "unsupported", "components"} instead of a
bare array, spells out the slot shape, and names the columns-versus-
product_grid choice. A bare array is still accepted.
- BandAudit compares the band count the model reports to what it emitted and
reports the difference, plus any band the catalog cannot express. It
reports and does not retry: a second round trip would double the cost of
every build to re-roll an answer a small model is as likely to get wrong
again, and the missing band cannot be synthesised locally.
Measured with the mock and expected shape now under Test/Fixture, driven
through the new --screenshot option. Ten runs each, gpt-4o-mini:
before: 5 bands x5, 6 bands x4, 1 hard failure; 9/9 mis-slotted
after: 5 bands x10, 0 mis-slotted, 8/10 with both columns containers
gpt-4o gets the slots right with either prompt, so the mis-slotting was
model capacity and the prompt closes the gap on the small model. 0e5f35 -
Merge pull request #79 from breezefront/fix/build-note-page-id-type
fix(content-builder): match cms_page.page_id column type in the build note table (#72) 7faf89 -
fix(content-builder): match cms_page.page_id column type in the build note table (#72) 7f6aaa
-
Merge pull request #78 from breezefront/claude/priceless-turing-a344ac
fix(config): offer model names the providers still serve (#73) 21eee6 -
Merge pull request #77 from breezefront/claude/jovial-chatelet-3447d7
feat(content-builder): report what the validator repaired (#72) d23f45 -
fix(content-builder): drop a corrupt build note instead of reporting a clean build (#72) e3994a
-
feat(content-builder): report what the validator repaired (#72) 4b47e4
-
fix(gemini): redact api key from error, notice for retired models (#73) 922d51
-
fix(config): offer model names the providers still serve (#73) b1963c
-
Merge pull request #76 from breezefront/claude/lucid-kepler-a7ce73
fix(content-builder): keep authoring placeholders out of generated copy (#71) 6a5b87 -
fix(content-builder): keep required sample copy on the malformed-value path (#71)
fallbackValue() blanked any authoring placeholder, so a required copy prop the
model sent as an array — quote.text, in the fixture — came out empty, which is
the broken component the omitted-prop path deliberately avoids. The rule now
lives in one predicate used by both paths.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 8a6461 -
fix(content-builder): keep authoring placeholders out of generated copy (#71)
The catalog's "default" attribute does two jobs: the value a renderer needs
when a prop is unset, and the sample text the editor drops into a fresh
component. The validator filled both, so "Discover Something New" shipped as
the heading of three FAQ answers. Copy-typed props whose default reads as
prose are now stored blank — blank, not absent, because the templates read
`$props['heading'] ?? '<sample>'` — and the prompt marks them VISIBLE-TEXT
instead of quoting the sample text that taught the model to echo it.
A list-valued prop is blanked as an empty list, so an unusable faq_items no
longer falls back to the two sample questions. Tabs keeps its declared titles:
the container counts its slots from them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 00e837 -
Merge pull request #75 from breezefront/claude/amazing-lichterman-a86181
fix(content-builder): keep the list props the model wrote (#70) 819b38 -
fix(content-builder): keep every tab title paired with its slot (#70)
Dropping an unusable title compacted the list while normalizeChildren()
truncates slots from the end, so ["Shipping", "", "Returns"] over three
populated slots rendered "Returns" above the middle slot's content and threw
the last one away.
A list of strings is positional — its container pairs slot N with entry N —
so an unusable entry is now replaced where it stands, taking the declared
default's title for that position ("Tab 2"), which is what the container's
renderer pads a short title list with anyway. Entries that carry their own
content, like the FAQ items, have no slot to stay paired with and are still
dropped.
An empty list now says so rather than reporting that none of its zero
entries was usable. efb774 -
fix(content-builder): read a keyed object as one list entry (#70)
decodeList() accepted any array, so a keyed object counted as a list: an FAQ
item the model sent unwrapped — {"q": ..., "a": ...} instead of [{...}] —
was iterated field by field, turning the question and the answer into two
bogus items, each with the other's text as its question.
Only a real list is a list now. A keyed object carrying at least one declared
field is wrapped as the single entry it is, so the question the model wrote
survives; one carrying none falls back to the default with a warning naming
the fields that were expected. 71d179 -
fix(content-builder): keep the list props the model wrote (#70)
The catalog asks the model for the Tabs titles and the FAQ items as JSON
arrays, then ComponentValidator coerced every non-scalar prop back to its
default — so a generated FAQ arrived with its questions replaced by "Tab 1"
/ "Tab 2" / "Tab 3", and the Tabs slot count fell back to the default three,
dropping any further answer with it.
A prop whose declared default is a JSON array is now validated as a list
instead of discarded: entries are cleaned one by one and written back in the
JSON-string shape the editor stores and the renderers decode, so the slot
count follows the titles the model actually wrote. The default is only
reached when nothing usable is left, and the warning then names the reason.
Recognised by the shape of the declared default rather than by prop name, so
the FAQ accordion's question/answer items are covered by the same path. a4765b -
Merge pull request #69 from breezefront/claude/stoic-yonath-58797a
fix(content-builder): drop fake "spacing" prop from catalog prompt 7db871 -
fix(content-builder): drop fake "spacing" prop from catalog prompt
The margin/padding summary line started with the group label instead of
prop names, so the model read "spacing" as a prop and emitted it on every
component that had one. Render one line per value range, leading with the
real names, which also stops claiming -200 as the minimum for paddings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> beb335 -
chore: drop stray .b64 scratch files from screenshots dir
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> e5e228 -
docs: add screenshots to test guide, correct scenarios after live testing
Vision-model dropdown pre-filters server-side (10.6 unreachable via UI as
written), Remove+re-attach confirmed clean (10.9). Found and filed a real
draft-persistence bug in module-breeze-content-builder (#31) — re-run builds
report success but don't survive a fresh page reload.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> 8062ec -
docs: add test scenarios and how-to-test guide for Content Builder screenshot import
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> 8e6e8d -
Merge pull request #67 from breezefront/feat/value-backup
feat(backup): record values before AI writes 70f7cc -
fix(backup): skip ambiguous staging rows 27e298
-
fix(backup): guard capture reads, batch pruning 19f666
-
feat(backup): record values before AI writes 21d8f6
-
Merge pull request #66 from breezefront/test/bulk-selection-regression
test(bulk): pin the scope a run resolves to b69352 -
test(bulk): seed the products the scope cases run against
Copilot was right on both counts: the cases borrowed whatever the
catalogue happened to hold, and the exclusion case skipped itself whenever
no two SKUs shared a five-character prefix. A regression check that goes
quiet when the surrounding data does not suit it is not a guard.
Three products with a prefix unique to the run are created in setUp and
removed in tearDown, so every case is deterministic and none can skip. The
prefix also gets its own case, since the two filtered cases rest on it
selecting this test's products and nothing else.
Reverting BulkEntityIdResolver to the pre-33d5e97 behaviour now turns five
of the six red, up from four. The unfiltered select-all stays green, which
is correct - that path is the legitimate one and behaved the same before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> e82c74 -
test(bulk): pin the scope a run resolves to
The selection fault has landed three times. 15c1e7c introduced an
all_selected flag driven by allSelected(), #31 closed it in June, and
c13907e swapped in excludeMode() and brought it straight back - Magento
raises either flag by itself once the checked rows cover the rows the grid
is showing. Both attempts only ever argued about the client-side flag,
while the resolver went on loading the collection with no filters at all.
The E2E spec covers the request the wizard posts. Nothing covered what
that request then resolves to, which is the number that decides how many
products get rewritten. These cases do, against the live catalogue: an
explicit selection, a genuine select-all, a select-all narrowed by a
filter, exclusions inside a filtered set, and an empty selection.
Reverting BulkEntityIdResolver to the pre-33d5e97 behaviour turns four of
the five red, the exception being the unfiltered select-all - which is
correct, since that case is the legitimate path and behaved the same
before.
Refs #61
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 8e8b71
-
-
1.19.0
-
Version 1.19.0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 94fd39 -
fix(preload): cap font preloads and fix @font-face parsing (#117)
Font preloads had no cap and no notion of whether a face blocks rendering, while the url() parsing dropped most real-world forms and could aim a preload at a nonexistent host. Measured on a live Argento product page, 138 KB of preloaded fonts plus an eager 219 KB footer PNG stretched a 68 KB LCP JPEG into a 3.1 s download; removing them took LCP from 3.83 s to 2.11 s on Slow 4G.
- pagespeed/main/font_preload_limit (default 0, opt-in) caps the preloads per response
- pagespeed/main/font_preload_ignore skips urls by substring, for icon fonts
- candidates ranked by effective font-display: render-blocking first, then fallback, then swap; optional never preloaded
- url() values unquoted, fragments dropped, queries kept, hrefs mirroring the protocol-relative form the delivered css uses
- @font-face delimited and read on a comment-masked copy, with offset-based injection
- collection armed one-shot per process() call, so it no longer depends on caller order
Preload gained a Config constructor argument, so deploys need setup:di:compile.
Fixes #117 9a26a2 -
fix(preload): make font preloading from stylesheets opt-in (#117)
Default font_preload_limit goes from 2 to 0.
Fixing the url parsing means stores whose @font-face urls are quoted or
absolute — most of them — start getting font preloads they never had,
because the old code silently dropped those urls. Shipping a default of 2
would therefore add up to two High priority requests into the LCP window
on upgrade, which is the exact regression this branch exists to remove:
on the store that prompted it, 138 KB of preloaded fonts cost 1.7 s of
LCP. With font-display:swap injected the fonts do not block rendering
anyway, so the bandwidth buys little.
The budget and the ranking stay; an admin who knows which weight renders
above the fold raises it to 1 or 2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> a6c93d -
fix(preload): make font collection one-shot per armed call (#117)
Sixth review pass pointed out that CssResolver calls process() without
ever calling setResponse(), so merged stylesheets never contribute
candidates — my claim that the ranking converges across every stylesheet
was wrong. Only the critical style blocks feed it.
Rather than widen collection, which would pull fonts out of every merged
stylesheet in a change meant to cap them, the gate is now honest and
deterministic. setResponse() arms exactly the next process() call, so a
bare process() never pushes. Left armed, whether a merged stylesheet
contributed depended on the order the two callers happened to run in for
a given request, which made the preload set vary between requests.
CriticalCss already chains setResponse() before each process() call, so
production behaviour is unchanged. The multi-call tests now re-arm the
same way, which also makes them mirror that caller.
Also pins the two remaining shapes of the mask's documented limit — a }
and a ; inside a quoted value — as misses rather than corruption.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 95ecf9 -
docs(preload): record the comment mask's limit and pin its failure mode (#117)
The mask cannot tell a real comment from a comment opener inside a css
string or data uri, so a fake opener can blank a whole @font-face. That
limit is now stated at maskCssComments(), together with why a css
tokenizer is not worth carrying to read one descriptor.
The test pins the direction the failure takes: the face is skipped, so it
gets no injection and contributes no preload candidate, and the
stylesheet passes through unchanged. Skipping is acceptable; rewriting a
stylesheet we could not read is not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 48133a -
fix(preload): delimit @font-face on a comment-masked copy (#117)
Fifth review pass.
A } inside a comment ended the face body early for the plain regex, so
@font-face{/* } */src:url(a.woff2)} yielded the body "/* " — the url was
never collected, and worse, the injection then ran
str_replace('/* ', 'font-display:swap;/* ') across the whole stylesheet.
The truncation predates this branch; the injection path made it harmful.
Comments are now blanked to spaces of the same byte length before the
faces are delimited, so a masked } cannot end a body while every offset
still points into the original text. Injection splices at those offsets,
from the end so earlier ones stay valid. That also fixes a second
injection bug: two identical face bodies in one stylesheet had their
swap injected by str_replace on the body text, which rewrote both and
then did it again on the next iteration.
Invalid duplicate descriptors no longer beat a valid one either. The
browser drops invalid declarations at parse time, so
font-display:optional;font-display:garbage stays optional; the last
valid value wins and auto applies only when nothing valid was declared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 7b87f1 -
fix(preload): validate the whole font-display value (#117)
Fourth review pass. The value pattern captured the leading alphabetic
token, so font-display: optional garbage read as optional and its
preload was suppressed — but the whole value is invalid css and the
browser falls back to auto, which blocks. Same for a descriptor written
with !important, which is dropped entirely.
The value is now taken up to the declaration terminator, trimmed, then
checked against the five valid keywords, so anything invalid resolves to
auto as the browser does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 9ca057 -
fix(preload): read @font-face without its comments (#117)
Third review pass. A css comment may sit wherever whitespace may, so
font-family:A;/* keep */font-display:optional; hid the declaration from
the boundary matcher. The face then got a swap injected in front of a
declaration that stays effective, and was ranked as swap and preloaded
even though the author marked it optional.
Every read of a @font-face body now works on a comment-free copy, which
fixes the same class of bug in url extraction too: a commented-out
url(old.woff2) was being harvested as a preload candidate and could
spend the budget on a file the browser never requests. The injection
itself still targets the original text.
While there: a face declaring font-display twice resolved to the first
declaration, where the cascade takes the last.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> d0f213 -
fix(preload): match font-display as a declaration, mirror scheme case (#117)
Second review pass found two more holes.
The injection guard tested for the substring "font-display" anywhere in
the @font-face body, so a family named after the descriptor or a font
path containing the word suppressed the swap injection and left the face
blocking. Both the guard and the value read now require the descriptor
at a declaration boundary — start of the body, or after a previous
declaration.
Scheme stripping accepted any spelling, but the css rewrite in process()
is a case-sensitive str_replace: HTTPS://host/f.woff2 stays absolute in
the delivered css while the preload became protocol-relative, which is a
different url on an http storefront. The href now mirrors that replace
exactly and keeps an unusual spelling absolute. The http(s) check also
moved off filter_var, which accepts file:// happily.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> ba6b69 -
fix(preload): move the font budget to Preload and tighten parsing (#117)
Review follow-up. Four defects in the first pass:
Selection ran per process() call, but CriticalCss calls the improver
once per critical <style> block and the css resolver plugin once per
stylesheet. A swap face in the first block could spend the whole budget
before a render-blocking face in a later one was seen, and the same href
in two blocks consumed two slots. Candidates now accumulate in Preload
and the ranked selection happens in getAssets(), once, when every
stylesheet has been through the improver. Selection is pure, so
repeated getAssets() calls agree.
Deduplication kept the least urgent rank. Wrong: when one woff2 backs
two families, the family that blocks still blocks, so the file is
render-critical and the most urgent declaration has to win.
The font-display regex was case-sensitive and matched only the
alphabetic prefix of a value, so FONT-DISPLAY: OPTIONAL read as
render-blocking while the invalid optional-invalid read as optional and
was skipped. It is now case-insensitive, takes the whole token, and
resolves anything outside the five valid values to auto.
Preload hrefs forced https. process() rewrites absolute css urls to
protocol-relative, so on an http storefront the preload pointed at a
url the stylesheet never requests. Hrefs now mirror the delivered css:
protocol-relative for absolute and protocol-relative sources,
root-relative left alone, non-http schemes skipped.
Also adds declare(strict_types=1) to the new test file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 8b22be -
fix(preload): cap font preloads and fix url() parsing (#117)
Improver pushed a preload for every woff2 it could parse out of a
stylesheet, with no cap and no check on whether the face blocks
rendering. Chrome fetches preloaded fonts at High priority, so each one
competes with the LCP image on the same connection and fetchpriority
cannot outrank it. Measured on a live Argento product page: dropping
138 KB of preloaded fonts (plus an eager 219 KB footer PNG) took LCP
from 3.83 s to 2.11 s on Slow 4G.
The url handling was broken in both directions. str_replace('//', '/')
ran before the validity check, so absolute and quoted urls were
silently dropped, while a root-relative url became https://<first path
segment> — a preload aimed at a host named "static". The cache-busting
query was stripped too, so a preload that did survive missed the css
request and the font downloaded twice.
- pagespeed/main/font_preload_limit (default 2, 0 disables) caps the
preloads per response, shared across every stylesheet processed
- pagespeed/main/font_preload_ignore skips urls by substring, for
decorative icon fonts
- candidates are ranked by effective font-display: render-blocking
first, then fallback, then swap; optional is never preloaded
- url() values are unquoted, fragments dropped, queries kept, and
root-relative hrefs left alone; hrefs are deduped
The budget lands together with the parsing fix on purpose: fixing the
parsing alone would start emitting the preloads that were being dropped
and ship the regression by default.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> c16e23
-
-
1.18.8
-
Version 1.18.8 953e26
-
fix(preload): crossorigin="anonymous" is font-only and host-independent (#115)
crossorigin was added to any third-party resource hint based on host alone.
It switches the hint to a CORS-mode fetch, so a third-party script whose
server sends no Access-Control-Allow-Origin had its response blocked
(s.kk-resources.com/leadtag.js, #115). Even a permissive third party is hurt:
a CORS-mode hint never matches the no-cors <script> request, so the file is
fetched twice.
The host check was wrong in the other direction too — @font-face fetches in
CORS mode even from the store's own domain, so every same-origin font hint
was emitted without crossorigin and could not be reused by the real font
request.
crossorigin now depends on the asset type alone: fonts always, whatever host
they come from; scripts, styles and images never — they mirror the origin
element instead. An explicit crossorigin from the asset definition or from an
existing link is preserved; the rule only governs adding one. Applied in both
createPreloadLinkElement() and updatePreloadLinkInPlace(), so preload links
the theme or a tag manager already placed in <head> follow it too.
With the host out of the decision, isThirdPartySource(), getHost() and
getBaseUrlHost() are dead and removed, along with the StoreManagerInterface
constructor argument and the try/catch that only swallowed Laminas URI parse
errors.
Tests: 8 regression cases in AddLinkPreloadTest — third-party script, style
and image get no crossorigin; third-party and same-origin fonts keep it;
explicit crossorigin survives; existing in-head links follow the same rule.
Fixes #115
Closes #116 41be41 -
fix(preload): make font crossorigin host-independent (#115)
Review catch on the previous commit: @font-face fetches in CORS mode even
when the font sits on the store's own domain, so gating crossorigin on
"third-party host" left every same-origin font hint without the attribute
— the hint then cannot serve the real request and the font is downloaded
twice. That is the same failure this PR set out to remove, only pointed
at local fonts instead of third-party scripts.
crossorigin now depends on the asset type alone: fonts always, everything
else never. With the host no longer part of the decision, the whole
isThirdPartySource() / getHost() / getBaseUrlHost() chain is dead, and
with it the StoreManager dependency and the try/catch that only existed
to swallow Laminas URI parse errors.
- needsCrossorigin(string $as) — type-only decision
- Drop isThirdPartySource(), getHost(), getBaseUrlHost(), $baseUrlHost
- Drop the StoreManagerInterface constructor argument (DI resolves the
constructor by reflection, so no di.xml change) and the store mocks it
required in 28 test construction sites
- Drop the now-unreachable try/catch around both call sites
Tests: 2 more cases — a same-origin font hint, and a same-origin font
preload link already in <head>, both must keep crossorigin.
709 unit, 34 integration — all green. 225428 -
fix(preload): limit crossorigin="anonymous" to font hints (#115)
A third-party resource hint got crossorigin="anonymous" based on host
alone, with no regard for the asset type or for what the live tag asked
for. crossorigin switches the hint to a CORS-mode fetch, so for
s.kk-resources.com/leadtag.js — a script whose server sends no
Access-Control-Allow-Origin — the browser blocked the response outright.
Even a permissive third party is hurt: the CORS-mode hint never matches
the no-cors <script> request, so the file is fetched twice.
Only fonts are always fetched in CORS mode and therefore need the
attribute. Scripts, styles and images must mirror the origin element,
never invent it.
- Add CROSSORIGIN_TYPES = ['font'] and needsCrossorigin($as, $href)
- Use it in both createPreloadLinkElement() and
updatePreloadLinkInPlace(), so the rule also covers preload links the
theme or a tag manager already placed in <head>
- An explicit crossorigin from the asset definition or the existing link
is still preserved — the rule only governs adding one
The blanket rule dates back to e7a35a0 (2020), whose own comment noted
"add warning and more requests but increase score. why???".
Tests: 6 regression cases in AddLinkPreloadTest — third-party script,
style and image get no crossorigin; fonts keep it; explicit crossorigin
survives; existing in-head preload links follow the same rule. Three of
them reproduce the reported markup exactly against the old code.
Fixes #115 cc021a
-
-
3.3.0
-
3.2.1
-
3.3.0
-
Version 3.3.0 7f1f00
-
Added Contact Us page installer a791e7
-
Improve switcher label alignment inside mobile slideout a6415d
-
Fixed transparent menu icon for few seconds on slow web connection 7b8ab8
-
Sync gallery slider arrows with theme settings 6ce6f4
-
USP block content updated 96047c
-
Updated footer_top_content block content 946d00
-
Updated BCB homepage: moved to json; updated benefits block content 62bfe1
-
Fixed listing styles on Magento < 2.4.8
LESS ^3.2.0 bugfix: This css block moved to the start of the styles. a38c1c -
Firefox: Fixed horizontal scrollbar inside search slideout 613e59
-
-
3.2.0
-
Version 3.2.0 4c4818
-
Per page limits according to design e54d9e
-
Fixed empty search slideout in Safari browser after closing keyboard cb1f77
-
Show testimonials block under category decsription, see #14 b30944
-
Theme Editor: Allow using gradients as bg color 3cb9ff
-
Improve recent products widget styles in sidebar 3d94fb
-
Revert "Move testimonials below category description"
This reverts commit 046f5bb650b031b250b05a0009b5ec2edc27a619. 66ab0d -
Move testimonials below category description 046f5b
-
Use `apollo-` prefix for custom quote icon 7f3938
-
Prevent zoom om mobile devices when focusing quantity field 0de256
-
Limit upsell and related products to 6 items on the product page 94dd46
-
Added BLB installation: Testimonials widget on category pages 60339b
-
Testimonials and BCB slider styles bb8fa5
-
Fixed qty overlap with actions in minicart on small screens, see #16 96cd74
-
Enabled mobile slider layout for columns and grid 77cb58
-
Created separate mobile slider 8880b4
-
Sync products widget styles with product list. Closes #13 241874
-
-
1.2.0
-
1.1.1
-
1.2.0
-
1.1.0
-
Version 1.1.0 d0ef07
-
Merge pull request #35 from breezefront/feat/ai-components-bridge
feat(content-builder): apply AI-generated components to the working copy (#34) fa5587 -
Added new Video component 94bb8d
-
fix(content-builder): clone the incoming array before installing it as the working copy d02528
-
fix(content-builder): stay a no-op until sub-modules finish initializing 2c2289
-
fix(content-builder): commit a pending dirty edit before pushing the AI snapshot 3a1ce7
-
feat(content-builder): apply AI-generated components to the working copy (#34) 030d4c
-
Added 5 columns layout for the Columns component 4adc04
-