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