ENHANCEMENT: Product/Offer JSON-LD structured data for membership levels - #3735
Conversation
… levels Emit schema.org Product + Offer markup on levels listing and checkout pages (and Single Membership Level blocks) so membership pricing is machine-readable for Google product snippets and AI agents. Includes a settings toggle to hide output when another SEO plugin already covers schema. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address Codex PR review: free-to-start levels advertise recurring price, stable product @id without discount codes, no discount leakage in URLs, idempotent print guard, stricter context validation, currency decimal places. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…data - Trial levels use trial_amount for Offer.price and omit incomplete recurring priceSpecification - has_block gate before parse_blocks on every singular page - Normalize cycle_period casing for unit codes - Only scan post content on is_singular() - Drop dead discount-in-URL filter branch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
This idea came up before. Funny thing, I was having Flint look into the "Agents Ready" stuff and he kind of accidentally did a pass RE how to do the structured data for membership levels. So I said, go ahead and make the PR. This looks good. I'm testing the output in Google's tester here: https://search.google.com/test/rich-results We can pull onto our live site for more testing. Someone should double/triple check the pricing logic (picking the price when there are trials/etc is a little tricky, but I think the default logic there is sound). We can second guess the filters. They seem okay for me. One of them is around a "level image", which isn't a thing. But an image for the structured data seems very important (for showing up in Google shopping) and so people will want to insert images into the structured data to make it look good there. In fact, we should consider bundling this with a featured image for membership levels feature (use it here and as the og:image when viewing a checkout for a single level). |
Google Rich Results Test marks missing image as a critical Product error. Fallback chain: site icon → custom logo → bundled PMPro banner. Also add Brand and a short description fallback. Filter still overrides image. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Image is only required for Google merchant listing experiences, not the product snippets this markup targets. Publishing the bundled PMPro banner as a customer site's product image misrepresents the marked-up content, so omit the image when the site has no icon or logo. Per-level images can plug into the existing filter/fallback later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Offer.price now only uses trial_amount when it is nonzero, falling through to the free-to-start rule so a paid membership with a free trial is not marked up as a free product. The recurring UnitPriceSpecification is now always emitted (matching its docblock), with billingStart indicating how many periods pass before the regular rate begins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an optional Level Image media selector to the edit level page, stored as an attachment ID in level meta. Structured data uses it as the Product image ahead of the site icon / custom logo fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I did a full review of this PR and pushed three follow-up commits. Overall the implementation was solid — output is correctly gated, safely encoded, discount codes never leak into crawlable URLs, and I verified the emitted schema across one-time, recurring, multi-year-cycle, free-to-start, finite-limit, and discount-adjusted levels on a test site. Summary of the changes I pushed: 6e4a088 — Image fallback no longer uses the PMPro banner. An image is only required for Google's merchant listing experience, not the product snippets track this PR targets (it's recommended there). The Rich Results Test error that motivated the banner fallback comes from the merchant-listings section of the test. Publishing our own marketing banner as the product image on customer sites would misrepresent the marked-up content (and be the identical image across every site using the fallback), so the chain is now: site icon → custom logo → omit. 4eb9113 — Trial levels now publish the ongoing rate. Previously a level like "$0 now, free first month, then $50/month" emitted d32373f — Level Image setting. Following up on the image discussion above: levels now have an optional image, selected from the media library on the edit level screen and stored as an attachment ID in level meta. Structured data uses it ahead of the site icon/logo fallback. The hint text notes it isn't shown at checkout. This also sets us up to reuse it as One known limitation to be aware of when testing on the live site: WordPress canonicals strip query args, so every Also note the PR description is now slightly stale on two points: trials no longer omit the recurring |
InStock is the conventional availability value shopping tools key on; OnlineOnly mainly signals absence of in-store purchase, which is the odd one out for product snippets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All Submissions:
Changes proposed in this Pull Request:
Adds membership-level structured data (JSON-LD) to PMPro core so levels are machine-readable for Google product snippets and AI shopping/browsing agents. Also adds an optional Level Image setting on the edit level page, used as the
Productimage in the structured data.New file:
includes/structured-data.php(plus Level Image field inadminpages/levels/edit-level.php/save-level.phpand the hide toggle inadminpages/advancedsettings.php)Where it outputs:
[pmpro_levels]/pmpro/levels-page→ItemListofProductsProduct+Offer(uses checkout-adjusted level incl. discount-adjusted price)pmpro/single-levelblocks → Product(s) / ItemListDoes not auto-output for:
[pmpro_membership_level]field shortcodes (not a full product surface)pmpro_structured_data_multi_level_checkout_context)Schema choices (doc-checked against Google Product + schema.org):
Product+ nestedOffer(product snippets track; not full merchant-listing with shipping/returns)Offer.pricepriority:initial_paymentif > 0 → elsetrial_amountif a paid trial → else recurringbilling_amountif free-to-start (including free trials) → else 0UnitPriceSpecificationwithpriceComponentType: Subscription,billingIncrement,unitCode(DAY/WEE/MON/ANN), optionalbillingDurationwhenbilling_limit > 0UnitPriceSpecificationis still emitted, withbillingStartindicating how many periods pass before the regular rate beginsOffer.availability:InStockOffer.seller(notProduct.provider)image: Level Image (level meta, set on the edit level page) → site icon → custom logo → omitted (image is only required for merchant listings, not product snippets)@idwithout discount codes; discount codes never appear in crawlable offer URLsJSON_HEX_TAG|AMP|APOS|QUOTSettings:
Filters:
pmpro_structured_data_enabled,pmpro_structured_data_context,pmpro_structured_data_schema,pmpro_structured_data_offer,pmpro_structured_data_seller,pmpro_structured_data_item_list_schema,pmpro_structured_data_product_image,pmpro_structured_data_multi_level_checkout_context.Known limitation: WordPress canonicals strip query args, so
checkout/?pmpro_level=NURLs all declare the bare checkout page as canonical and Google may consolidate them. The stable SEO surfaces are the levels page (ItemList) and dedicated per-level landing pages using thepmpro/single-levelblock; treat checkout JSON-LD as best-effort. See PR comments for details.How to test the changes in this Pull Request:
ItemListJSON-LD with each signup-eligible level.priceSpecification(unitCodeMON/ANN/…)."price": "0".priceSpecificationpresent withbillingStart. Level with a free trial → Offer.price uses the recurring billing_amount (not 0).imageuses it. Remove it → falls back to site icon / custom logo, or omitsimagewhen neither exists.Other information:
flintdev.testPlan + PR reviewed by Codex gpt-5.6-sol; PR Review Council run processed (trial pricing major + nits fixed in follow-up commits). Follow-up commits from review (trial pricing, image fallback, Level Image setting,
InStockavailability) verified against a local site across one-time, recurring, multi-year-cycle, free-to-start, finite-limit, trial, and discount-adjusted levels — see PR comments.Changelog entry