Skip to content

Releases: Automattic/woocommerce-ai-storefront

v0.40.0

Choose a tag to compare

@github-actions github-actions released this 26 Aug 01:00

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

Choose a tag to compare

@github-actions github-actions released this 24 Aug 02:09

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.json feed 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".
  • 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

Choose a tag to compare

@github-actions github-actions released this 23 Aug 05:05

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.
      ...
Read more

v0.38.1

Choose a tag to compare

@github-actions github-actions released this 19 Aug 02:56

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

Choose a tag to compare

@github-actions github-actions released this 18 Aug 19:40

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

Choose a tag to compare

@github-actions github-actions released this 17 Aug 04:56

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 to was also stale at 9.9; development runs against 10.9.

v0.36.0

Choose a tag to compare

@github-actions github-actions released this 15 Aug 04:44

Added

  • /products.json now emits the full Shopify variant and image shape (#627).
    • Variants gain grams, taxable, position, product_id, created_at, updated_at and featured_image. Images gain width, height, position, product_id, created_at, updated_at, alt and variant_ids — previously each image was just { id, src }.
    • grams converts 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 emits 0, matching a live feed where 6 of 413 variants carry grams: 0 and none carry null.
    • 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_ids would come back empty on every image for a typical store and the field would do nothing. Expect images[] to grow on variable products whose variations have their own photos.
    • variant_ids reverses 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.
    • alt is emitted in both positions, deliberately diverging from Shopify, which carries it on featured_image but omits it from images[]. 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: null even 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 in images[].
    • 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.

Fixed

  • Simple products were missing their options key (#627).

    • Shopify emits options on every product, including ones with nothing to choose, as [{ "name": "Title", "position": 1, "values": ["Default Title"] }]. We omitted it while still emitting a variant whose option1 was "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 and product["options"] raised KeyError — 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.
  • Variable products could emit duplicate variant position values (#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.
  • 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 Gender attributes (#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 of seed(), after all six create_attribute() calls have run. Pre-fix, the window closed the moment the plugin-version option was written, on plugins_loaded. Post-fix, on the deferred (non-activation) path, it closes later — after the six creates, on init — 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 bumps SEED_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 into active_plugins before it fires the activation hook, so a request that lands in that gap loads the plugin, sees the version mismatch, finds needs_seeding() still true, and defers its own seed run to init — 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_VERSION bump 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 every create_attribute() call returns false and nothing new is created — but it is a real run, not the skip that later releases at the same SEED_VERSION will get.
    • No merchant action needed: only the pre-release test store ran the affected version.

v0.35.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 19:42

Added

  • Weight and dimensions now emit on OfferShippingDetails and on each variant (#614, #615).

    • Schema.org defines weight, height, width and depth on OfferShippingDetails as well as on Product, 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 hasVariant entry — 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.
  • 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_material and pa_pattern if 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 the wc_ai_storefront_seed_attributes filter (below) to opt out first if that's not wanted.
    • Gender and Age group carry Google's complete accepted values (male/female/unisex, and newborn/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 is XS through 3XL plus One size) per Google's consistency guidance, rather than the Small/Medium/Large form 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_attributes filter returns false to skip seeding entirely.
  • Gender and Age group now emit as a typed Product.audience block (#618).

    • Google requires gender and age_group on all Apparel & Accessories products, and reads them from a typed PeopleAudience block, not from additionalProperty. A merchant's Gender attribute previously reached the markup only as a generic additionalProperty entry — 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_group attributes the plugin now seeds (#623), with a bare gender / age_group custom attribute accepted as a compatibility fallback. When both forms are present with different values, pa_gender wins over a bare gender (same for age group) — order-independent, via a priority on each map entry. The losing attribute is never discarded; it still emits as its own additionalProperty entry.
    • Gender is not validated. Any non-empty value emits as suggestedGender — a value matching male/female/unisex is normalised to lowercase, anything else (even a value Google itself rejects, like "Womens") emits trimmed and verbatim. schema:suggestedGender is 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. suggestedAge is a QuantitativeValue needing minValue/maxValue/unitCode, so an unrecognised bucket (e.g. "Grown-up") has no honest numbers to emit and falls back to additionalProperty instead. This is a data-model constraint, not an inconsistency with gender's pass-through behaviour.
    • Recognised buckets: newborn (0–3 MON), infant (3–12 MON), toddler (1–5 ANN), kids (5–13 ANN), adult (13+ ANN, no upper bound — matching Google's own worked example).
    • Variable products advertise suggestedGender / suggestedAge in variesBy when that's the varying axis, and each variant carries its own resolved audience, inherited per-field from the parent when the variation doesn't define its own.

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 to 0. A merchant who recorded only a height was therefore publishing depth: 0 and width: 0: fabricated measurements, presented as real ones.
    • Each axis is now emitted independently, gated on its own value. A 0 the 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": 0 reads 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, and height published their QuantitativeValue.value as a string literal ("value": "10") while weight on the same product published a number ("value": 1.5). Two adjacent properties in one block disagreed on type.
    • QuantitativeValue.value is Number-ranged. The markup was not invalid, since the property also accepts Text, 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_codesniffer to 3.13.6 for CVE-2026-67434 (#611). An OS-command-injection advisory (high severity, affecting <3.13.6 and >=4.0.0,<4.0.2) was published 2026-08-05 and failed the composer audit CI gate on every open PR. The package is require-dev only 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 devDependencies only — nothing in the tree reaches the built bundle. Six alerts remain and are tracked in #596: extract-zip has no upstream fix at all, and adm-zip, @opentelemetry/core and webpack-dev-server each need a @wordpress/scripts semver-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 — new test_dimension_and_weight_values_encode_as_json_numbers asserts on the wp_json_encode output rather than the array. The pre-existing dimension test used assertEquals( '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 asserts assertIsFloat per value and that no QuantitativeValue anywhere in the markup encodes its value quoted.

v0.34.3

Choose a tag to compare

@github-actions github-actions released this 29 Jul 04:32

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) and offers[].checkoutPageURLTemplate alongside availability: 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 is exists && published && has a price and never consults stock — while WC_Cart::add_to_cart() rejects on ! is_in_stock(). The two gates were checking different things. Buy-link emission now additionally requires is_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 is BackOrder. Gating on quantity would have suppressed exactly the oversold-but-orderable variants corrected in #601.

v0.34.2

Choose a tag to compare

@github-actions github-actions released this 29 Jul 01:22

Fixed

  • Backordered variants no longer emit availability: InStock alongside a negative inventoryLevel (#601).
    • A variable product oversold under an allow-backorders setting published availability: https://schema.org/InStock on every variant Offer while the same Offer carried inventoryLevel.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-state stock_status — it reports 'outofstock' !== $stock_status, so onbackorder reads as true. The per-variant Offer builder branched on that bool alone; the inventory emitter read get_stock_quantity() directly, which is not lossy. Variant Offers now map the full three states, matching WooCommerce core's own WC_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.json surfaces express availability as a required bool per spec and are unchanged.
    • inventoryLevel.value is now clamped to 0 instead of publishing a negative quantity. schema.org defines the property as the "current approximate inventory level", so 0 misrepresents nothing it promised to be exact, and availability: BackOrder carries the "still orderable" signal instead. A level of exactly 0 remains meaningful ("none on hand") and is still emitted; only an untracked (null) quantity suppresses the property.

Changed

  • Dev tooling: bump wp-coding-standards/wpcs to 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 the composer audit CI gate on every open PR. The package is require-dev only — 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.json already declared the constraint as *, so only composer.lock moved. phpcsstandards/phpcsutils (1.2.2 → 1.2.3) and phpcsstandards/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 inventoryLevel clamp (#601). JSON-LD-SCHEMA.md now documents why the out-of-stock branch is checked first (the woocommerce_product_is_in_stock filter can decouple the bool from stock_status), that Google's merchant-listing spec never reads inventoryLevel, 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.