Releases: Automattic/woocommerce-ai-storefront
Release list
v0.40.0
Added
- Your tag and brand archive pages now get the same link previews and structured data as your category pages (#705).
- Three parts of the plugin each decided on their own which archive pages to cover, and they disagreed. Category archives were fully covered. A tag archive carried a machine-readable product list but no share card, so pasting its link showed a blank preview. A brand archive was submitted to instant indexing but had no title, description, share card, or product list, so crawlers were sent to a page the plugin had chosen not to describe.
- A tag or brand archive now gets the page title, meta description, and Open Graph and Twitter share card, the product list (a JSON-LD ItemList) of what is on the page, and an instant-indexing ping when the term changes, matching how a category archive already behaved.
- Editing or renaming a tag or brand now refreshes that archive's cached product list right away, instead of leaving the old one for up to an hour.
- Nothing to set up. Brand pages apply only if your store has a brand taxonomy (typically WooCommerce Brands or similar).
v0.39.1
Fixed
- Your posts and pages no longer lose their share preview when an SEO plugin is installed but not set up (#690).
- The plugin used to check which SEO plugins were installed before deciding whether to add a share preview to a post or page. Installed is not the same as working: a site with Rank Math installed but its setup wizard unfinished got no preview from Rank Math and none from us either, so shared links showed a blank card.
- It now checks whether anything actually put tags on the page. If nothing did, you get the preview; if something did, we stay out of the way.
- Same fix helps if you switch an SEO plugin's social settings off but leave the plugin active.
- One limit worth knowing: this recognises the five SEO plugins we test against. A store using something else, or a theme with its own share tags, can still end up with two sets.
- Large catalogs are now fully submitted to instant indexing instead of being cut off (#698).
- IndexNow accepts up to 10,000 URLs per request. The plugin treated that as a limit on your whole catalog and discarded anything beyond it, so a store with more than 10,000 URLs had no way to submit all of them, not even with the "Submit entire catalog now" button.
- Larger catalogs now go out as several requests a minute apart.
- If the queue does overflow, the Discovery tab now tells you how many URLs were dropped instead of reporting plain success.
- Instant indexing stopped telling search engines to re-crawl a page it also tells them to ignore (#692).
- Your
/products.jsonfeed is deliberately kept out of search results, and the plugin was submitting it to IndexNow anyway. On stores with the feed turned off it was submitting a URL that returns "not found".
- Your
- Assigning categories, tags or attributes to a product now counts as a change worth reporting (#694).
- Editing a product in the admin always did. Doing it through an import, a bulk edit, or a sync plugin did not, because those change the product's terms without re-saving the product itself.
v0.39.0
Added
-
Product links you share now carry richer previews (#679).
- When a product is shared on social media or in a messaging app, the preview card now shows whether it's in stock, on backorder or sold out, plus its condition when you've set one, new, used or refurbished.
- On Twitter/X, the preview also shows price and stock as readable text beneath the image, matching what stores running Rank Math or the Yoast WooCommerce SEO add-on already show.
- The product photo's size is included too, so previews render at full size right away instead of sometimes needing to be measured first.
- Its description (the featured image's alt text) is included as well, for anyone using a screen reader.
- Nothing to set up. This is read from what you've already filled in: your product's stock status, its Condition attribute if it has one, and your featured image's alt text.
-
Your blog posts and pages now get share previews too (#680).
- On a store with no SEO plugin, sharing a post or page gave whoever received it a blank card: no title, no summary, no image. Your product pages have had these for a while; everything else had nothing.
- Posts and pages now carry a title, a summary taken from your excerpt or the post itself, your featured image, and a description for search results.
- Your store stays out of the way when Yoast, Rank Math, All in One SEO, SEOPress or Jetpack's social previews are already doing this. If you use Jetpack's SEO description without its social previews, you get the share card and keep Jetpack's description, with no duplication.
- Less common SEO plugins are not yet recognised, so if you run one of those you may see two sets of tags. Turning the plugin's metadata off in its settings avoids it, and wider detection is planned.
- Only posts and pages. Author and date archives are left alone, because a made-up summary there is worse than none.
Fixed
-
Search results on your store shared as a blank link, and could share worse than before (#692).
- Someone searching your shop and pasting that link into Slack or Facebook got no preview: no headline, no image, no summary.
- Search pages now get the same kind of preview as your Shop page, headlined with what was searched for, and page two of a long result list no longer claims to be page one.
- On a store using Jetpack's social previews, this was worse than nothing. Your store removed Jetpack's tags from search pages, because it normally replaces them with better ones, but on search it put nothing back. That page now gets a real preview instead.
- Search pages stay out of search-engine results exactly as before. That setting is about crawlers; this is about what people see when a link is shared.
-
Your Shop page could go out with no description at all (#682).
- Product and category pages each fall back to a generated description when you haven't written one. Your Shop page did not, so on a store with an empty tagline and an empty Shop page, which is how WooCommerce ships, it published none.
- It now falls back to naming what you sell, taken from your top product categories: "Shop Hoodies, Tees and Accessories at Saltwarp." Anything you wrote yourself still wins.
-
Page two of your Shop listing claimed to be page one when shared (#682).
- Every paginated Shop page published the same address and headline as the first, so sharing page two showed a preview of page one.
- Both now match the page you are actually on, and the headline agrees with the browser tab. Product category listings get the same fix.
- The shared address is also built from your shop's own permalink now, so sorting or filtering the page no longer changes the address it publishes to social networks.
-
A Shop page containing only a stray space published that as your description (#682).
- Opening your Shop page in the editor, pressing Enter and leaving stores an invisible character. Your store treated that as your description, published it, and skipped both your site tagline and the generated fallback beneath it.
- Leftovers from a deactivated plugin behaved the same way, so a shortcode could end up as your search-result snippet.
- Your store now looks for actual words before publishing anything, and falls through to the next candidate when it does not find any.
-
Your share previews are unaffected when another SEO plugin is installed but silent (#676).
- Rank Math is active from the moment you install it, but publishes nothing until you finish its setup wizard. Yoast and All in One SEO both have a switch that turns their share tags off, which people use when another plugin is handling them.
- In all three cases your store checks that the other plugin actually published something before standing its own tags down, rather than assuming it did because it is installed. If it published nothing, your store publishes its own.
- A free product's price is published as 0.00 rather than 0, because both Rank Math and All in One SEO discard a bare zero on the way out and the price would have vanished.
-
Rank Math and All in One SEO both described your product pages to social networks too (#676).
- Same fix as Yoast, through each plugin's own output rather than a second set of tags. Products are labelled as products; category, shop and search pages as ordinary pages.
- Rank Math was already close. It gets the price on products with options, which it leaves out, and the stock wording Pinterest reads, which it never emits.
- All in One SEO gets price, currency and stock, and its "article" labels are removed from your product and shop pages.
- One thing to know about All in One SEO: it publishes no share tags at all on a product category, so on those pages your store keeps publishing its own.
-
Yoast SEO and your store both described your product pages to social networks (#676).
- With Yoast active, your product, category and shop pages carried two sets of share tags, and Yoast's called every one of them an article. A product is not an article, and neither is a category listing.
- Your store now corrects that inside Yoast's own output rather than publishing a second set. Products are labelled as products, and category, shop and search pages as ordinary pages.
- The commerce facts Yoast leaves out are filled in beside its own: price, currency and stock. If you run the paid Yoast WooCommerce SEO add-on, it already supplies most of these and keeps supplying them; your store only adds what is missing, which on a product with options is the price.
- Yoast's "modified on" date is no longer attached to your product and shop pages. It describes an article, and these are not articles.
- Anything you wrote in Yoast's own SEO fields still wins, exactly as before.
-
SEOPress and your store both described your product pages to social networks (#676).
- With SEOPress active, every product, category and shop page carried two sets of share tags. Networks pick one, so previews looked fine at a glance, but which one you got was not yours to choose.
- Your store now stands SEOPress's social tags down on those pages and publishes its own, which carry the commerce details SEOPress does not: price, stock, and condition.
- Everything else SEOPress does is untouched. Your titles, canonical links, robots directives, meta descriptions and structured data are all still SEOPress's.
- Off commerce pages nothing changes. Your posts and pages are still described entirely by SEOPress.
- Your posts and pages are untouched. This applies only to product, category, shop and search pages.
-
Sharing your Shop page or a product category showed a blank preview (#683).
- The preview card was set up to display a large photo, but no photo was attached to it, so social networks and messaging apps fell back to showing a plain link.
- Your Shop page now uses its own featured image, the one on its edit screen in WordPress, and a product category uses the category image you set in WooCommerce.
- Where you haven't set either, your store picks one of your Featured products instead. That reads a choice you already made, so there is nothing new to fill in, and the picture stays put rather than changing as shoppers page through or re-sort the listing.
- If your store has no image to offer at all, the preview now asks for the small card format rather than promising a photo it cannot supply.
- The image's size is sent along too, so previews render at full size right away instead of sometimes needing to be measured first, and it is left out when your image library cannot report it rather than guessed at.
- Products without a featured image are covered by the same fix. They were asking for the large card too, with nothing to fill it. They now ask for the small one. Products that do have a featured image are unchanged.
-
Your meta description appeared twice when another SEO plugin was active (#669).
- With Yoast SEO, Rank Math, SEOPress or All in One SEO also running, your product, category and shop pages each carried two meta descriptions. Search engines pick one of them, so nothing broke outright, but validators flagged it and the only way to clear it was to deactivate one of the two plugins.
- Your store now checks whether the other SEO plugin is supplying a description for the page it is about to render, and stays quiet when it is. One description per page, whichever plugin wrote it, with nothing to deactivate.
- Where the other plugin supplies nothing, your store still publishes its own, keeping a description on the page in the common case: free Yoast with nothing filled in on most pages, and SEOPress on your Shop page, or on a product category you haven't written a category description for. One gap remains: some SEO plugins have a separate switch that turns descriptions off completely, and it can suppress the other plugin's tag after your store has already stayed quiet for it, leaving the page with none.
...
v0.38.1
Fixed
- Upgrading could create a duplicate Condition attribute (#649).
- Two copies of Condition could appear under Products then Attributes, depending on how busy your store was during the upgrade.
- Upgrading to this version removes the extra copy automatically and keeps your values.
- Don't delete a duplicate yourself from Products then Attributes. WooCommerce removes every value in the attribute when you do that, which empties the copy you keep.
v0.38.0
Added
- You can now record whether a product is new, refurbished or used (#646).
- Google requires a condition on used and refurbished products and disapproves listings without one.
- A Condition attribute is created for you with the three values Google accepts. It isn't applied to any product, so set it on the ones that need it.
- Variations can carry their own condition, so you can list the same item as new and used. A variation left on "Any" publishes nothing, so set every one.
- Already had your own Condition attribute? It is left untouched, so add Google's three values to it if you want them published.
- Selling only new products? Nothing to do.
- You can now mark a product as adult content (#644).
- Google requires adult-oriented products to be labelled and will disapprove them if they are not. There was previously no way to say so.
- Tick Adult content on the product's Inventory tab. Variations follow the parent.
- If your whole store is adult oriented, use the account-level setting in Google Merchant Center instead, under Settings then Business info.
- This is for adult content specifically. Google tells you not to use it for alcohol. Set the product's Google category instead.
v0.37.0
Added
- You can now say which days you dispatch orders (#637).
- Handling time on its own is ambiguous: "ships in 1 day" on a Friday order reads as Saturday dispatch to anything working out a delivery date. Ticking your working days removes the guesswork.
- Set it under Policies → Shipping. Leaving every day unticked publishes nothing, exactly as before — it is not read as "never dispatches".
- Published in both places assistants look: on each product alongside the handling time, and once on your store's shipping policy.
- Your shipping rates are now published for AI assistants and Google (#635).
- Assistants could previously see where you ship and how fast you pack, but nothing about what delivery costs. A product can only carry one shipping price, so a store offering "free over $20, otherwise $20" had no honest number to publish — and we published none rather than a misleading one.
- Your shipping zones now appear on your homepage as a store-level policy, where each destination and order-value band carries its own price. Free-over-a-threshold is published as two bands, so an assistant can tell a shopper both what shipping costs now and what it would cost if they spent a little more.
- Rates that depend on the basket are left out rather than guessed. A per-item cost, or a flat fee combined with a percentage, can't be stated without knowing the whole order, and a published price that disagrees with your checkout is worse than no price at all.
- Where several methods serve the same destination, the cheapest is published, because that is the one Google shows.
- Nothing changes on your product pages, and no new settings were added. This is read entirely from the shipping zones you already have.
Fixed
- Stores on WooCommerce 9.9 to 10.2 could hit a blank page (#638).
- The plugin reads your shipping zones to work out whether you offer free shipping. It used a WooCommerce function that only exists in 10.3 and later, so on older versions the product page and homepage could fail to load rather than simply skipping that detail.
- Zones are now read in a way that works on every WooCommerce version the plugin supports, so nothing is lost on an older install — shipping information is published there too, rather than going quietly missing.
WC tested up towas also stale at 9.9; development runs against 10.9.
v0.36.0
Added
/products.jsonnow emits the full Shopify variant and image shape (#627).- Variants gain
grams,taxable,position,product_id,created_at,updated_atandfeatured_image. Images gainwidth,height,position,product_id,created_at,updated_at,altandvariant_ids— previously each image was just{ id, src }. gramsconverts from the store's configured weight unit rather than assuming kilograms, and rounds to an integer because Shopify types the field as one. An unrecorded weight emits0, matching a live feed where 6 of 413 variants carrygrams: 0and none carrynull.images[]now also includes photos assigned to individual variations. WooCommerce keeps those outside the parent gallery, while Shopify guarantees a variant's image is always one of the product's images. Without this,variant_idswould come back empty on every image for a typical store and the field would do nothing. Expectimages[]to grow on variable products whose variations have their own photos.variant_idsreverses WooCommerce's relation — each variation points at one image; Shopify lists, per image, every variant using it — so one colourway photo maps to all of its sizes. That is how an agent picks the right photo when a shopper chooses a colour.altis emitted in both positions, deliberately diverging from Shopify, which carries it onfeatured_imagebut omits it fromimages[]. Alt text is often the only description of what is actually in a photo, which is exactly what an agent that cannot see the image needs.- A simple product's variant emits
featured_image: nulleven when the product has photos, matching Shopify on 73 of 73 single-variant products measured. The field marks a photo belonging to one variant, and a simple product has no sibling to differ from — its photos are already inimages[]. - Cost: about 8 extra queries on a 17-product store. Those are first reads of variation-owned attachments that were never fetched before because they were never emitted, not repeat reads of existing ones.
- Variants gain
Fixed
-
Simple products were missing their
optionskey (#627).- Shopify emits
optionson every product, including ones with nothing to choose, as[{ "name": "Title", "position": 1, "values": ["Default Title"] }]. We omitted it while still emitting a variant whoseoption1was"Default Title"— half the convention, leaving that value with nothing to say what it meant. - The cost was a crash rather than a missing fact. A client written against Shopify's shape assumes the key exists, so
product.options.map(…)threw andproduct["options"]raisedKeyError— on exactly the products a single-SKU store sells most of. - #627 originally scoped this out as "already at parity". That was measured against a catalogue with zero single-variant products in 250, so the sample could not show it.
- Shopify emits
-
Variable products could emit duplicate variant
positionvalues (#627).- A four-variation product emitted positions
[1, 1, 2, 3]. WooCommerce already returns children sorted by menu order, and its menu order is 0-based while Shopify positions start at 1, so reading the raw value and falling back to a 1-based index for "unset" mixed two numbering schemes and collided. Position is now the loop index alone, which is both dense and unique. - Found by comparing real output against a live store, not by the test suite — every fixture returned menu order 0, which never produced the collision.
- A four-variation product emitted positions
-
Attribute seeding no longer races on every release — it can still race, just rarely (#629).
- Seeding was keyed to the plugin version, and that check runs on every request until one of them writes the new version. After an upgrade, several concurrent requests could each conclude that seeding had not happened yet and each start it. On a live store that produced two
Genderattributes (#628). - It is now keyed to a version of the attribute set rather than of the plugin, and the check happens before any work is scheduled: an already-seeded store schedules nothing at all, rather than scheduling a no-op that could still race.
- The honest comparison: the window did not get narrower.
needs_seeding()is a read, and the flag it checks is written only at the very end ofseed(), after all sixcreate_attribute()calls have run. Pre-fix, the window closed the moment the plugin-version option was written, onplugins_loaded. Post-fix, on the deferred (non-activation) path, it closes later — after the six creates, oninit— which is wider, not narrower. What actually improved is frequency: the window used to open on every release, on every store; now it opens only when a future change to the attribute set bumpsSEED_VERSION. - Activation now calls
seed()directly, which is a real improvement for the common case: only the activating request itself normally runs it, instead of every request racing until one of them wins. It is not race-free, though. WordPress core writes the plugin intoactive_pluginsbefore it fires the activation hook, so a request that lands in that gap loads the plugin, sees the version mismatch, findsneeds_seeding()still true, and defers its own seed run toinit— racing the activation request's inline call. - Seeding is gated on the version change specifically, not on the whole branch. That branch also opens when a merchant toggles the syndication setting, and the toggle path carries the same multi-request exposure — every request sees the flag until the first one clears it. Seeding from a settings save would therefore have re-opened the very race this closes, on any store whose seed flag happened to be stale. A settings toggle is not an install event and has no business provisioning taxonomies.
- The version-mismatch check stays as a backstop, because WordPress does not re-run the activation hook on an in-place upgrade — activation alone can never reach a store that already has the plugin. That backstop has no activation hook to serialise it, so a future
SEED_VERSIONbump reopens the same shape of race #628 hit, across every store, on whichever concurrent post-upgrade request gets there first. - This release attempts one seeding run everywhere, not zero. The new flag has never been written before, so every existing 0.35.0 store finds
needs_seeding()true on this upgrade. It's harmless — #623 already created all six taxonomies, so everycreate_attribute()call returnsfalseand nothing new is created — but it is a real run, not the skip that later releases at the sameSEED_VERSIONwill get. - No merchant action needed: only the pre-release test store ran the affected version.
- Seeding was keyed to the plugin version, and that check runs on every request until one of them writes the new version. After an upgrade, several concurrent requests could each conclude that seeding had not happened yet and each start it. On a live store that produced two
v0.35.0
Added
-
Weight and dimensions now emit on
OfferShippingDetailsand on each variant (#614, #615).- Schema.org defines
weight,height,widthanddepthonOfferShippingDetailsas well as onProduct, and Google draws the same distinction:product_*describes the item,shipping_*describes the parcel. Both now carry the same values. - WooCommerce has exactly one set of dimension fields — filed under the product editor's Shipping tab, and consumed by WooCommerce's own shipping methods to compute rates — so that one set legitimately answers both questions. For a single-item order the numbers are identical; any divergence is packaging overhead, the same approximation Google Merchant Center already accepts.
- Each variant now carries dimensions too. A consumer reading a single
hasVariantentry — the node holding the purchasable offer — previously saw none. Values resolve per variant: its own if set, the parent's otherwise, which is WooCommerce's own getter inheritance rather than logic this plugin wrote. Every variant emits, including inherited values, so a variant read in isolation never looks like it has no shipping data. - Virtual products, whether simple or a variation, continue to emit no shipping/dimension data — the existing
needs_shipping()/has_weight()/has_dimensions()gates are unchanged. Downloadable is a separate WooCommerce checkbox and does not suppress either field: a physical product sold with a bundled download still ships and still carries its dimensions.
- Schema.org defines
-
The six recommended product attributes are now created automatically (#623).
- A fresh WooCommerce store ships with no product attributes at all, so merchants build them ad hoc, name them freely, and type values freely. The plugin now creates
pa_gender,pa_age_group,pa_color,pa_size,pa_materialandpa_patternif they are missing. Seeding is attempted on activation, on every plugin upgrade, and whenever a merchant toggles the syndication enabled setting (the same internal flag that triggers a rewrite-rule flush) — not just on a version change. This is a no-op almost every time, since each attribute is skipped once it exists; the one visible case is a merchant who deliberately deleted one of these attributes seeing it recreated on the next such trigger. Use thewc_ai_storefront_seed_attributesfilter (below) to opt out first if that's not wanted. - Gender and Age group carry Google's complete accepted values (
male/female/unisex, andnewborn/infant/toddler/kids/adult), because Google defines those lists exhaustively. Merchants should not need to add to them. - Color, Size, Material and Pattern carry a small starting set that merchants extend. Google treats these as free text and asks that submitted values match the merchant's own product page, so a canonical list would be wrong. Size deliberately uses abbreviations (e.g.
S,M,L— the full set isXSthrough3XLplusOne size) per Google's consistency guidance, rather than theSmall/Medium/Largeform WooCommerce's own sample data creates. - An attribute that already exists is left completely alone, terms included, decided per attribute rather than all-or-nothing. Existing terms may be variation axes, so adding to or renaming them would break variations and orphan product data.
- New
wc_ai_storefront_seed_attributesfilter returnsfalseto skip seeding entirely.
- A fresh WooCommerce store ships with no product attributes at all, so merchants build them ad hoc, name them freely, and type values freely. The plugin now creates
-
Gender and Age group now emit as a typed
Product.audienceblock (#618).- Google requires
genderandage_groupon all Apparel & Accessories products, and reads them from a typedPeopleAudienceblock, not fromadditionalProperty. A merchant's Gender attribute previously reached the markup only as a genericadditionalPropertyentry — published, but invisible to Google for that purpose. For an apparel product that means disapproval rather than a thinner listing. - Sourced from the
pa_gender/pa_age_groupattributes the plugin now seeds (#623), with a baregender/age_groupcustom attribute accepted as a compatibility fallback. When both forms are present with different values,pa_genderwins over a baregender(same for age group) — order-independent, via apriorityon each map entry. The losing attribute is never discarded; it still emits as its ownadditionalPropertyentry. - Gender is not validated. Any non-empty value emits as
suggestedGender— a value matchingmale/female/unisexis normalised to lowercase, anything else (even a value Google itself rejects, like "Womens") emits trimmed and verbatim.schema:suggestedGenderis Text-ranged, so this is valid markup either way; Google's Merchant Center and Search Console are the intended place to flag a bad value to the merchant, not silent validation here. - Age group IS mapped, and can't follow gender's rule.
suggestedAgeis aQuantitativeValueneedingminValue/maxValue/unitCode, so an unrecognised bucket (e.g. "Grown-up") has no honest numbers to emit and falls back toadditionalPropertyinstead. This is a data-model constraint, not an inconsistency with gender's pass-through behaviour. - Recognised buckets:
newborn(0–3MON),infant(3–12MON),toddler(1–5ANN),kids(5–13ANN),adult(13+ANN, no upper bound — matching Google's own worked example). - Variable products advertise
suggestedGender/suggestedAgeinvariesBywhen that's the varying axis, and each variant carries its own resolvedaudience, inherited per-field from the parent when the variation doesn't define its own.
- Google requires
Fixed
-
Only the dimension axes WooCommerce actually holds are emitted.
has_dimensions()in WooCommerce core is true when any one of length, width or height is set. The emitter published all three off that single gate, and WooCommerce stores an unset axis as an empty string — which casts to0. A merchant who recorded only a height was therefore publishingdepth: 0andwidth: 0: fabricated measurements, presented as real ones.- Each axis is now emitted independently, gated on its own value. A
0the merchant actually typed still emits — the check is for an empty value, not a falsy one, so a deliberate (if unphysical) zero is preserved rather than silently dropped. - Present since dimensions were first emitted, and made more convincing by the numeric cast below:
"depth": ""reads as obviously absent,"depth": 0reads as a measurement. Caught in review before it reached three placements instead of one.
-
Product dimension values now emit as JSON numbers instead of quoted strings (#613).
depth,width, andheightpublished theirQuantitativeValue.valueas a string literal ("value": "10") whileweighton the same product published a number ("value": 1.5). Two adjacent properties in one block disagreed on type.QuantitativeValue.valueis Number-ranged. The markup was not invalid, since the property also acceptsText, but a consumer that doesn't coerce read a string where every sibling gave a number.- The weight cast was added as audit bug #4 for exactly this reason — WooCommerce persists both weight and dimensions as free-form decimal strings (
.5,10).get_dimensions( false )returns those props untouched, and the fix was never applied to the dimension branch. All three now cast through(float). - No change to which products emit dimensions, to unit codes, or to any other field.
Changed
-
Dev tooling: bump
squizlabs/php_codesnifferto 3.13.6 for CVE-2026-67434 (#611). An OS-command-injection advisory (high severity, affecting<3.13.6and>=4.0.0,<4.0.2) was published 2026-08-05 and failed thecomposer auditCI gate on every open PR. The package isrequire-devonly and is not shipped in the plugin zip, so no released version exposed merchants to it; the risk was confined to running lint locally or in CI. -
Dev tooling: clear ten npm advisories from the dev dependency tree (#616). Transitive
devDependenciesonly — nothing in the tree reaches the built bundle. Six alerts remain and are tracked in #596:extract-ziphas no upstream fix at all, andadm-zip,@opentelemetry/coreandwebpack-dev-servereach need a@wordpress/scriptssemver-major that would be a larger change than the risk warrants. All are dev-scope, and the CI security gate scopes npm auditing to production dependencies.
Tests
JsonLdTest.php— newtest_dimension_and_weight_values_encode_as_json_numbersasserts on thewp_json_encodeoutput rather than the array. The pre-existing dimension test usedassertEquals( '10', … ), which passes against both a string and a float under loose comparison and so could not detect this defect in either direction; the quotes are only visible after encoding. Also assertsassertIsFloatper value and that noQuantitativeValueanywhere in the markup encodes its value quoted.
v0.34.3
Fixed
- Out-of-stock products no longer advertise a buy link that errors at the cart (#606).
- An out-of-stock product emitted
potentialAction(BuyAction) andoffers[].checkoutPageURLTemplatealongsideavailability: OutOfStock. Following that advertised URL didn't merely dead-end — it landed on an empty cart carrying?wc_error=You cannot add "…" to the cart because the product is out of stock, so an AI agent or search crawler acting on the published JSON-LD reached a broken flow. - Both emission sites gated on
is_purchasable()alone, which in WooCommerce core isexists && published && has a priceand never consults stock — whileWC_Cart::add_to_cart()rejects on! is_in_stock(). The two gates were checking different things. Buy-link emission now additionally requiresis_in_stock(), at both the parent/simple path and the per-variant path. - Descriptive fields (
@id,name,sku,image,offers[].price,offers[].availability) still emit, so agents continue to see that the product exists and why it can't be bought — they just aren't handed a URL that fails. - Backordered products keep their buy links. The predicate is
is_in_stock(), not a stock-quantity test: a backordered product reports in-stock, WooCommerce accepts it at cart-add, and its availability isBackOrder. Gating on quantity would have suppressed exactly the oversold-but-orderable variants corrected in #601.
- An out-of-stock product emitted
v0.34.2
Fixed
- Backordered variants no longer emit
availability: InStockalongside a negativeinventoryLevel(#601).- A variable product oversold under an allow-backorders setting published
availability: https://schema.org/InStockon every variant Offer while the same Offer carriedinventoryLevel.value: -4, so the two fields told an AI agent contradictory stories about the same variant. WC_Product::is_in_stock()is a lossy two-state view of WooCommerce's three-statestock_status— it reports'outofstock' !== $stock_status, soonbackorderreads astrue. The per-variant Offer builder branched on that bool alone; the inventory emitter readget_stock_quantity()directly, which is not lossy. Variant Offers now map the full three states, matching WooCommerce core's ownWC_Structured_Data, which checks the backorder case ahead of plain in-stock and has done so since WC 7.8 — comfortably below this plugin's WC 9.9 floor.- Only per-variant Offers were affected. Simple products and parent Offers are built by WooCommerce core, which already handled this correctly across the whole supported WC range. The UCP and
products.jsonsurfaces express availability as a required bool per spec and are unchanged. inventoryLevel.valueis now clamped to0instead of publishing a negative quantity. schema.org defines the property as the "current approximate inventory level", so0misrepresents nothing it promised to be exact, andavailability: BackOrdercarries the "still orderable" signal instead. A level of exactly0remains meaningful ("none on hand") and is still emitted; only an untracked (null) quantity suppresses the property.
- A variable product oversold under an allow-backorders setting published
Changed
- Dev tooling: bump
wp-coding-standards/wpcsto 3.4.1 for CVE-2026-45293. The WordPressCS arbitrary-code-execution advisory (high severity, affecting>=0.14.1,<3.4.1) was published 2026-07-28 and failed thecomposer auditCI gate on every open PR. The package isrequire-devonly — it is the PHPCS ruleset and is not shipped in the plugin zip — so no released version exposed users to it; the risk was confined to running lint locally or in CI.composer.jsonalready declared the constraint as*, so onlycomposer.lockmoved.phpcsstandards/phpcsutils(1.2.2 → 1.2.3) andphpcsstandards/phpcsextra(1.5.0 → 1.5.1) are bumped alongside it, because wpcs 3.4.1 requires them; a lockfile update naming wpcs alone silently resolves backwards to 0.14.0 (which predates the advisory range) instead of forwards. Repo-wide PHPCS is clean on the new ruleset with no code changes.
Docs
- JSON-LD field reference updated for the three-state availability mapping and the
inventoryLevelclamp (#601).JSON-LD-SCHEMA.mdnow documents why the out-of-stock branch is checked first (thewoocommerce_product_is_in_stockfilter can decouple the bool fromstock_status), that Google's merchant-listing spec never readsinventoryLevel, and the shared-pool caveat — when stock is managed on a variable parent, every variant reports the same inherited number, which is faithful to WooCommerce but is not a per-variant figure.